-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
149 lines (134 loc) · 3.36 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package main
import (
"context"
"errors"
"io"
"log"
"net/http"
"strconv"
"strings"
"cloud.google.com/go/storage"
"github.com/julienschmidt/httprouter"
"github.com/spf13/pflag"
"go.uber.org/zap"
"google.golang.org/api/option"
)
type Server struct {
Address string
Bucket string
Cred string
Username string
Password string
Debug bool
gcs *storage.Client
logger *zap.Logger
}
func (s *Server) Init() error {
if s.Bucket == "" {
return errors.New("--bucket required")
}
if s.Username == "" {
return errors.New("--username required")
}
if s.Password == "" {
return errors.New("--password required")
}
var err error
ctx := context.Background()
// init GCS client
opts := make([]option.ClientOption, 0)
if s.Cred != "" {
opts = append(opts, option.WithCredentialsFile(s.Cred))
}
s.gcs, err = storage.NewClient(ctx, opts...)
if err != nil {
return err
}
// init logger
cfg := zap.NewProductionConfig()
if s.Debug {
cfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
}
s.logger, err = cfg.Build()
if err != nil {
return err
}
return nil
}
func (s Server) handleError(w http.ResponseWriter, err error) {
if err == storage.ErrObjectNotExist {
s.logger.Debug("object not found")
http.Error(w, err.Error(), http.StatusNotFound)
return
}
s.logger.Error("error getting object", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
}
func (s Server) Handle(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// authorize
user, pass, ok := r.BasicAuth()
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
if !ok {
http.Error(w, "Not authorized", 401)
return
}
if user != s.Username || pass != s.Password {
http.Error(w, "Not authorized", 401)
return
}
// get object metainfo
oname := strings.TrimPrefix(ps.ByName("object"), "/")
if strings.HasSuffix(oname, "/") {
oname += "index.html"
}
s.logger.Debug("getting object", zap.String("name", oname))
ctx := context.Background()
bucket := s.gcs.Bucket(s.Bucket)
obj := bucket.Object(oname)
attrs, err := obj.Attrs(ctx)
if err != nil {
s.handleError(w, err)
return
}
// write headers
w.Header().Add("Content-Type", attrs.ContentType)
w.Header().Add("Content-Length", strconv.FormatInt(attrs.Size, 10))
// write body
objr, err := obj.NewReader(ctx)
if err != nil {
s.handleError(w, err)
return
}
_, err = io.Copy(w, objr)
if err != nil {
s.logger.Warn("cannot write response body", zap.Error(err))
}
}
func main() {
// init server
var err error
s := Server{}
pflag.StringVar(&s.Address, "addr", "127.0.0.1:8080", "address to serve")
pflag.StringVar(&s.Bucket, "bucket", "", "bucket to serve")
pflag.StringVar(&s.Cred, "cred", "", "path to gcloud credential file")
pflag.StringVar(&s.Username, "username", "", "username for basic HTTP auth")
pflag.StringVar(&s.Password, "password", "", "password for basic HTTP auth")
pflag.BoolVar(&s.Debug, "debug", false, "show debug logs")
pflag.Parse()
err = s.Init()
if err != nil {
log.Fatalf("cannot init server: %v", err)
}
defer func() {
err := s.logger.Sync()
if err != nil {
log.Fatalf("cannot sync logger: %v", err)
}
}()
// serve
router := httprouter.New()
router.GET("/*object", s.Handle)
s.logger.Info("listening", zap.String("addr", s.Address))
err = http.ListenAndServe(s.Address, router)
s.logger.Error("server has stopped", zap.Error(err))
}