Form handler, saver and style timprovements
parent
bce86f4e20
commit
7f274c0e10
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -25,12 +26,45 @@ type credential struct {
|
|||
Prefix string
|
||||
}
|
||||
|
||||
func newCredential(endpoint, region, accessKey, secretKey, prefix, acl string) credential {
|
||||
parsedEndpoint, _ := url.Parse(endpoint)
|
||||
return credential{
|
||||
Endpoint: parsedEndpoint.String(),
|
||||
Region: region,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Prefix: prefix,
|
||||
ACL: acl,
|
||||
}
|
||||
}
|
||||
|
||||
func (cred credential) validate() error {
|
||||
parsedEndpoint, err := url.Parse(cred.Endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: endpoint must be a URL and not empty", errBadRequest)
|
||||
} else if parsedEndpoint.Host == "" {
|
||||
return fmt.Errorf("%w: endpoint must have a valid host", errBadRequest)
|
||||
} else if parsedEndpoint.User != nil {
|
||||
return fmt.Errorf("%w: endpoint must not have user credentials", errBadRequest)
|
||||
} else if parsedEndpoint.RawQuery != "" {
|
||||
return fmt.Errorf("%w: endpoint must not have query parameters", errBadRequest)
|
||||
} else if parsedEndpoint.RawFragment != "" {
|
||||
return fmt.Errorf("%w: endpoint must not have fragment", errBadRequest)
|
||||
} else if parsedEndpoint.Scheme != "http" && parsedEndpoint.Scheme != "https" {
|
||||
return fmt.Errorf("%w: endpoint must be http(s)", errBadRequest)
|
||||
}
|
||||
|
||||
if cred.Region == "" {
|
||||
return fmt.Errorf("%w: region must not be empty", errBadRequest)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(cred.Endpoint, "/") {
|
||||
return fmt.Errorf("%w: endpoint should not end with slash", errBadRequest)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cred.Prefix, "/") {
|
||||
return fmt.Errorf("%w: prefix should not start with slash", errBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ func handleCreateMultipartUpload(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
if err := r.validate(); err != nil {
|
||||
errorResponse(w, req, fmt.Errorf("%w: %s", errBadRequest, err))
|
||||
errorResponse(w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ func handleCreateMultipartUpload(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
result, err := initiateMultipartUpload(key, cred)
|
||||
if err != nil {
|
||||
errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err))
|
||||
errorResponse(w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -109,7 +109,7 @@ func handleGetUploadedParts(w http.ResponseWriter, req *http.Request) {
|
|||
for {
|
||||
page, err := listParts(key, uploadID, cred, nextPartNumberMarker)
|
||||
if err != nil {
|
||||
errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err))
|
||||
errorResponse(w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -208,13 +208,13 @@ func handleCompleteMultipartUpload(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
if err := r.validate(); err != nil {
|
||||
errorResponse(w, req, fmt.Errorf("%w: %s", errBadRequest, err))
|
||||
errorResponse(w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := completeMultipartUpload(key, uploadID, r.Parts, cred)
|
||||
if err != nil {
|
||||
errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err))
|
||||
errorResponse(w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -242,7 +242,7 @@ func handleAbortMultipartUpload(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
err = abortMultipartUpload(key, uploadID, cred)
|
||||
if err != nil {
|
||||
errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err))
|
||||
errorResponse(w, req, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
17
handlers.go
17
handlers.go
|
@ -114,15 +114,16 @@ type createReq struct {
|
|||
}
|
||||
|
||||
func handleCreateForm(w http.ResponseWriter, req *http.Request) {
|
||||
cred := credential{
|
||||
Endpoint: req.PostFormValue("Endpoint"),
|
||||
Region: req.PostFormValue("Region"),
|
||||
AccessKey: req.PostFormValue("AccessKey"),
|
||||
SecretKey: req.PostFormValue("SecretKey"),
|
||||
Prefix: req.PostFormValue("Prefix"),
|
||||
}
|
||||
cred := newCredential(
|
||||
req.PostFormValue("Endpoint"),
|
||||
req.PostFormValue("Region"),
|
||||
req.PostFormValue("AccessKey"),
|
||||
req.PostFormValue("SecretKey"),
|
||||
req.PostFormValue("Prefix"),
|
||||
req.PostFormValue("ACL"),
|
||||
)
|
||||
if err := cred.validate(); err != nil {
|
||||
errorResponse(w, req, fmt.Errorf("%w: %s", errBadRequest, err))
|
||||
errorResponse(w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
22
helpers.go
22
helpers.go
|
@ -8,6 +8,8 @@ import (
|
|||
var errNotFound = errors.New("not found")
|
||||
var errBadRequest = errors.New("bad request")
|
||||
var errInternalServerError = errors.New("internal server error")
|
||||
var errUnauthorized = errors.New("unauthorized")
|
||||
var errForbidden = errors.New("forbidden")
|
||||
|
||||
func errorResponseStatus(w http.ResponseWriter, req *http.Request, err error) {
|
||||
errorStatus := http.StatusInternalServerError
|
||||
|
@ -18,6 +20,10 @@ func errorResponseStatus(w http.ResponseWriter, req *http.Request, err error) {
|
|||
errorStatus = http.StatusBadRequest
|
||||
} else if errors.Is(err, errInternalServerError) {
|
||||
errorStatus = http.StatusInternalServerError
|
||||
} else if errors.Is(err, errUnauthorized) {
|
||||
errorStatus = http.StatusUnauthorized
|
||||
} else if errors.Is(err, errForbidden) {
|
||||
errorStatus = http.StatusForbidden
|
||||
}
|
||||
|
||||
w.WriteHeader(errorStatus)
|
||||
|
@ -31,3 +37,19 @@ func errorResponse(w http.ResponseWriter, req *http.Request, err error) {
|
|||
errorResponseStatus(w, req, err)
|
||||
w.Write([]byte(err.Error()))
|
||||
}
|
||||
|
||||
// responseToError converts a HTTP status code to an error
|
||||
func responseToError(resp *http.Response) error {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return errNotFound
|
||||
} else if resp.StatusCode == http.StatusBadRequest {
|
||||
return errBadRequest
|
||||
} else if resp.StatusCode == http.StatusInternalServerError {
|
||||
return errInternalServerError
|
||||
} else if resp.StatusCode == http.StatusUnauthorized {
|
||||
return errUnauthorized
|
||||
} else if resp.StatusCode == http.StatusForbidden {
|
||||
return errForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
4
main.go
4
main.go
|
@ -26,8 +26,8 @@ func main() {
|
|||
router.Methods(http.MethodGet).Path("/readyz").HandlerFunc(readyz)
|
||||
router.Methods(http.MethodGet).PathPrefix("/assets").HandlerFunc(handleAssets)
|
||||
|
||||
router.Methods(http.MethodGet).Path("/create").HandlerFunc(handleCreate)
|
||||
router.Methods(http.MethodPost).Path("/create").HandlerFunc(handleCreateForm)
|
||||
router.Methods(http.MethodGet).Path("/").HandlerFunc(handleCreate)
|
||||
router.Methods(http.MethodPost).Path("/").HandlerFunc(handleCreateForm)
|
||||
uploadRouter := router.PathPrefix("/{id}").Subrouter()
|
||||
|
||||
uploadTemplateRouter := uploadRouter.Path("").Subrouter()
|
||||
|
|
63
s3.go
63
s3.go
|
@ -7,6 +7,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
@ -52,6 +53,32 @@ func stripETag(t string) string {
|
|||
return strings.TrimSuffix(strings.TrimPrefix(t, "\""), "\"")
|
||||
}
|
||||
|
||||
type errEndpoint struct {
|
||||
err error
|
||||
status string
|
||||
body []byte
|
||||
}
|
||||
|
||||
func (e errEndpoint) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e errEndpoint) Error() string {
|
||||
body := bytes.ReplaceAll(e.body, []byte("\n"), []byte(""))
|
||||
if e.err != nil {
|
||||
return fmt.Sprintf("endpoint responded with %v: %s", e.err, body)
|
||||
}
|
||||
return fmt.Sprintf("endpoint responded with %s: %s", e.status, body)
|
||||
}
|
||||
|
||||
func endpointReturnedError(resp *http.Response) error {
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return errEndpoint{responseToError(resp), resp.Status, body}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/* initiateMultipartUpload */
|
||||
|
||||
type initiateMultipartUploadResult struct {
|
||||
|
@ -71,6 +98,7 @@ func initiateMultipartUpload(
|
|||
params.Set("uploads", "")
|
||||
unsignedReq, err := http.NewRequestWithContext(ctx, http.MethodPost, cred.Endpoint+"/"+key+"?"+params.Encode(), nil)
|
||||
if err != nil {
|
||||
log.Printf("failure creating request: %v", err)
|
||||
return initiateMultipartUploadResult{}, err
|
||||
}
|
||||
if cred.ACL != "" {
|
||||
|
@ -80,12 +108,14 @@ func initiateMultipartUpload(
|
|||
signedReq := sign(unsignedReq, cred)
|
||||
resp, err := httpClientS3.Do(signedReq)
|
||||
if err != nil {
|
||||
log.Printf("failure connecting to endpoint: %v", err)
|
||||
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)
|
||||
err = endpointReturnedError(resp)
|
||||
if err != nil {
|
||||
log.Printf("endpoint responded negatively: %v", err)
|
||||
return initiateMultipartUploadResult{}, err
|
||||
}
|
||||
|
||||
result := initiateMultipartUploadResult{}
|
||||
|
@ -136,18 +166,21 @@ func listParts(
|
|||
params.Set("uploadId", uploadID)
|
||||
unsignedReq, err := http.NewRequestWithContext(ctx, http.MethodGet, cred.Endpoint+"/"+key+"?"+params.Encode(), nil)
|
||||
if err != nil {
|
||||
log.Printf("failure creating request: %v", err)
|
||||
return listPartsResult{}, err
|
||||
}
|
||||
|
||||
signedReq := sign(unsignedReq, cred)
|
||||
resp, err := httpClientS3.Do(signedReq)
|
||||
if err != nil {
|
||||
log.Printf("failure connecting to endpoint: %v", err)
|
||||
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)
|
||||
err = endpointReturnedError(resp)
|
||||
if err != nil {
|
||||
log.Printf("endpoint responded negatively: %v", err)
|
||||
return listPartsResult{}, err
|
||||
}
|
||||
|
||||
result := listPartsResult{}
|
||||
|
@ -213,18 +246,21 @@ func completeMultipartUpload(
|
|||
params.Set("uploadId", uploadID)
|
||||
unsignedReq, err := http.NewRequestWithContext(ctx, http.MethodPost, cred.Endpoint+"/"+key+"?"+params.Encode(), &body)
|
||||
if err != nil {
|
||||
log.Printf("failure creating request: %v", err)
|
||||
return completeMultipartUploadResult{}, err
|
||||
}
|
||||
|
||||
signedReq := sign(unsignedReq, cred)
|
||||
resp, err := httpClientS3.Do(signedReq)
|
||||
if err != nil {
|
||||
log.Printf("failure connecting to endpoint: %v", err)
|
||||
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)
|
||||
err = endpointReturnedError(resp)
|
||||
if err != nil {
|
||||
log.Printf("endpoint responded negatively: %v", err)
|
||||
return completeMultipartUploadResult{}, err
|
||||
}
|
||||
|
||||
result := completeMultipartUploadResult{}
|
||||
|
@ -252,18 +288,21 @@ func abortMultipartUpload(
|
|||
params.Set("uploadId", uploadID)
|
||||
unsignedReq, err := http.NewRequestWithContext(ctx, http.MethodDelete, cred.Endpoint+"/"+key+"?"+params.Encode(), nil)
|
||||
if err != nil {
|
||||
log.Printf("failure creating request: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
signedReq := sign(unsignedReq, cred)
|
||||
resp, err := httpClientS3.Do(signedReq)
|
||||
if err != nil {
|
||||
log.Printf("failure connecting to endpoint: %v", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return fmt.Errorf("endpoint request failed: %d: %s", resp.StatusCode, body)
|
||||
err = endpointReturnedError(resp)
|
||||
if err != nil {
|
||||
log.Printf("endpoint responded negatively: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
6
store.go
6
store.go
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -62,7 +63,7 @@ func (s *redisStore) ping() error {
|
|||
return err
|
||||
}
|
||||
if pong != "PONG" {
|
||||
return errInternalServerError
|
||||
return fmt.Errorf("%w: pong request failed", errInternalServerError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -74,6 +75,7 @@ func (s *redisStore) put(key string, data []byte, expire time.Duration) error {
|
|||
exists := 0
|
||||
err := s.client.Do(ctx, radix.Cmd(&exists, "EXISTS", "upl:"+key))
|
||||
if err != nil {
|
||||
log.Printf("put failed on existence check: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -84,6 +86,7 @@ func (s *redisStore) put(key string, data []byte, expire time.Duration) error {
|
|||
expireS := int64(expire / time.Second)
|
||||
err = s.client.Do(ctx, radix.FlatCmd(nil, "SETEX", "upl:"+key, expireS, data))
|
||||
if err != nil {
|
||||
log.Printf("put failed: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -96,6 +99,7 @@ func (s *redisStore) get(key string) ([]byte, error) {
|
|||
var data []byte
|
||||
err := s.client.Do(ctx, radix.Cmd(&data, "GET", "upl:"+key))
|
||||
if err != nil {
|
||||
log.Printf("get failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,35 @@
|
|||
{{template "head.tmpl" "Create dropbox"}}
|
||||
<form class="space-y-8 px-6 my-8" method="POST">
|
||||
|
||||
<h1 class="my-4 text-4xl font-bold text-center">Create a dropbox</h1>
|
||||
{{template "head.tmpl" "Your dropboxes"}}
|
||||
<form class="create space-y-12 px-6 my-12" method="POST">
|
||||
|
||||
<section class="space-y-4">
|
||||
<h1 class="text-4xl font-bold text-center">Your dropboxes</h1>
|
||||
|
||||
<div class="flex items-center">
|
||||
<h4 class="my-2 flex-1 text-lg font-bold">Bucket options</h4>
|
||||
<h4 class="my-2 flex-1 text-xl font-bold">Previously created</h4>
|
||||
<span>
|
||||
<button
|
||||
type="button"
|
||||
class="log-clear px-6 py-2 rounded-md bg-yellow-600 text-white hover:bg-yellow-700 focus:ring-2 focus:ring-offset-2 focus:ring-yellow-600">
|
||||
Clear
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="log-area"></div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4">
|
||||
<h1 class="text-4xl font-bold text-center">Create a dropbox</h1>
|
||||
|
||||
<div class="notice-area hidden p-4 bg-red-100 text-red-700"></div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<h4 class="my-2 flex-1 text-xl font-bold">Bucket options</h4>
|
||||
<span>
|
||||
<input
|
||||
class="rounded"
|
||||
type="checkbox"
|
||||
id="options-save-bucket"
|
||||
data-save="Endpoint,Region,AccessKey,SecretKey,Prefix,ACL">
|
||||
data-save="Endpoint,Region,AccessKey,SecretKey">
|
||||
<label for="options-save-bucket">Remember</label>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -23,6 +41,7 @@
|
|||
type="url"
|
||||
id="options-endpoint"
|
||||
name="Endpoint"
|
||||
required
|
||||
placeholder="https://bucketname.s3.us-west-2.amazonaws.com">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,6 +53,7 @@
|
|||
type="text"
|
||||
id="options-region"
|
||||
name="Region"
|
||||
required
|
||||
placeholder="us-east-1">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -45,6 +65,7 @@
|
|||
type="text"
|
||||
id="options-accesskey"
|
||||
name="AccessKey"
|
||||
autocomplete="off"
|
||||
placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -56,23 +77,37 @@
|
|||
type="text"
|
||||
id="options-secretkey"
|
||||
name="SecretKey"
|
||||
autocomplete="off"
|
||||
placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4">
|
||||
<div class="flex items-center">
|
||||
<h4 class="my-2 flex-1 text-lg font-bold">Upload options</h4>
|
||||
<h4 class="my-2 flex-1 text-xl font-bold">Upload options</h4>
|
||||
<span>
|
||||
<input
|
||||
class="rounded"
|
||||
type="checkbox"
|
||||
id="options-save-upload"
|
||||
data-save="Prefix,ExpiresNumber,ExpiresUnits">
|
||||
data-save="ACL,Prefix,ExpiresNumber,ExpiresUnits">
|
||||
<label for="options-save-upload">Remember</label>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label for="options-acl">Canned ACL <span class="text-sm text-gray-500">Optional</span></label>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
class="w-full rounded-md border-gray-400"
|
||||
type="text"
|
||||
id="options-acl"
|
||||
name="ACL"
|
||||
placeholder=""
|
||||
value="">
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Specify an optional <a class="text-blue-700" target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl">canned ACL</a>. Otherwise, object access permissions will follow bucket defaults.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="options-prefix">Prefix</label>
|
||||
<div class="mt-1">
|
||||
|
@ -85,7 +120,7 @@
|
|||
value="{random}/">
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Files will be uploaded with this prefix. For a random <code>[a-z0-9]{16}</code> prefix , use <code>{random}</code>.
|
||||
Files will be uploaded with this prefix. For a random <code>[a-z0-9]{16}</code> prefix , use <code>{random}</code>.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -118,9 +153,6 @@
|
|||
data-derive="duration,ExpiresNumber,ExpiresUnits"
|
||||
data-derive-notice="#expiry-notice">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4">
|
||||
<div class="flex justify-end">
|
||||
<input
|
||||
class="px-6 py-2 rounded-md bg-green-600 text-white hover:bg-green-700 focus:ring-2 focus:ring-offset-2 focus:ring-green-600"
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import Log from './log';
|
||||
|
||||
async function throwForStatus(res) {
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const createAreas = document.querySelectorAll('.create');
|
||||
createAreas.forEach(createArea => {
|
||||
|
||||
/* Elements */
|
||||
|
||||
const logArea = createArea.querySelector('.log-area');
|
||||
const logClearBtn = createArea.querySelector('.log-clear');
|
||||
const noticeArea = createArea.querySelector('.notice-area');
|
||||
|
||||
/* Components */
|
||||
|
||||
const log = new Log(logArea, window.location.pathname, {
|
||||
empty: 'Your locally-stored dropbox creation history is empty',
|
||||
});
|
||||
|
||||
logClearBtn.addEventListener('click', () => {
|
||||
log.clear();
|
||||
});
|
||||
|
||||
/* Form */
|
||||
|
||||
function showError(error='') {
|
||||
let message = error.message || error.toString();
|
||||
if (message !== '') {
|
||||
noticeArea.classList.remove('hidden');
|
||||
noticeArea.innerText = message;
|
||||
} else {
|
||||
noticeArea.classList.add('hidden');
|
||||
noticeArea.innerText = message;
|
||||
}
|
||||
}
|
||||
|
||||
createArea.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
|
||||
// Clear previous errors
|
||||
showError();
|
||||
|
||||
const data = new FormData(e.target);
|
||||
fetch(e.target.action, {
|
||||
method: e.target.method,
|
||||
body: data,
|
||||
})
|
||||
.then(throwForStatus)
|
||||
.then(res => res.text())
|
||||
.then(id => {
|
||||
log.add({
|
||||
location: window.location.origin + '/' + id,
|
||||
title: `
|
||||
Endpoint: ${data.get('Endpoint')}
|
||||
Region: ${data.get('Region')}
|
||||
Prefix: ${data.get('Prefix')}
|
||||
Expires: ${data.get('ExpiresNumber')}${data.get('ExpiresUnits')}
|
||||
`.trim(),
|
||||
});
|
||||
})
|
||||
.catch(showError);
|
||||
});
|
||||
|
||||
});
|
|
@ -41,7 +41,7 @@ class Deriver {
|
|||
if (!this.notice) {
|
||||
return;
|
||||
}
|
||||
let message = error.toString();
|
||||
let message = error.message || error.toString();
|
||||
if (Array.isArray(error.problems)) {
|
||||
message = 'Invalid input: ' + error.problems.join(', ');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
class Limiter {
|
||||
constructor(delay=500) {
|
||||
this.delay = delay;
|
||||
|
||||
this.interval = null;
|
||||
this.next = null;
|
||||
}
|
||||
|
||||
call(fn) {
|
||||
if (this.interval == null) {
|
||||
fn();
|
||||
this.enableClock();
|
||||
} else {
|
||||
this.next = fn;
|
||||
}
|
||||
}
|
||||
|
||||
checkCall() {
|
||||
if (this.next == null) {
|
||||
return this.disableClock();
|
||||
}
|
||||
|
||||
// Function wanted to be called
|
||||
this.next();
|
||||
this.next = null;
|
||||
}
|
||||
|
||||
enableClock() {
|
||||
this.interval = setInterval(() => {
|
||||
this.checkCall();
|
||||
}, this.delay);
|
||||
}
|
||||
|
||||
disableClock() {
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default Limiter;
|
|
@ -1,14 +1,14 @@
|
|||
import filesize from 'filesize';
|
||||
|
||||
class Log {
|
||||
constructor(target, key) {
|
||||
constructor(target, key, messages) {
|
||||
this.key = key;
|
||||
this.target = target;
|
||||
this.items = [];
|
||||
|
||||
const empty = document.createElement('div');
|
||||
empty.innerText = 'Your locally-stored file upload history is empty';
|
||||
empty.classList.add('mt-1', 'p-2', 'bg-gray-50', 'text-gray-500', 'rounded-md', 'border', 'border-gray-400', 'flex', 'justify-center');
|
||||
empty.innerText = messages.empty || 'Empty';
|
||||
empty.classList.add('mt-2', 'p-2', 'bg-gray-50', 'text-gray-500', 'rounded-md', 'border', 'border-gray-400', 'flex', 'justify-center');
|
||||
this.empty = empty;
|
||||
|
||||
this.localStorageLoad();
|
||||
|
@ -26,24 +26,36 @@ class Log {
|
|||
|
||||
static renderItem(item) {
|
||||
const base = document.createElement('div');
|
||||
base.classList.add('mt-1');
|
||||
base.classList.add('mt-2');
|
||||
base.classList.add('flex');
|
||||
base.title = item.title;
|
||||
|
||||
const url = document.createElement('input');
|
||||
url.type = 'url';
|
||||
url.value = item.location;
|
||||
url.setAttribute('readonly', '');
|
||||
url.classList.add('w-full', 'rounded-l-md', 'border-gray-400');
|
||||
url.classList.add('w-full', 'rounded-l-md', 'border-r-0', 'border-gray-400');
|
||||
url.addEventListener('click', (e) => {
|
||||
//e.target.setSelectionRange(0, e.target.value.length);
|
||||
e.target.select();
|
||||
});
|
||||
base.appendChild(url);
|
||||
|
||||
const size = document.createElement('span');
|
||||
size.innerText = filesize(item.size);
|
||||
size.classList.add('text-sm', 'whitespace-nowrap', 'px-2', 'bg-gray-50', 'text-gray-500', 'rounded-r-md', 'border', 'border-gray-400', 'border-l-0', 'inline-flex', 'items-center', 'justify-center', 'w-24');
|
||||
base.appendChild(size);
|
||||
const urlOpen = document.createElement('a');
|
||||
urlOpen.target = '_blank';
|
||||
urlOpen.href = item.location;
|
||||
urlOpen.classList.add('rounded-r-md', 'px-2', 'text-blue-700', 'border', 'border-l-0', 'border-gray-400', 'inline-flex', 'items-center');
|
||||
urlOpen.innerText = 'Open';
|
||||
base.appendChild(urlOpen);
|
||||
|
||||
if (item.size) {
|
||||
urlOpen.classList.remove('rounded-r-md');
|
||||
|
||||
const size = document.createElement('span');
|
||||
size.innerText = filesize(item.size);
|
||||
size.classList.add('text-sm', 'whitespace-nowrap', 'px-2', 'bg-gray-50', 'text-gray-500', 'rounded-r-md', 'border', 'border-gray-400', 'border-l-0', 'inline-flex', 'items-center', 'justify-center', 'w-24');
|
||||
base.appendChild(size);
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
|
|
@ -11,3 +11,4 @@ import '@uppy/status-bar/dist/style.css';
|
|||
import './save';
|
||||
import './derive';
|
||||
import './upload';
|
||||
import './create';
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import Limiter from './limiter';
|
||||
|
||||
class Saver {
|
||||
constructor(target, inputs, key='') {
|
||||
this.target = target;
|
||||
this.inputs = inputs;
|
||||
this.key = '';
|
||||
|
||||
this.limiter = new Limiter();
|
||||
|
||||
this.updateState = this.updateState.bind(this);
|
||||
this.input = this.input.bind(this);
|
||||
this.save = this.save.bind(this);
|
||||
|
||||
this.target.addEventListener('input', this.updateState);
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
updateState() {
|
||||
if (this.target.checked) {
|
||||
this.inputs.forEach(input => input.addEventListener('input', this.input));
|
||||
this.save();
|
||||
} else {
|
||||
this.inputs.forEach(input => input.removeEventListener('input', this.input));
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
input() {
|
||||
this.limiter.call(this.save);
|
||||
}
|
||||
|
||||
load() {
|
||||
const values = JSON.parse(window.localStorage.getItem('save' + this.key) || '{}');
|
||||
for (const input of this.inputs) {
|
||||
if (input.name in values) {
|
||||
input.value = values[input.name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
const values = {};
|
||||
for (const input of this.inputs) {
|
||||
values[input.name] = input.value;
|
||||
}
|
||||
window.localStorage.setItem('save' + this.key, JSON.stringify(values));
|
||||
}
|
||||
|
||||
clear() {
|
||||
window.localStorage.removeItem('save' + this.key);
|
||||
}
|
||||
}
|
||||
|
||||
const saveInputs = document.querySelectorAll('[data-save]');
|
||||
saveInputs.forEach(saveInput => {
|
||||
|
||||
const inputNames = saveInput.dataset.save.split(',');
|
||||
|
||||
const inputs = inputNames.map(inputName => document.querySelector(`[name="${inputName}"]`));
|
||||
const saver = new Saver(saveInput, inputs);
|
||||
saver.load();
|
||||
|
||||
});
|
|
@ -17,7 +17,9 @@ uploadAreas.forEach(uploadArea => {
|
|||
|
||||
/* Components */
|
||||
|
||||
const log = new Log(logArea, window.location.pathname);
|
||||
const log = new Log(logArea, window.location.pathname, {
|
||||
empty: 'Your locally-stored file upload history is empty',
|
||||
});
|
||||
|
||||
logClearBtn.addEventListener('click', () => {
|
||||
log.clear();
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
{{template "head.tmpl" "Dropbox"}}
|
||||
<div class="upload space-y-8 px-6 my-8">
|
||||
<div>
|
||||
|
||||
<h1 class="my-4 text-4xl font-bold text-center">Drop a file</h1>
|
||||
|
||||
<section>
|
||||
<div class="drop-area"></div>
|
||||
<div class="status-area"></div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<h4 class="my-2 flex-1 text-lg font-bold">File upload history</h4>
|
||||
<span>
|
||||
<button
|
||||
class="log-clear px-6 py-2 rounded-md bg-yellow-600 text-white hover:bg-yellow-700 focus:ring-2 focus:ring-offset-2 focus:ring-yellow-600">
|
||||
Clear
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="log-area"></div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4">
|
||||
<div class="flex items-center">
|
||||
<h4 class="my-2 flex-1 text-xl font-bold">File upload history</h4>
|
||||
<span>
|
||||
<button
|
||||
class="log-clear px-6 py-2 rounded-md bg-yellow-600 text-white hover:bg-yellow-700 focus:ring-2 focus:ring-offset-2 focus:ring-yellow-600">
|
||||
Clear
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="log-area"></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
{{template "foot.tmpl"}}
|
||||
|
|
Loading…
Reference in New Issue