1
0
Fork 0

Read credentials from Redis

upload-progress
Ambrose Chua 2021-05-23 00:46:15 +08:00
parent ce971a32b8
commit bf94a2e2bf
11 changed files with 183 additions and 84 deletions

35
credential.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"fmt"
"strings"
)
/* types */
type credential struct {
AccessKey string
SecretKey string
// Region is critical when signing requests.
Region string
// Endpoint is the base URL of the bucket, including the bucket name (in either the domain or path).
//
// Example:
// https://bucketname.s3.us-west-2.amazonaws.com
// http://my-minio.example.com/bucket-name
Endpoint string
// ACL is an optional canned ACL to set on objects
ACL string
// Prefix is a string to prepend to object keys
Prefix string
}
func (cred credential) validate() error {
if strings.HasSuffix(cred.Endpoint, "/") {
return fmt.Errorf("%w: endpoint should not end with slash", errBadRequest)
}
if strings.HasPrefix(cred.Prefix, "/") {
return fmt.Errorf("%w: prefix should not start with slash", errBadRequest)
}
return nil
}

View File

@ -6,7 +6,6 @@ import (
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"github.com/gorilla/mux"
@ -40,6 +39,13 @@ type createMultipartUploadRes struct {
}
func handleCreateMultipartUpload(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
cred, err := getCredential(vars["id"])
if err != nil {
errorResponse(w, req, err)
return
}
r := createMultipartUploadReq{}
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&r); err != nil {
@ -52,14 +58,6 @@ func handleCreateMultipartUpload(w http.ResponseWriter, req *http.Request) {
return
}
cred := credential{
AccessKey: os.Getenv("MINIO_ACCESS_KEY"),
SecretKey: os.Getenv("MINIO_SECRET_KEY"),
Region: os.Getenv("MINIO_REGION_NAME"),
Endpoint: os.Getenv("MINIO_ENDPOINT"),
Prefix: os.Getenv("PREFIX"),
}
// Derive the object key
key := cred.Prefix + r.Filename
@ -82,6 +80,12 @@ type getUploadedPartsRes []part
func handleGetUploadedParts(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
cred, err := getCredential(vars["id"])
if err != nil {
errorResponse(w, req, err)
return
}
uploadID := vars["uploadID"]
key := req.URL.Query().Get("key")
@ -90,14 +94,6 @@ func handleGetUploadedParts(w http.ResponseWriter, req *http.Request) {
return
}
cred := credential{
AccessKey: os.Getenv("MINIO_ACCESS_KEY"),
SecretKey: os.Getenv("MINIO_SECRET_KEY"),
Region: os.Getenv("MINIO_REGION_NAME"),
Endpoint: os.Getenv("MINIO_ENDPOINT"),
Prefix: os.Getenv("PREFIX"),
}
parts := make(getUploadedPartsRes, 0, 0)
var nextPartNumberMarker uint32
for {
@ -127,6 +123,12 @@ type signPartUploadRes struct {
func handleSignPartUpload(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
cred, err := getCredential(vars["id"])
if err != nil {
errorResponse(w, req, err)
return
}
uploadID := vars["uploadID"]
key := req.URL.Query().Get("key")
partNumber, err := strconv.ParseUint(vars["uploadPart"], 10, 16)
@ -140,14 +142,6 @@ func handleSignPartUpload(w http.ResponseWriter, req *http.Request) {
return
}
cred := credential{
AccessKey: os.Getenv("MINIO_ACCESS_KEY"),
SecretKey: os.Getenv("MINIO_SECRET_KEY"),
Region: os.Getenv("MINIO_REGION_NAME"),
Endpoint: os.Getenv("MINIO_ENDPOINT"),
Prefix: os.Getenv("PREFIX"),
}
params := make(url.Values)
params.Add("partNumber", strconv.FormatUint(partNumber, 10))
params.Add("uploadId", uploadID)
@ -182,6 +176,12 @@ func (r completeMultipartUploadReq) validate() error {
func handleCompleteMultipartUpload(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
cred, err := getCredential(vars["id"])
if err != nil {
errorResponse(w, req, err)
return
}
uploadID := vars["uploadID"]
key := req.URL.Query().Get("key")
@ -202,14 +202,6 @@ func handleCompleteMultipartUpload(w http.ResponseWriter, req *http.Request) {
return
}
cred := credential{
AccessKey: os.Getenv("MINIO_ACCESS_KEY"),
SecretKey: os.Getenv("MINIO_SECRET_KEY"),
Region: os.Getenv("MINIO_REGION_NAME"),
Endpoint: os.Getenv("MINIO_ENDPOINT"),
Prefix: os.Getenv("PREFIX"),
}
result, err := completeMultipartUpload(key, uploadID, r.Parts, cred)
if err != nil {
errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err))
@ -224,6 +216,12 @@ func handleCompleteMultipartUpload(w http.ResponseWriter, req *http.Request) {
func handleAbortMultipartUpload(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
cred, err := getCredential(vars["id"])
if err != nil {
errorResponse(w, req, err)
return
}
uploadID := vars["uploadID"]
key := req.URL.Query().Get("key")
@ -232,15 +230,7 @@ func handleAbortMultipartUpload(w http.ResponseWriter, req *http.Request) {
return
}
cred := credential{
AccessKey: os.Getenv("MINIO_ACCESS_KEY"),
SecretKey: os.Getenv("MINIO_SECRET_KEY"),
Region: os.Getenv("MINIO_REGION_NAME"),
Endpoint: os.Getenv("MINIO_ENDPOINT"),
Prefix: os.Getenv("PREFIX"),
}
err := abortMultipartUpload(key, uploadID, cred)
err = abortMultipartUpload(key, uploadID, cred)
if err != nil {
errorResponse(w, req, fmt.Errorf("%w: %s", errInternalServerError, err))
return

View File

@ -1,9 +1,14 @@
package main
import (
"encoding/json"
"errors"
"html/template"
"net/http"
"os"
"time"
"github.com/gorilla/mux"
)
var globalStore store
@ -16,6 +21,38 @@ func setupHandlers() {
}
}
/* credentials */
func getCredential(id string) (credential, error) {
cred := credential{}
b, err := globalStore.get(id)
if err != nil {
return cred, err
}
err = json.Unmarshal(b, &cred)
if err != nil {
return cred, err
}
return cred, nil
}
func setCredential(id string, cred credential, expire time.Duration) error {
b, err := json.Marshal(cred)
if err != nil {
return err
}
err = globalStore.put(id, b, expire)
if err != nil {
return err
}
return nil
}
/* templates */
var tmpl = template.Must(template.ParseFS(assets, "web/*.tmpl"))
@ -23,5 +60,23 @@ var tmpl = template.Must(template.ParseFS(assets, "web/*.tmpl"))
/* upload template */
func handleUpload(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
_, err := getCredential(vars["id"])
if errors.Is(err, errNotFound) {
errorResponseStatus(w, req, err)
tmpl.ExecuteTemplate(w, "upload-not-found.tmpl", nil)
return
}
if err != nil {
errorResponse(w, req, err)
return
}
tmpl.ExecuteTemplate(w, "upload.tmpl", nil)
}
/* create template */
func handleCreate(w http.ResponseWriter, req *http.Request) {
tmpl.ExecuteTemplate(w, "create.tmpl", nil)
}

View File

@ -10,7 +10,7 @@ var errNotFound = errors.New("not found")
var errBadRequest = errors.New("bad request")
var errInternalServerError = errors.New("internal server error")
func errorResponse(w http.ResponseWriter, req *http.Request, err error) {
func errorResponseStatus(w http.ResponseWriter, req *http.Request, err error) {
errorMessage := err.Error()
errorStatus := http.StatusInternalServerError
@ -22,6 +22,11 @@ func errorResponse(w http.ResponseWriter, req *http.Request, err error) {
errorStatus = http.StatusInternalServerError
}
log.Printf("%s %s: %s", req.Method, req.URL.Path, errorMessage)
log.Printf("%s %s: %s", req.Method, req.RequestURI, errorMessage)
w.WriteHeader(errorStatus)
}
func errorResponse(w http.ResponseWriter, req *http.Request, err error) {
errorResponseStatus(w, req, err)
w.Write([]byte(err.Error()))
}

15
logger.go Normal file
View File

@ -0,0 +1,15 @@
package main
import (
"log"
"net/http"
)
func middlewareLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do stuff here
log.Printf("%s: %s", r.Method, r.RequestURI)
// Call the next handler, which can be another middleware in the chain, or the final handler.
next.ServeHTTP(w, r)
})
}

16
main.go
View File

@ -2,6 +2,7 @@ package main
import (
"io/fs"
"log"
"net/http"
"os"
"time"
@ -20,16 +21,22 @@ func main() {
panic(err)
}
setupS3()
setupHandlers()
setupS3()
router := mux.NewRouter()
uploadRouter := router.PathPrefix("/{id}").Subrouter()
router.PathPrefix("/assets").Handler(http.FileServer(http.FS(assetsWeb)))
router.Use(middlewareLogger)
uploadRouter.Path("").HandlerFunc(handleUpload)
router.Methods(http.MethodGet).PathPrefix("/assets").Handler(http.FileServer(http.FS(assetsWeb)))
router.Methods(http.MethodGet).Path("/favicon.ico").Handler(http.FileServer(http.FS(assetsWeb)))
router.Methods(http.MethodGet).Path("/").HandlerFunc(handleCreate)
uploadRouter := router.PathPrefix("/{id}").Subrouter()
uploadTemplateRouter := uploadRouter.Path("").Subrouter()
s3Router := uploadRouter.PathPrefix("/s3/multipart").Subrouter()
uploadTemplateRouter.Methods(http.MethodGet).Path("").HandlerFunc(handleUpload)
s3Router.Methods(http.MethodPost).Path("").HandlerFunc(handleCreateMultipartUpload)
s3Router.Methods(http.MethodGet).Path("/{uploadID}").HandlerFunc(handleGetUploadedParts)
s3Router.Methods(http.MethodGet).Path("/{uploadID}/{uploadPart}").HandlerFunc(handleSignPartUpload)
@ -42,6 +49,7 @@ func main() {
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
log.Printf("listeining on %s", listen)
err = server.ListenAndServe()
if err != nil {
panic(err)

26
s3.go
View File

@ -52,32 +52,6 @@ func stripETag(t string) string {
return strings.TrimSuffix(strings.TrimPrefix(t, "\""), "\"")
}
/* types */
type credential struct {
AccessKey string
SecretKey string
// Region is critical when signing requests.
Region string
// Endpoint is the base URL of the bucket, including the bucket name (in either the domain or path).
//
// Example:
// https://bucketname.s3.us-west-2.amazonaws.com
// http://my-minio.example.com/bucket-name
Endpoint string
// ACL is an optional canned ACL to set on objects
ACL string
// Prefix is a string to prepend to object keys
Prefix string
}
func (cred credential) validate() error {
if strings.HasSuffix(cred.Endpoint, "/") {
return fmt.Errorf("%w: endpoint should not end with slash", errBadRequest)
}
return nil
}
/* initiateMultipartUpload */
type initiateMultipartUploadResult struct {

View File

@ -12,6 +12,7 @@ import (
var errKeyCollision = errors.New("key collision")
var errKeyNotFound = fmt.Errorf("key %w", errNotFound)
var errInvalidConnectionType = errors.New("invalid connection type")
type store interface {
put(key string, data []byte, expire time.Duration) error
@ -38,15 +39,14 @@ func newRedisStore(connection string) (*redisStore, error) {
var err error
if rtype == "simple" {
client, err = (radix.PoolConfig{}).New(ctx, "tcp", connectionParts[1])
if err != nil {
return nil, nil
}
} else if rtype == "cluster" {
clusterAddrs := strings.Split(connectionParts[1], ",")
client, err = (radix.ClusterConfig{}).New(ctx, clusterAddrs)
} else {
err = fmt.Errorf("%w: %#v of string %#v", errInvalidConnectionType, rtype, connection)
}
if err != nil {
return nil, nil
return nil, fmt.Errorf("unable to initialize redis store: %w", err)
}
return &redisStore{client}, nil
}
@ -56,7 +56,7 @@ func (s *redisStore) put(key string, data []byte, expire time.Duration) error {
defer cancel()
exists := 0
err := s.client.Do(ctx, radix.Cmd(&exists, "EXISTS", key))
err := s.client.Do(ctx, radix.Cmd(&exists, "EXISTS", "upl:"+key))
if err != nil {
return err
}
@ -78,7 +78,7 @@ func (s *redisStore) get(key string) ([]byte, error) {
defer cancel()
var data []byte
err := s.client.Do(ctx, radix.Cmd(&data, "GET", key))
err := s.client.Do(ctx, radix.Cmd(&data, "GET", "upl:"+key))
if err != nil {
return nil, err
}

View File

@ -3,7 +3,8 @@ import filesize from 'filesize';
import './log.css';
class Log {
constructor(selector) {
constructor(selector, key) {
this.key = key;
this.target = document.querySelector(selector);
this.items = [];
@ -12,12 +13,12 @@ class Log {
}
localStorageLoad() {
const loaded = JSON.parse(window.localStorage.getItem("log") || "[]");
const loaded = JSON.parse(window.localStorage.getItem("log" + this.key) || "[]");
this.items.push(...loaded);
}
localStorageSave() {
window.localStorage.setItem("log", JSON.stringify(this.items));
window.localStorage.setItem("log" + this.key, JSON.stringify(this.items));
}
static renderItem(item) {

View File

@ -25,7 +25,7 @@ uppy.use(AwsS3Multipart, {
companionUrl: window.location.pathname,
});
const log = new Log('#log-area');
const log = new Log('#log-area', window.location.pathname);
uppy.on('upload-success', (f, res) => {
log.add({

16
web/upload-not-found.tmpl Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dropbox Not Found</title>
<link rel="stylesheet" href="assets/bundle.css">
</head>
<body>
<div class="upload-wrapper">
<div class="upload">
<h1>Dropbox Not Found</h1>
<p>The dropbox you are looking for doesn't exist or has expired.</p>
</div>
</div>
</body>