From 74d7be083d4dbea001f1dbf4908ce2f3d280525f Mon Sep 17 00:00:00 2001 From: Ambrose Chua Date: Sat, 22 May 2021 22:24:28 +0800 Subject: [PATCH] Working uploader --- .gitignore | 2 + Dockerfile | 21 ++ handle-create-multipart-upload.go | 72 ----- handle.go | 47 --- handlers.go | 236 +++++++++++++++ helpers.go | 3 + main.go | 8 +- s3-initiate-multipart-upload.go | 47 --- s3-signing.go | 27 -- s3.go | 227 +++++++++++++- web/index.html | 5 + web/package-lock.json | 478 +++++++++++++++++++++++++++++- web/package.json | 4 +- web/rollup.config.js | 19 +- web/src/log.css | 27 ++ web/src/log.js | 65 ++++ web/src/main.css | 26 +- web/src/main.js | 15 +- 18 files changed, 1113 insertions(+), 216 deletions(-) create mode 100644 Dockerfile delete mode 100644 handle-create-multipart-upload.go delete mode 100644 handle.go create mode 100644 handlers.go delete mode 100644 s3-initiate-multipart-upload.go delete mode 100644 s3-signing.go create mode 100644 web/src/log.css create mode 100644 web/src/log.js 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 @@
+
+

Completed

+ +
+
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(); });