From 3024c907ebb4c2dabb6dc13f2b15220466916e80 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Mon, 20 Feb 2023 20:25:38 +0100 Subject: [PATCH] . --- upctl/lib/client.go | 4 ++ upd/lib/handlers.go | 142 +++++++++++++++++++++++++++++++++++++++++ upd/lib/server.go | 149 +++++++------------------------------------- 3 files changed, 170 insertions(+), 125 deletions(-) create mode 100644 upd/lib/handlers.go diff --git a/upctl/lib/client.go b/upctl/lib/client.go index b9f2d7c..7cfafb3 100644 --- a/upctl/lib/client.go +++ b/upctl/lib/client.go @@ -48,6 +48,10 @@ func Runclient(cfg *cfg.Config, args []string) error { postfiles[filepath.Base(file)] = file } + // FIXME: doesn't set name=upload[] + // see https://github.com/go-resty/resty/issues/617 + // however, this works: + // curl -X POST localhost:8080/api/putfile -F "upload[]=@xxx" -F "upload[]=@yyy" -H "Content-Type: multipart/form-data" resp, err := client.R(). SetFiles(postfiles). SetFormData(map[string]string{"expire": "1d"}). diff --git a/upd/lib/handlers.go b/upd/lib/handlers.go new file mode 100644 index 0000000..6058de0 --- /dev/null +++ b/upd/lib/handlers.go @@ -0,0 +1,142 @@ +/* +Copyright © 2023 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package lib + +import ( + //"archive/zip" + "fmt" + "github.com/gin-gonic/gin" + //"github.com/gin-gonic/gin/binding" + "encoding/json" + "github.com/google/uuid" + "github.com/tlinden/up/upd/cfg" + bolt "go.etcd.io/bbolt" + //"io" + // "net/http" + "os" + "path/filepath" + //"regexp" + "strings" + "time" +) + +func Putfile(c *gin.Context, cfg *cfg.Config, db *bolt.DB) (string, error) { + // supports upload of multiple files with: + // + // curl -X POST localhost:8080/putfile \ + // -F "upload[]=@/home/scip/2023-02-06_10-51.png" \ + // -F "upload[]=@/home/scip/pgstat.png" \ + // -H "Content-Type: multipart/form-data" + // + // If multiple files are uploaded, they are zipped, otherwise + // the file is being stored as is. + // + // Returns the name of the uploaded file. + // + // FIXME: normalize or rename filename of single file to avoid dumb file names + id := uuid.NewString() + + var returnUrl string + var formdata Meta + + os.MkdirAll(filepath.Join(cfg.StorageDir, id), os.ModePerm) + + // fetch auxiliary form data + form, _ := c.MultipartForm() + if err := c.ShouldBind(&formdata); err != nil { + return "", err + } + + // init upload obj + entry := &Upload{Id: id, Uploaded: time.Now(), Expire: formdata.Expire} + if len(entry.Expire) == 0 { + entry.Expire = "asap" + } + + // retrieve files, if any + files := form.File["upload[]"] + for _, file := range files { + filename := NormalizeFilename(filepath.Base(file.Filename)) + path := filepath.Join(cfg.StorageDir, id, filename) + entry.Members = append(entry.Members, filename) + Log("Received: %s => %s/%s", file.Filename, id, filename) + + if err := c.SaveUploadedFile(file, path); err != nil { + cleanup(filepath.Join(cfg.StorageDir, id)) + return "", err + } + } + + if len(entry.Members) == 1 { + returnUrl = cfg.Url + cfg.ApiPrefix + "/getfile/" + id + "/" + entry.Members[0] + entry.File = entry.Members[0] + } else { + zipfile := Ts() + "data.zip" + + if err := zipSource(filepath.Join(cfg.StorageDir, id), filepath.Join(cfg.StorageDir, id, zipfile)); err != nil { + cleanup(filepath.Join(cfg.StorageDir, id)) + return "", err + } + + returnUrl = strings.Join([]string{cfg.Url + cfg.ApiPrefix, "getfile", id, zipfile}, "/") + entry.File = zipfile + + // clean up after us + go func() { + for _, file := range entry.Members { + if err := os.Remove(filepath.Join(cfg.StorageDir, id, file)); err != nil { + Log("ERROR: unable to delete %s: %s", file, err) + } + } + }() + + } + + Log("Now serving %s from %s/%s", returnUrl, cfg.StorageDir, id) + Log("Expire set to: %s", entry.Expire) + + go func() { + // => db.go ! + err := db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte("uploads")) + if err != nil { + return fmt.Errorf("create bucket: %s", err) + } + + jsonentry, err := json.Marshal(entry) + if err != nil { + return fmt.Errorf("json marshalling failure: %s", err) + } + + err = b.Put([]byte(id), []byte(jsonentry)) + if err != nil { + return fmt.Errorf("insert data: %s", err) + } + + // results in: + // bbolt get /tmp/uploads.db uploads fb242922-86cb-43a8-92bc-b027c35f0586 + // {"id":"fb242922-86cb-43a8-92bc-b027c35f0586","expire":"1d","file":"2023-02-17-13-09-data.zip"} + return nil + }) + if err != nil { + Log("DB error: %s", err.Error()) + } + }() + + return returnUrl, nil +} diff --git a/upd/lib/server.go b/upd/lib/server.go index 2bb9886..44945b8 100644 --- a/upd/lib/server.go +++ b/upd/lib/server.go @@ -22,8 +22,8 @@ import ( "fmt" "github.com/gin-gonic/gin" //"github.com/gin-gonic/gin/binding" - "encoding/json" - "github.com/google/uuid" + //"encoding/json" + //"github.com/google/uuid" "github.com/tlinden/up/upd/cfg" bolt "go.etcd.io/bbolt" "io" @@ -31,7 +31,7 @@ import ( "os" "path/filepath" "regexp" - "strings" + //"strings" "time" ) @@ -41,6 +41,20 @@ type Result struct { error string } +// Binding from JSON, data coming from user, not tainted +type Meta struct { + Expire string `form:"expire"` +} + +// stores 1 upload object, gets into db +type Upload struct { + Id string `json:"id"` + Expire string `json:"expire"` + File string `json:"file"` // final filename (visible to the downloader) + Members []string `json:"members"` // contains multiple files, so File is an archive + Uploaded time.Time `json:"uploaded"` +} + func Log(format string, values ...any) { fmt.Fprintf(gin.DefaultWriter, "[GIN] "+format+"\n", values...) } @@ -56,17 +70,6 @@ func NormalizeFilename(file string) string { return Ts() + r.ReplaceAllString(file, "") } -// Binding from JSON, data coming from user, not tainted -type Meta struct { - Expire string `form:"expire"` -} - -type DbEntry struct { - Id string `json:"id"` - Expire string `json:"expire"` - File string `json:"file"` -} - func Runserver(cfg *cfg.Config, args []string) error { dst := cfg.StorageDir router := gin.Default() @@ -79,123 +82,19 @@ func Runserver(cfg *cfg.Config, args []string) error { } defer db.Close() - // FIXME: put these beast into their own funcs!!!!!!! { api.POST("/putfile", func(c *gin.Context) { - // supports upload of multiple files with: - // - // curl -X POST localhost:8080/putfile \ - // -F "upload[]=@/home/scip/2023-02-06_10-51.png" \ - // -F "upload[]=@/home/scip/pgstat.png" \ - // -H "Content-Type: multipart/form-data" - // - // If multiple files are uploaded, they are zipped, otherwise - // the file is being stored as is. - // - // Returns the name of the uploaded file. - // - // FIXME: normalize or rename filename of single file to avoid dumb file names - id := uuid.NewString() + uri, err := Putfile(c, cfg, db) - os.MkdirAll(filepath.Join(dst, id), os.ModePerm) - - var formdata Meta - if err := c.ShouldBind(&formdata); err != nil { + if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - form, _ := c.MultipartForm() - files := form.File["upload[]"] - uploaded := []string{} - - for _, file := range files { - filename := NormalizeFilename(filepath.Base(file.Filename)) - path := filepath.Join(dst, id, filename) - uploaded = append(uploaded, filename) - Log("Received: %s => %s/%s", file.Filename, id, filename) - - if err := c.SaveUploadedFile(file, path); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusOK, - "message": "upload file err: " + err.Error(), - "success": false, - }) - - cleanup(filepath.Join(dst, id)) - - return - } - } - - var returnUrl string - entry := &DbEntry{Id: id, Expire: formdata.Expire} - - if len(uploaded) == 1 { - returnUrl = cfg.Url + cfg.ApiPrefix + "/getfile/" + id + "/" + uploaded[0] - entry.File = uploaded[0] } else { - zipfile := Ts() + "data.zip" - - if err := zipSource(filepath.Join(dst, id), filepath.Join(dst, id, zipfile)); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": http.StatusBadRequest, - "message": "upload file err: " + err.Error(), - "success": false, - }) - - cleanup(filepath.Join(dst, id)) - } - - returnUrl = strings.Join([]string{cfg.Url + cfg.ApiPrefix, "getfile", id, zipfile}, "/") - entry.File = zipfile - - // clean up after us - go func() { - for _, file := range uploaded { - if err := os.Remove(filepath.Join(dst, id, file)); err != nil { - Log("ERROR: unable to delete %s: %s", file, err) - } - } - }() - - } - - c.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "message": returnUrl, - "success": true, - }) - - Log("Now serving %s from %s/%s", returnUrl, dst, id) - Log("Expire set to: %s", formdata.Expire) - - go func() { - err := db.Update(func(tx *bolt.Tx) error { - b, err := tx.CreateBucketIfNotExists([]byte("uploads")) - if err != nil { - return fmt.Errorf("create bucket: %s", err) - } - - jsonentry, err := json.Marshal(entry) - if err != nil { - return fmt.Errorf("json marshalling failure: %s", err) - } - - err = b.Put([]byte(id), []byte(jsonentry)) - if err != nil { - return fmt.Errorf("insert data: %s", err) - } - - // results in: - // bbolt get /tmp/uploads.db uploads fb242922-86cb-43a8-92bc-b027c35f0586 - // {"id":"fb242922-86cb-43a8-92bc-b027c35f0586","expire":"1d","file":"2023-02-17-13-09-data.zip"} - return nil + c.JSON(http.StatusOK, gin.H{ + "code": http.StatusOK, + "message": uri, + "success": true, }) - if err != nil { - Log("DB error: %s", err.Error()) - } - }() + } }) api.GET("/getfile/:id/:file", func(c *gin.Context) {