diff --git a/credential.go b/credential.go new file mode 100644 index 0000000..0b2d9a6 --- /dev/null +++ b/credential.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "strings" +) + +/* types */ + +type credential struct { + AccessKey string + SecretKey string + // Region is critical when signing requests. + Region string + // Endpoint is the base URL of the bucket, including the bucket name (in either the domain or path). + // + // Example: + // https://bucketname.s3.us-west-2.amazonaws.com + // http://my-minio.example.com/bucket-name + Endpoint string + // ACL is an optional canned ACL to set on objects + ACL string + // Prefix is a string to prepend to object keys + Prefix string +} + +func (cred credential) validate() error { + if strings.HasSuffix(cred.Endpoint, "/") { + return fmt.Errorf("%w: endpoint should not end with slash", errBadRequest) + } + if strings.HasPrefix(cred.Prefix, "/") { + return fmt.Errorf("%w: prefix should not start with slash", errBadRequest) + } + return nil +} diff --git a/handlers-s3.go b/handlers-s3.go index 6a87fd2..8e1ff56 100644 --- a/handlers-s3.go +++ b/handlers-s3.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "net/url" - "os" "strconv" "github.com/gorilla/mux" @@ -40,6 +39,13 @@ type createMultipartUploadRes struct { } func handleCreateMultipartUpload(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + cred, err := getCredential(vars["id"]) + if err != nil { + errorResponse(w, req, err) + return + } + r := createMultipartUploadReq{} decoder := json.NewDecoder(req.Body) if err := decoder.Decode(&r); err != nil { @@ -52,14 +58,6 @@ func handleCreateMultipartUpload(w http.ResponseWriter, req *http.Request) { return } - cred := credential{ - AccessKey: os.Getenv("MINIO_ACCESS_KEY"), - SecretKey: os.Getenv("MINIO_SECRET_KEY"), - Region: os.Getenv("MINIO_REGION_NAME"), - Endpoint: os.Getenv("MINIO_ENDPOINT"), - Prefix: os.Getenv("PREFIX"), - } - // Derive the object key key := cred.Prefix + r.Filename @@ -82,6 +80,12 @@ type getUploadedPartsRes []part func handleGetUploadedParts(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) + cred, err := getCredential(vars["id"]) + if err != nil { + errorResponse(w, req, err) + return + } + uploadID := vars["uploadID"] key := req.URL.Query().Get("key") @@ -90,14 +94,6 @@ func handleGetUploadedParts(w http.ResponseWriter, req *http.Request) { return } - cred := credential{ - AccessKey: os.Getenv("MINIO_ACCESS_KEY"), - SecretKey: os.Getenv("MINIO_SECRET_KEY"), - Region: os.Getenv("MINIO_REGION_NAME"), - Endpoint: os.Getenv("MINIO_ENDPOINT"), - Prefix: os.Getenv("PREFIX"), - } - parts := make(getUploadedPartsRes, 0, 0) var nextPartNumberMarker uint32 for { @@ -127,6 +123,12 @@ type signPartUploadRes struct { func handleSignPartUpload(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) + cred, err := getCredential(vars["id"]) + if err != nil { + errorResponse(w, req, err) + return + } + uploadID := vars["uploadID"] key := req.URL.Query().Get("key") partNumber, err := strconv.ParseUint(vars["uploadPart"], 10, 16) @@ -140,14 +142,6 @@ func handleSignPartUpload(w http.ResponseWriter, req *http.Request) { return } - cred := credential{ - AccessKey: os.Getenv("MINIO_ACCESS_KEY"), - SecretKey: os.Getenv("MINIO_SECRET_KEY"), - Region: os.Getenv("MINIO_REGION_NAME"), - Endpoint: os.Getenv("MINIO_ENDPOINT"), - Prefix: os.Getenv("PREFIX"), - } - params := make(url.Values) params.Add("partNumber", strconv.FormatUint(partNumber, 10)) params.Add("uploadId", uploadID) @@ -182,6 +176,12 @@ func (r completeMultipartUploadReq) validate() error { func handleCompleteMultipartUpload(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) + cred, err := getCredential(vars["id"]) + if err != nil { + errorResponse(w, req, err) + return + } + uploadID := vars["uploadID"] key := req.URL.Query().Get("key") @@ -202,14 +202,6 @@ func handleCompleteMultipartUpload(w http.ResponseWriter, req *http.Request) { return } - cred := credential{ - AccessKey: os.Getenv("MINIO_ACCESS_KEY"), - SecretKey: os.Getenv("MINIO_SECRET_KEY"), - Region: os.Getenv("MINIO_REGION_NAME"), - Endpoint: os.Getenv("MINIO_ENDPOINT"), - Prefix: os.Getenv("PREFIX"), - } - result, err := completeMultipartUpload(key, uploadID, r.Parts, cred) if err != nil { errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err)) @@ -224,6 +216,12 @@ func handleCompleteMultipartUpload(w http.ResponseWriter, req *http.Request) { func handleAbortMultipartUpload(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) + cred, err := getCredential(vars["id"]) + if err != nil { + errorResponse(w, req, err) + return + } + uploadID := vars["uploadID"] key := req.URL.Query().Get("key") @@ -232,15 +230,7 @@ func handleAbortMultipartUpload(w http.ResponseWriter, req *http.Request) { return } - cred := credential{ - AccessKey: os.Getenv("MINIO_ACCESS_KEY"), - SecretKey: os.Getenv("MINIO_SECRET_KEY"), - Region: os.Getenv("MINIO_REGION_NAME"), - Endpoint: os.Getenv("MINIO_ENDPOINT"), - Prefix: os.Getenv("PREFIX"), - } - - err := abortMultipartUpload(key, uploadID, cred) + err = abortMultipartUpload(key, uploadID, cred) if err != nil { errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err)) return diff --git a/handlers.go b/handlers.go index 5f96d35..726b6d1 100644 --- a/handlers.go +++ b/handlers.go @@ -1,9 +1,14 @@ package main import ( + "encoding/json" + "errors" "html/template" "net/http" "os" + "time" + + "github.com/gorilla/mux" ) var globalStore store @@ -16,6 +21,38 @@ func setupHandlers() { } } +/* credentials */ + +func getCredential(id string) (credential, error) { + cred := credential{} + + b, err := globalStore.get(id) + if err != nil { + return cred, err + } + + err = json.Unmarshal(b, &cred) + if err != nil { + return cred, err + } + + return cred, nil +} + +func setCredential(id string, cred credential, expire time.Duration) error { + b, err := json.Marshal(cred) + if err != nil { + return err + } + + err = globalStore.put(id, b, expire) + if err != nil { + return err + } + + return nil +} + /* templates */ var tmpl = template.Must(template.ParseFS(assets, "web/*.tmpl")) @@ -23,5 +60,23 @@ var tmpl = template.Must(template.ParseFS(assets, "web/*.tmpl")) /* upload template */ func handleUpload(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + _, err := getCredential(vars["id"]) + if errors.Is(err, errNotFound) { + errorResponseStatus(w, req, err) + tmpl.ExecuteTemplate(w, "upload-not-found.tmpl", nil) + return + } + if err != nil { + errorResponse(w, req, err) + return + } + tmpl.ExecuteTemplate(w, "upload.tmpl", nil) } + +/* create template */ + +func handleCreate(w http.ResponseWriter, req *http.Request) { + tmpl.ExecuteTemplate(w, "create.tmpl", nil) +} diff --git a/helpers.go b/helpers.go index eef3816..10dc5a8 100644 --- a/helpers.go +++ b/helpers.go @@ -10,7 +10,7 @@ var errNotFound = errors.New("not found") var errBadRequest = errors.New("bad request") var errInternalServerError = errors.New("internal server error") -func errorResponse(w http.ResponseWriter, req *http.Request, err error) { +func errorResponseStatus(w http.ResponseWriter, req *http.Request, err error) { errorMessage := err.Error() errorStatus := http.StatusInternalServerError @@ -22,6 +22,11 @@ func errorResponse(w http.ResponseWriter, req *http.Request, err error) { errorStatus = http.StatusInternalServerError } - log.Printf("%s %s: %s", req.Method, req.URL.Path, errorMessage) + log.Printf("%s %s: %s", req.Method, req.RequestURI, errorMessage) w.WriteHeader(errorStatus) } + +func errorResponse(w http.ResponseWriter, req *http.Request, err error) { + errorResponseStatus(w, req, err) + w.Write([]byte(err.Error())) +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..4e50b21 --- /dev/null +++ b/logger.go @@ -0,0 +1,15 @@ +package main + +import ( + "log" + "net/http" +) + +func middlewareLogger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Printf("%s: %s", r.Method, r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) +} diff --git a/main.go b/main.go index 017a987..4b9753a 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "io/fs" + "log" "net/http" "os" "time" @@ -20,16 +21,22 @@ func main() { panic(err) } - setupS3() setupHandlers() + setupS3() router := mux.NewRouter() - uploadRouter := router.PathPrefix("/{id}").Subrouter() - router.PathPrefix("/assets").Handler(http.FileServer(http.FS(assetsWeb))) + router.Use(middlewareLogger) - uploadRouter.Path("").HandlerFunc(handleUpload) + router.Methods(http.MethodGet).PathPrefix("/assets").Handler(http.FileServer(http.FS(assetsWeb))) + router.Methods(http.MethodGet).Path("/favicon.ico").Handler(http.FileServer(http.FS(assetsWeb))) + router.Methods(http.MethodGet).Path("/").HandlerFunc(handleCreate) + uploadRouter := router.PathPrefix("/{id}").Subrouter() + + uploadTemplateRouter := uploadRouter.Path("").Subrouter() s3Router := uploadRouter.PathPrefix("/s3/multipart").Subrouter() + uploadTemplateRouter.Methods(http.MethodGet).Path("").HandlerFunc(handleUpload) + s3Router.Methods(http.MethodPost).Path("").HandlerFunc(handleCreateMultipartUpload) s3Router.Methods(http.MethodGet).Path("/{uploadID}").HandlerFunc(handleGetUploadedParts) s3Router.Methods(http.MethodGet).Path("/{uploadID}/{uploadPart}").HandlerFunc(handleSignPartUpload) @@ -42,6 +49,7 @@ func main() { ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } + log.Printf("listeining on %s", listen) err = server.ListenAndServe() if err != nil { panic(err) diff --git a/s3.go b/s3.go index 52da727..288d020 100644 --- a/s3.go +++ b/s3.go @@ -52,32 +52,6 @@ func stripETag(t string) string { return strings.TrimSuffix(strings.TrimPrefix(t, "\""), "\"") } -/* types */ - -type credential struct { - AccessKey string - SecretKey string - // Region is critical when signing requests. - Region string - // Endpoint is the base URL of the bucket, including the bucket name (in either the domain or path). - // - // Example: - // https://bucketname.s3.us-west-2.amazonaws.com - // http://my-minio.example.com/bucket-name - Endpoint string - // ACL is an optional canned ACL to set on objects - ACL string - // Prefix is a string to prepend to object keys - Prefix string -} - -func (cred credential) validate() error { - if strings.HasSuffix(cred.Endpoint, "/") { - return fmt.Errorf("%w: endpoint should not end with slash", errBadRequest) - } - return nil -} - /* initiateMultipartUpload */ type initiateMultipartUploadResult struct { diff --git a/store.go b/store.go index 12a8419..a9d7309 100644 --- a/store.go +++ b/store.go @@ -12,6 +12,7 @@ import ( var errKeyCollision = errors.New("key collision") var errKeyNotFound = fmt.Errorf("key %w", errNotFound) +var errInvalidConnectionType = errors.New("invalid connection type") type store interface { put(key string, data []byte, expire time.Duration) error @@ -38,15 +39,14 @@ func newRedisStore(connection string) (*redisStore, error) { var err error if rtype == "simple" { client, err = (radix.PoolConfig{}).New(ctx, "tcp", connectionParts[1]) - if err != nil { - return nil, nil - } } else if rtype == "cluster" { clusterAddrs := strings.Split(connectionParts[1], ",") client, err = (radix.ClusterConfig{}).New(ctx, clusterAddrs) + } else { + err = fmt.Errorf("%w: %#v of string %#v", errInvalidConnectionType, rtype, connection) } if err != nil { - return nil, nil + return nil, fmt.Errorf("unable to initialize redis store: %w", err) } return &redisStore{client}, nil } @@ -56,7 +56,7 @@ func (s *redisStore) put(key string, data []byte, expire time.Duration) error { defer cancel() exists := 0 - err := s.client.Do(ctx, radix.Cmd(&exists, "EXISTS", key)) + err := s.client.Do(ctx, radix.Cmd(&exists, "EXISTS", "upl:"+key)) if err != nil { return err } @@ -78,7 +78,7 @@ func (s *redisStore) get(key string) ([]byte, error) { defer cancel() var data []byte - err := s.client.Do(ctx, radix.Cmd(&data, "GET", key)) + err := s.client.Do(ctx, radix.Cmd(&data, "GET", "upl:"+key)) if err != nil { return nil, err } diff --git a/web/src/log.js b/web/src/log.js index 54801fd..f8bb0ed 100644 --- a/web/src/log.js +++ b/web/src/log.js @@ -3,7 +3,8 @@ import filesize from 'filesize'; import './log.css'; class Log { - constructor(selector) { + constructor(selector, key) { + this.key = key; this.target = document.querySelector(selector); this.items = []; @@ -12,12 +13,12 @@ class Log { } localStorageLoad() { - const loaded = JSON.parse(window.localStorage.getItem("log") || "[]"); + const loaded = JSON.parse(window.localStorage.getItem("log" + this.key) || "[]"); this.items.push(...loaded); } localStorageSave() { - window.localStorage.setItem("log", JSON.stringify(this.items)); + window.localStorage.setItem("log" + this.key, JSON.stringify(this.items)); } static renderItem(item) { diff --git a/web/src/main.js b/web/src/main.js index 5850950..178bfaa 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -25,7 +25,7 @@ uppy.use(AwsS3Multipart, { companionUrl: window.location.pathname, }); -const log = new Log('#log-area'); +const log = new Log('#log-area', window.location.pathname); uppy.on('upload-success', (f, res) => { log.add({ diff --git a/web/upload-not-found.tmpl b/web/upload-not-found.tmpl new file mode 100644 index 0000000..3ae42b4 --- /dev/null +++ b/web/upload-not-found.tmpl @@ -0,0 +1,16 @@ + + + + + + Dropbox Not Found + + + +
+
+

Dropbox Not Found

+

The dropbox you are looking for doesn't exist or has expired.

+
+
+