diff --git a/.gitignore b/.gitignore
index 72e20c0..6181b05 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
node_modules
web/assets
+
+.env
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..6c8193f
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,21 @@
+FROM node:16-alpine3.13 as build-web
+
+WORKDIR /src
+COPY . .
+RUN cd web && npm install
+RUN cd web && npm run build
+
+FROM golang:1.16-alpine3.13 as build
+
+ARG CGO_ENABLED=0
+
+WORKDIR /src
+COPY --from=build-web . .
+RUN go build -ldflags="-s -w" -v
+
+FROM scratch
+
+COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
+COPY --from=build upl /upl
+
+RUN ["/upl"]
diff --git a/handle-create-multipart-upload.go b/handle-create-multipart-upload.go
deleted file mode 100644
index efa28fc..0000000
--- a/handle-create-multipart-upload.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "os"
-)
-
-type createMultipartUploadReq struct {
- Filename string `json:"filename"`
- Type string `json:"type"`
- Metadata createMultipartUploadReqMetadata `json:"metadata"`
-}
-
-func (r createMultipartUploadReq) validate() error {
- if r.Filename == "" {
- return errors.New("invalid filename")
- } else if r.Type == "" {
- return errors.New("invalid content type")
- }
- return nil
-}
-
-type createMultipartUploadReqMetadata struct {
- Name string `json:"name"`
- Type string `json:"type"`
-}
-
-type createMultipartUploadRes struct {
- Key string `json:"key"`
- UploadID string `json:"uploadId"`
-}
-
-func handleCreateMultipartUpload(w http.ResponseWriter, req *http.Request) {
- defer req.Body.Close()
-
- r := createMultipartUploadReq{}
- decoder := json.NewDecoder(req.Body)
- if err := decoder.Decode(&r); err != nil {
- errorResponse(w, req, fmt.Errorf("%w: %s", errBadRequest, err))
- return
- }
-
- if err := r.validate(); err != nil {
- errorResponse(w, req, fmt.Errorf("%w: %s", errBadRequest, err))
- return
- }
-
- // Derive the object key
- // TODO: configurable
- key := fmt.Sprintf("uploads/%s", r.Filename)
-
- 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"),
- }
- uploadID, err := createMultipartUpload(key, cred)
- if err != nil {
- errorResponse(w, req, fmt.Errorf("%w: %s", errBadRequest, err))
- return
- }
-
- encoder := json.NewEncoder(w)
- encoder.Encode(createMultipartUploadRes{
- Key: key,
- UploadID: uploadID,
- })
-}
diff --git a/handle.go b/handle.go
deleted file mode 100644
index 246c49d..0000000
--- a/handle.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package main
-
-import (
- "net/http"
- "os"
-)
-
-func getUploadedParts(w http.ResponseWriter, req *http.Request) {
-}
-
-func signPartUpload(w http.ResponseWriter, req *http.Request) {
- method := http.MethodGet
- url := "https://minio1.makerforce.io/test"
-
- unsignedReq, err := http.NewRequest(method, url, nil)
- if err != nil {
- w.WriteHeader(500)
- 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"),
- }
-
- signedReq := preSign(unsignedReq, cred)
-
- w.Write([]byte(signedReq.URL.String()))
-}
-
-func completeMultipartUpload(w http.ResponseWriter, req *http.Request) {
-}
-
-func abortMultipartUpload(w http.ResponseWriter, req *http.Request) {
-}
-
-var globalStore store
-
-func setupHandlers() {
- var err error
- globalStore, err = newRedisStore(os.Getenv("REDIS_CONNECTION"))
- if err != nil {
- panic(err)
- }
-}
diff --git a/handlers.go b/handlers.go
new file mode 100644
index 0000000..ac53505
--- /dev/null
+++ b/handlers.go
@@ -0,0 +1,236 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "strconv"
+
+ "github.com/gorilla/mux"
+)
+
+var globalStore store
+
+func setupHandlers() {
+ var err error
+ globalStore, err = newRedisStore(os.Getenv("REDIS_CONNECTION"))
+ if err != nil {
+ panic(err)
+ }
+}
+
+/* createMultipartUpload */
+
+type createMultipartUploadReq struct {
+ Filename string `json:"filename"`
+ Type string `json:"type"`
+ Metadata createMultipartUploadReqMetadata `json:"metadata"`
+}
+
+func (r createMultipartUploadReq) validate() error {
+ if r.Filename == "" {
+ return errors.New("invalid filename")
+ } else if r.Type == "" {
+ return errors.New("invalid content type")
+ }
+ return nil
+}
+
+type createMultipartUploadReqMetadata struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+}
+
+type createMultipartUploadRes struct {
+ Key string `json:"key"`
+ UploadID string `json:"uploadId"`
+}
+
+func handleCreateMultipartUpload(w http.ResponseWriter, req *http.Request) {
+ r := createMultipartUploadReq{}
+ decoder := json.NewDecoder(req.Body)
+ if err := decoder.Decode(&r); err != nil {
+ errorResponse(w, req, fmt.Errorf("%w: %s", errBadRequest, err))
+ return
+ }
+
+ if err := r.validate(); err != nil {
+ errorResponse(w, req, fmt.Errorf("%w: %s", errBadRequest, err))
+ 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
+
+ result, err := initiateMultipartUpload(key, cred)
+ if err != nil {
+ errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err))
+ return
+ }
+
+ encoder := json.NewEncoder(w)
+ encoder.Encode(createMultipartUploadRes{
+ Key: key,
+ UploadID: result.UploadID,
+ })
+}
+
+/* getUploadedParts */
+
+type getUploadedPartsRes []part
+
+func handleGetUploadedParts(w http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ uploadID := vars["id"]
+ key := req.URL.Query().Get("key")
+
+ if uploadID == "" || key == "" {
+ errorResponse(w, req, fmt.Errorf("%w", errBadRequest))
+ 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 {
+ page, err := listParts(key, uploadID, cred, nextPartNumberMarker)
+ if err != nil {
+ errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err))
+ return
+ }
+
+ parts = append(parts, page.Parts...)
+ nextPartNumberMarker = page.NextPartNumberMarker
+
+ if !page.IsTruncated {
+ break
+ }
+ }
+
+ encoder := json.NewEncoder(w)
+ encoder.Encode(getUploadedPartsRes(parts))
+}
+
+/* signPartUpload */
+
+type signPartUploadRes struct {
+ URL string `json:"url"`
+}
+
+func handleSignPartUpload(w http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ uploadID := vars["id"]
+ key := req.URL.Query().Get("key")
+ partNumber, err := strconv.ParseUint(vars["part"], 10, 16)
+
+ if uploadID == "" || key == "" {
+ errorResponse(w, req, fmt.Errorf("%w", errBadRequest))
+ return
+ }
+ if partNumber < 1 || partNumber > 10000 || err != nil {
+ errorResponse(w, req, fmt.Errorf("%w: invalid part number", errBadRequest))
+ 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)
+ unsignedReq, err := http.NewRequest(http.MethodPut, cred.Endpoint+"/"+key+"?"+params.Encode(), nil)
+ if err != nil {
+ errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err))
+ return
+ }
+
+ signedReq := preSign(unsignedReq, cred)
+
+ encoder := json.NewEncoder(w)
+ encoder.Encode(signPartUploadRes{
+ URL: signedReq.URL.String(),
+ })
+}
+
+/* completeMultipartUpload */
+
+type completeMultipartUploadReq struct {
+ Parts []completePart `json:"parts"`
+}
+
+func (r completeMultipartUploadReq) validate() error {
+ for _, part := range r.Parts {
+ if err := part.validate(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func handleCompleteMultipartUpload(w http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ uploadID := vars["id"]
+ key := req.URL.Query().Get("key")
+
+ if uploadID == "" || key == "" {
+ errorResponse(w, req, fmt.Errorf("%w", errBadRequest))
+ return
+ }
+
+ r := completeMultipartUploadReq{}
+ decoder := json.NewDecoder(req.Body)
+ if err := decoder.Decode(&r); err != nil {
+ errorResponse(w, req, fmt.Errorf("%w: %s", errBadRequest, err))
+ return
+ }
+
+ if err := r.validate(); err != nil {
+ errorResponse(w, req, fmt.Errorf("%w: %s", errBadRequest, err))
+ 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))
+ return
+ }
+
+ encoder := json.NewEncoder(w)
+ encoder.Encode(result)
+}
+
+/* abortMultipartUpload */
+
+func handleAbortMultipartUpload(w http.ResponseWriter, req *http.Request) {
+}
diff --git a/helpers.go b/helpers.go
index 12a1219..eef3816 100644
--- a/helpers.go
+++ b/helpers.go
@@ -8,6 +8,7 @@ import (
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) {
errorMessage := err.Error()
@@ -17,6 +18,8 @@ func errorResponse(w http.ResponseWriter, req *http.Request, err error) {
errorStatus = http.StatusNotFound
} else if errors.Is(err, errBadRequest) {
errorStatus = http.StatusBadRequest
+ } else if errors.Is(err, errInternalServerError) {
+ errorStatus = http.StatusInternalServerError
}
log.Printf("%s %s: %s", req.Method, req.URL.Path, errorMessage)
diff --git a/main.go b/main.go
index 8431869..394491e 100644
--- a/main.go
+++ b/main.go
@@ -28,10 +28,10 @@ func main() {
router.PathPrefix("/").Handler(http.FileServer(http.FS(assetsWeb)))
multipartRouter.HandleFunc("", handleCreateMultipartUpload).Methods(http.MethodPost)
- multipartRouter.HandleFunc("/{id}", getUploadedParts).Methods(http.MethodGet)
- multipartRouter.HandleFunc("/{id}/{part}", signPartUpload).Methods(http.MethodGet)
- multipartRouter.HandleFunc("/{id}/complete", completeMultipartUpload).Methods(http.MethodPost)
- multipartRouter.HandleFunc("", abortMultipartUpload).Methods(http.MethodDelete)
+ multipartRouter.HandleFunc("/{id}", handleGetUploadedParts).Methods(http.MethodGet)
+ multipartRouter.HandleFunc("/{id}/{part}", handleSignPartUpload).Methods(http.MethodGet)
+ multipartRouter.HandleFunc("/{id}/complete", handleCompleteMultipartUpload).Methods(http.MethodPost)
+ multipartRouter.HandleFunc("", handleAbortMultipartUpload).Methods(http.MethodDelete)
server := &http.Server{
Handler: router,
diff --git a/s3-initiate-multipart-upload.go b/s3-initiate-multipart-upload.go
deleted file mode 100644
index 2140ee2..0000000
--- a/s3-initiate-multipart-upload.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package main
-
-import (
- "context"
- "encoding/xml"
- "fmt"
- "net/http"
- "time"
-)
-
-type initiateMultipartUploadResult struct {
- Bucket string
- Key string
- UploadID string `xml:"UploadId"`
-}
-
-func createMultipartUpload(key string, cred credential) (string, error) {
- ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
- defer cancel()
-
- unsignedReq, err := http.NewRequestWithContext(ctx, http.MethodPost, cred.Endpoint+"/"+key+"?uploads", nil)
- if err != nil {
- return "", err
- }
- if cred.ACL != "" {
- unsignedReq.Header.Set("X-Amz-Acl", cred.ACL)
- }
-
- signedReq := sign(unsignedReq, cred)
- resp, err := httpClientS3.Do(signedReq)
- if err != nil {
- return "", err
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return "", fmt.Errorf("endpoint request failed: %d", resp.StatusCode)
- }
-
- initiateMultipartUploadResult := initiateMultipartUploadResult{}
- decoder := xml.NewDecoder(resp.Body)
- err = decoder.Decode(&initiateMultipartUploadResult)
- if err != nil {
- return "", err
- }
-
- return initiateMultipartUploadResult.UploadID, nil
-}
diff --git a/s3-signing.go b/s3-signing.go
deleted file mode 100644
index 7ae07ea..0000000
--- a/s3-signing.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package main
-
-import (
- "net/http"
-
- "github.com/minio/minio-go/v7/pkg/signer"
-)
-
-func preSign(req *http.Request, cred credential) *http.Request {
- signedReq := signer.PreSignV4(
- *req,
- cred.AccessKey, cred.SecretKey, "",
- cred.Region,
- 60*60, // seconds
- )
- return signedReq
-}
-
-func sign(req *http.Request, cred credential) *http.Request {
- req.Header.Set("X-Amz-Content-Sha256", "UNSIGNED-PAYLOAD")
- signedReq := signer.SignV4(
- *req,
- cred.AccessKey, cred.SecretKey, "",
- cred.Region,
- )
- return signedReq
-}
diff --git a/s3.go b/s3.go
index 63a48a5..12a5b4b 100644
--- a/s3.go
+++ b/s3.go
@@ -1,12 +1,59 @@
package main
import (
+ "bytes"
+ "context"
+ "encoding/xml"
+ "errors"
"fmt"
+ "io/ioutil"
"net/http"
+ "net/url"
+ "strconv"
"strings"
"time"
+
+ "github.com/minio/minio-go/v7/pkg/signer"
)
+var httpClientS3 *http.Client
+
+func setupS3() {
+ httpClientS3 = &http.Client{
+ Timeout: 10 * time.Second,
+ }
+}
+
+/* signing */
+
+func preSign(req *http.Request, cred credential) *http.Request {
+ signedReq := signer.PreSignV4(
+ *req,
+ cred.AccessKey, cred.SecretKey, "",
+ cred.Region,
+ 60*60, // seconds
+ )
+ return signedReq
+}
+
+func sign(req *http.Request, cred credential) *http.Request {
+ req.Header.Set("X-Amz-Content-Sha256", "UNSIGNED-PAYLOAD")
+ signedReq := signer.SignV4(
+ *req,
+ cred.AccessKey, cred.SecretKey, "",
+ cred.Region,
+ )
+ return signedReq
+}
+
+/* helpers */
+
+func stripETag(t string) string {
+ return strings.TrimSuffix(strings.TrimPrefix(t, "\""), "\"")
+}
+
+/* types */
+
type credential struct {
AccessKey string
SecretKey string
@@ -20,6 +67,8 @@ type credential struct {
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 {
@@ -29,10 +78,178 @@ func (cred credential) validate() error {
return nil
}
-var httpClientS3 *http.Client
+/* initiateMultipartUpload */
-func setupS3() {
- httpClientS3 = &http.Client{
- Timeout: 10 * time.Second,
- }
+type initiateMultipartUploadResult struct {
+ Bucket string
+ Key string
+ UploadID string `xml:"UploadId"`
+}
+
+func initiateMultipartUpload(key string, cred credential) (initiateMultipartUploadResult, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+ defer cancel()
+
+ params := make(url.Values)
+ params.Set("uploads", "")
+ unsignedReq, err := http.NewRequestWithContext(ctx, http.MethodPost, cred.Endpoint+"/"+key+"?"+params.Encode(), nil)
+ if err != nil {
+ return initiateMultipartUploadResult{}, err
+ }
+ if cred.ACL != "" {
+ unsignedReq.Header.Set("X-Amz-Acl", cred.ACL)
+ }
+
+ signedReq := sign(unsignedReq, cred)
+ resp, err := httpClientS3.Do(signedReq)
+ if err != nil {
+ return initiateMultipartUploadResult{}, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ body, _ := ioutil.ReadAll(resp.Body)
+ return initiateMultipartUploadResult{}, fmt.Errorf("endpoint request failed: %d: %s", resp.StatusCode, body)
+ }
+
+ result := initiateMultipartUploadResult{}
+ decoder := xml.NewDecoder(resp.Body)
+ err = decoder.Decode(&result)
+ if err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
+
+/* listParts */
+
+type part struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Part" json:"-"`
+ PartNumber uint16 `json:"PartNumber"`
+ ETag string `json:"ETag"`
+ Size uint32 `json:"Size"`
+}
+
+type listPartsResult struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListPartsResult" json:"-"`
+ Bucket string
+ Key string
+ UploadID string `xml:"UploadId"`
+ // not implemented: Initiator
+ // not implemented: Owner
+ // not implemented: StorageClass
+ PartNumberMarker uint32
+ NextPartNumberMarker uint32
+ MaxParts uint32
+ IsTruncated bool
+ Parts []part `xml:"Part"`
+}
+
+func listParts(key, uploadID string, cred credential, partNumberMarker uint32) (listPartsResult, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+ defer cancel()
+
+ params := make(url.Values)
+ params.Set("max-parts", "1000")
+ params.Set("part-number-marker", strconv.FormatUint(uint64(partNumberMarker), 10))
+ params.Set("uploadId", uploadID)
+ unsignedReq, err := http.NewRequestWithContext(ctx, http.MethodGet, cred.Endpoint+"/"+key+"?"+params.Encode(), nil)
+ if err != nil {
+ return listPartsResult{}, err
+ }
+
+ signedReq := sign(unsignedReq, cred)
+ resp, err := httpClientS3.Do(signedReq)
+ if err != nil {
+ return listPartsResult{}, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ body, _ := ioutil.ReadAll(resp.Body)
+ return listPartsResult{}, fmt.Errorf("endpoint request failed: %d: %s", resp.StatusCode, body)
+ }
+
+ result := listPartsResult{}
+ decoder := xml.NewDecoder(resp.Body)
+ err = decoder.Decode(&result)
+ if err != nil {
+ return result, err
+ }
+
+ for i := range result.Parts {
+ result.Parts[i].ETag = strings.TrimSuffix(strings.TrimPrefix(result.Parts[i].ETag, "\""), "\"")
+ }
+
+ return result, nil
+}
+
+/* completeMultipartUpload */
+
+type completeMultipartUploadBody struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUpload" json:"-"`
+ Parts []completePart `xml:"Part"`
+}
+
+type completePart struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Part" json:"-"`
+ PartNumber uint16 `json:"PartNumber"`
+ ETag string `json:"ETag"`
+}
+
+func (r completePart) validate() error {
+ if r.PartNumber < 1 || r.PartNumber > 10000 {
+ return errors.New("invalid part number")
+ } else if r.ETag == "" {
+ return errors.New("invalid etag")
+ }
+ return nil
+}
+
+type completeMultipartUploadResult struct {
+ Location string
+ Bucket string
+ Key string
+ ETag string
+}
+
+func completeMultipartUpload(key, uploadID string, parts []completePart, cred credential) (completeMultipartUploadResult, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+ defer cancel()
+
+ var body bytes.Buffer
+ complete := completeMultipartUploadBody{Parts: parts}
+ b := xml.NewEncoder(&body)
+ err := b.Encode(complete)
+ if err != nil {
+ return completeMultipartUploadResult{}, err
+ }
+
+ params := make(url.Values)
+ params.Set("uploadId", uploadID)
+ unsignedReq, err := http.NewRequestWithContext(ctx, http.MethodPost, cred.Endpoint+"/"+key+"?"+params.Encode(), &body)
+ if err != nil {
+ return completeMultipartUploadResult{}, err
+ }
+
+ signedReq := sign(unsignedReq, cred)
+ resp, err := httpClientS3.Do(signedReq)
+ if err != nil {
+ return completeMultipartUploadResult{}, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ body, _ := ioutil.ReadAll(resp.Body)
+ return completeMultipartUploadResult{}, fmt.Errorf("endpoint request failed: %d: %s", resp.StatusCode, body)
+ }
+
+ result := completeMultipartUploadResult{}
+ decoder := xml.NewDecoder(resp.Body)
+ err = decoder.Decode(&result)
+ if err != nil {
+ return result, err
+ }
+
+ result.ETag = stripETag(result.ETag)
+
+ return result, nil
}
diff --git a/web/index.html b/web/index.html
index e5375bc..1c7a74d 100644
--- a/web/index.html
+++ b/web/index.html
@@ -11,6 +11,11 @@
diff --git a/web/package-lock.json b/web/package-lock.json
index 77a3cdb..110c63c 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -14,8 +14,36 @@
"@uppy/core": "^1.18.1",
"@uppy/drag-drop": "^1.4.27",
"@uppy/status-bar": "^1.9.3",
+ "filesize": "^6.3.0",
"rollup": "^2.48.0",
- "rollup-plugin-import-css": "^2.0.1"
+ "rollup-plugin-import-css": "^2.0.1",
+ "rollup-plugin-terser": "^7.0.2"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
+ "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.12.13"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
+ "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
+ "dev": true
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
+ "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.14.0",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
}
},
"node_modules/@rollup/plugin-commonjs": {
@@ -183,6 +211,18 @@
"integrity": "sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==",
"dev": true
},
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -199,6 +239,12 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
"node_modules/builtin-modules": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
@@ -208,12 +254,47 @@
"node": ">=6"
}
},
+ "node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==",
"dev": true
},
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -241,12 +322,30 @@
"node": ">=0.10.0"
}
},
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/estree-walker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
"dev": true
},
+ "node_modules/filesize": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.3.0.tgz",
+ "integrity": "sha512-ytx0ruGpDHKWVoiui6+BY/QMNngtDQ/pJaFwfBpQif0J63+E8DLdFyqS3NkKQn7vIruUEpoGD9JUJSg7Kp+I0g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -301,6 +400,15 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -341,6 +449,47 @@
"@types/estree": "*"
}
},
+ "node_modules/jest-worker": {
+ "version": "26.6.2",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
+ "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
@@ -356,6 +505,12 @@
"sourcemap-codec": "^1.4.4"
}
},
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
"node_modules/mime-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz",
@@ -434,6 +589,15 @@
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -474,12 +638,113 @@
"@rollup/pluginutils": "^3.1.0"
}
},
+ "node_modules/rollup-plugin-terser": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
+ "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "jest-worker": "^26.2.1",
+ "serialize-javascript": "^4.0.0",
+ "terser": "^5.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.0.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/serialize-javascript": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
+ "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
+ "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.7.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz",
+ "integrity": "sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==",
+ "dev": true,
+ "dependencies": {
+ "commander": "^2.20.0",
+ "source-map": "~0.7.2",
+ "source-map-support": "~0.5.19"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/url-parse": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
@@ -504,6 +769,32 @@
}
},
"dependencies": {
+ "@babel/code-frame": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
+ "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.12.13"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
+ "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
+ "dev": true
+ },
+ "@babel/highlight": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
+ "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.14.0",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
"@rollup/plugin-commonjs": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-19.0.0.tgz",
@@ -662,6 +953,15 @@
"integrity": "sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==",
"dev": true
},
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -678,18 +978,56 @@
"concat-map": "0.0.1"
}
},
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
"builtin-modules": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
"integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
"dev": true
},
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
"classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==",
"dev": true
},
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
"commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -714,12 +1052,24 @@
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true
},
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
"estree-walker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
"dev": true
},
+ "filesize": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.3.0.tgz",
+ "integrity": "sha512-ytx0ruGpDHKWVoiui6+BY/QMNngtDQ/pJaFwfBpQif0J63+E8DLdFyqS3NkKQn7vIruUEpoGD9JUJSg7Kp+I0g==",
+ "dev": true
+ },
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -762,6 +1112,12 @@
"function-bind": "^1.1.1"
}
},
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -802,6 +1158,40 @@
"@types/estree": "*"
}
},
+ "jest-worker": {
+ "version": "26.6.2",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
+ "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
"lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
@@ -817,6 +1207,12 @@
"sourcemap-codec": "^1.4.4"
}
},
+ "merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
"mime-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz",
@@ -886,6 +1282,15 @@
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
+ "randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -920,12 +1325,83 @@
"@rollup/pluginutils": "^3.1.0"
}
},
+ "rollup-plugin-terser": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
+ "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "jest-worker": "^26.2.1",
+ "serialize-javascript": "^4.0.0",
+ "terser": "^5.0.0"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
+ "serialize-javascript": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
+ "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "source-map": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
+ "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
+ "dev": true
+ },
+ "source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "terser": {
+ "version": "5.7.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz",
+ "integrity": "sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==",
+ "dev": true,
+ "requires": {
+ "commander": "^2.20.0",
+ "source-map": "~0.7.2",
+ "source-map-support": "~0.5.19"
+ }
+ },
"url-parse": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
diff --git a/web/package.json b/web/package.json
index 1e7d89c..1d96fd5 100644
--- a/web/package.json
+++ b/web/package.json
@@ -14,7 +14,9 @@
"@uppy/core": "^1.18.1",
"@uppy/drag-drop": "^1.4.27",
"@uppy/status-bar": "^1.9.3",
+ "filesize": "^6.3.0",
"rollup": "^2.48.0",
- "rollup-plugin-import-css": "^2.0.1"
+ "rollup-plugin-import-css": "^2.0.1",
+ "rollup-plugin-terser": "^7.0.2"
}
}
diff --git a/web/rollup.config.js b/web/rollup.config.js
index 42cfb11..03fff41 100644
--- a/web/rollup.config.js
+++ b/web/rollup.config.js
@@ -1,9 +1,20 @@
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
-import css from "rollup-plugin-import-css";
+import css from 'rollup-plugin-import-css';
+import { terser } from 'rollup-plugin-terser';
export default {
- input: "src/main.js",
- output: { file: "assets/bundle.js", format: "iife" },
- plugins: [ commonjs(), resolve({ browser: true }), css() ]
+ input: 'src/main.js',
+ output: {
+ file: 'assets/bundle.js',
+ format: 'iife',
+ plugins: [
+ terser(),
+ ],
+ },
+ plugins: [
+ commonjs(),
+ resolve({ browser: true }),
+ css({ minify: true }),
+ ],
};
diff --git a/web/src/log.css b/web/src/log.css
new file mode 100644
index 0000000..85bbdb7
--- /dev/null
+++ b/web/src/log.css
@@ -0,0 +1,27 @@
+.log-item {
+ display: flex;
+ align-items: center;
+ margin: 0.5rem 0;
+
+ border: 2px solid #adadad;
+ border-radius: 7px;
+}
+
+.log-url {
+ padding: 0.5rem;
+ flex: 1;
+ width: 10rem;
+
+ appearance: none;
+ outline: none;
+ border: none;
+ background: none;
+}
+
+.log-size {
+ padding: 0.25rem 0.5rem;
+
+ white-space: nowrap;
+ font-weight: 600;
+ font-size: 0.8em;
+}
diff --git a/web/src/log.js b/web/src/log.js
new file mode 100644
index 0000000..54801fd
--- /dev/null
+++ b/web/src/log.js
@@ -0,0 +1,65 @@
+import filesize from 'filesize';
+
+import './log.css';
+
+class Log {
+ constructor(selector) {
+ this.target = document.querySelector(selector);
+ this.items = [];
+
+ this.localStorageLoad();
+ this.render();
+ }
+
+ localStorageLoad() {
+ const loaded = JSON.parse(window.localStorage.getItem("log") || "[]");
+ this.items.push(...loaded);
+ }
+
+ localStorageSave() {
+ window.localStorage.setItem("log", JSON.stringify(this.items));
+ }
+
+ static renderItem(item) {
+ const base = document.createElement('div');
+ base.classList.add('log-item');
+
+ const url = document.createElement('input');
+ url.value = item.location;
+ url.setAttribute("readonly", "");
+ url.classList.add('log-url');
+ url.addEventListener("click", (e) => {
+ e.target.setSelectionRange(0, e.target.value.length);
+ });
+ base.appendChild(url);
+
+ const size = document.createElement('span');
+ size.innerText = filesize(item.size);
+ size.classList.add('log-size');
+ base.appendChild(size);
+
+ return base;
+ }
+
+ render() {
+ const elements = this.items.map(this.constructor.renderItem);
+ this.target.innerHTML = "";
+ elements.forEach(element => {
+ this.target.appendChild(element);
+ });
+ }
+
+ add(item) {
+ this.items.push(item);
+ this.localStorageSave();
+ this.target.appendChild(this.constructor.renderItem(item));
+ }
+
+ clear() {
+ this.items = [];
+ this.localStorageSave();
+ this.render();
+ }
+}
+
+export default Log;
diff --git a/web/src/main.css b/web/src/main.css
index a15b46d..472eab9 100644
--- a/web/src/main.css
+++ b/web/src/main.css
@@ -1,26 +1,38 @@
+*, *:before, *:after {
+ box-sizing: border-box;
+}
+
html {
height: 100%;
}
+html, input {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+}
+
body {
height: 100%;
margin: 0;
-
- display: flex;
- justify-content: center;
- align-items: center;
}
.upload-wrapper {
max-width: 48rem;
width: 100%;
+ margin: 0 auto;
}
.upload {
- margin: 2rem;
+ padding: 2rem;
}
-#drop-area {
- height: 16rem;
+#log-header {
+ margin-top: 2rem;
+
+ display: flex;
+}
+
+#log-header h4 {
+ flex: 1;
+ margin: 0.5rem 0;
}
diff --git a/web/src/main.js b/web/src/main.js
index a0be6c0..5045bbf 100644
--- a/web/src/main.js
+++ b/web/src/main.js
@@ -8,11 +8,14 @@ import '@uppy/drag-drop/dist/style.css';
import '@uppy/status-bar/dist/style.css';
import './main.css';
+import Log from './log';
+
const uppy = new Uppy({
autoProceed: true,
});
uppy.use(DragDrop, {
target: '#drop-area',
+ height: '16rem',
});
uppy.use(StatusBar, {
target: '#status-area',
@@ -22,6 +25,16 @@ uppy.use(AwsS3Multipart, {
companionUrl: '.',
});
+const log = new Log('#log-area');
+
uppy.on('upload-success', (f, res) => {
- console.log(f, res);
+ log.add({
+ name: f.name,
+ size: f.size,
+ location: res.body.Location,
+ });
+});
+
+document.querySelector('#log-clear').addEventListener('click', () => {
+ log.clear();
});