1
0
Fork 0

Working uploader

upload-progress
Ambrose Chua 2021-05-22 22:24:28 +08:00
parent c91fa732f8
commit 74d7be083d
18 changed files with 1113 additions and 216 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
node_modules
web/assets
.env

21
Dockerfile Normal file
View File

@ -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"]

View File

@ -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,
})
}

View File

@ -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)
}
}

236
handlers.go Normal file
View File

@ -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) {
}

View File

@ -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)

View File

@ -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,

View File

@ -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
}

View File

@ -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
}

227
s3.go
View File

@ -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
}

View File

@ -11,6 +11,11 @@
<div class="upload">
<div id="drop-area"></div>
<div id="status-area"></div>
<div id="log-header">
<h4>Completed</h4>
<button id="log-clear">Clear</button>
</div>
<div id="log-area"></div>
</div>
</div>

478
web/package-lock.json generated
View File

@ -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",

View File

@ -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"
}
}

View File

@ -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 }),
],
};

27
web/src/log.css Normal file
View File

@ -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;
}

65
web/src/log.js Normal file
View File

@ -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;

View File

@ -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;
}

View File

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