From 01a0dc054da78ce315dfbf93719f24933ceb6d33 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Wed, 8 Mar 2023 19:31:42 +0100 Subject: [PATCH] changes: - added cleaner goroutine - added delete cmd - added list cmd - refactoring --- README.md | 7 +- upctl/cmd/delete.go | 47 +++++++++++++ upctl/cmd/list.go | 3 + upctl/cmd/root.go | 1 + upctl/cmd/upload.go | 5 +- upctl/go.mod | 2 + upctl/go.sum | 4 ++ upctl/lib/client.go | 64 ++++++++++++++++-- upctl/lib/output.go | 50 ++++++++++++++ upctl/lib/timestamp.go | 65 ++++++++++++++++++ upd/api/cleaner.go | 85 +++++++++++++++++++++++ upd/api/common.go | 46 ++++++++++--- upd/api/db.go | 29 ++++---- upd/api/fileio.go | 2 +- upd/api/handlers.go | 36 +++++----- upd/api/server.go | 148 ++++++++++++++++++++++++++++------------- upd/cfg/config.go | 16 +++++ 17 files changed, 506 insertions(+), 104 deletions(-) create mode 100644 upctl/cmd/delete.go create mode 100644 upctl/lib/output.go create mode 100644 upctl/lib/timestamp.go create mode 100644 upd/api/cleaner.go diff --git a/README.md b/README.md index 16966fd..e6d3aed 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,10 @@ Simple standalone file upload server with api and cli ## TODO -- implement goroutine to expire after 1d, 10m etc - implemented. add go routine to server, use Db.Iter() -- use bolt db to retrieve list of items to expire - also serve a html upload page -- add auth options (access key, users, roles, oauth2) - add metrics -- add upctl command to remove a file -- use global map of api endpoints like /file/get/ etc - create cobra client commands (upload, list, delete, edit) +- add authorization checks for delete and list based on apicontext ## BUGS diff --git a/upctl/cmd/delete.go b/upctl/cmd/delete.go new file mode 100644 index 0000000..6b03f64 --- /dev/null +++ b/upctl/cmd/delete.go @@ -0,0 +1,47 @@ +/* +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 cmd + +import ( + "errors" + "github.com/spf13/cobra" + "github.com/tlinden/up/upctl/cfg" + "github.com/tlinden/up/upctl/lib" +) + +func DeleteCommand(conf *cfg.Config) *cobra.Command { + var deleteCmd = &cobra.Command{ + Use: "delete [options] ", + Short: "delete an upload", + Long: `Delete an upload identified by its id`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("No id specified to delete!") + } + + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + return lib.Delete(conf, args) + }, + } + + deleteCmd.Aliases = append(deleteCmd.Aliases, "rm") + deleteCmd.Aliases = append(deleteCmd.Aliases, "d") + + return deleteCmd +} diff --git a/upctl/cmd/list.go b/upctl/cmd/list.go index 72a9d78..e1a8920 100644 --- a/upctl/cmd/list.go +++ b/upctl/cmd/list.go @@ -38,5 +38,8 @@ func ListCommand(conf *cfg.Config) *cobra.Command { // options listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context") + listCmd.Aliases = append(listCmd.Aliases, "ls") + listCmd.Aliases = append(listCmd.Aliases, "l") + return listCmd } diff --git a/upctl/cmd/root.go b/upctl/cmd/root.go index 10b7d7e..4381664 100644 --- a/upctl/cmd/root.go +++ b/upctl/cmd/root.go @@ -88,6 +88,7 @@ func Execute() { rootCmd.AddCommand(UploadCommand(&conf)) rootCmd.AddCommand(ListCommand(&conf)) + rootCmd.AddCommand(DeleteCommand(&conf)) err := rootCmd.Execute() if err != nil { diff --git a/upctl/cmd/upload.go b/upctl/cmd/upload.go index 47d59ea..980eee3 100644 --- a/upctl/cmd/upload.go +++ b/upctl/cmd/upload.go @@ -36,12 +36,15 @@ func UploadCommand(conf *cfg.Config) *cobra.Command { // errors at this stage do not cause the usage to be shown cmd.SilenceUsage = true - return lib.Upload(conf, args) + return lib.UploadFiles(conf, args) }, } // options uploadCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)") + uploadCmd.Aliases = append(uploadCmd.Aliases, "up") + uploadCmd.Aliases = append(uploadCmd.Aliases, "u") + return uploadCmd } diff --git a/upctl/go.mod b/upctl/go.mod index 190d986..39e0da0 100644 --- a/upctl/go.mod +++ b/upctl/go.mod @@ -19,7 +19,9 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onsi/ginkgo/v2 v2.2.0 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/quic-go/qpack v0.4.0 // indirect diff --git a/upctl/go.sum b/upctl/go.sum index b7c0035..fd6df58 100644 --- a/upctl/go.sum +++ b/upctl/go.sum @@ -150,8 +150,12 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= diff --git a/upctl/lib/client.go b/upctl/lib/client.go index d235b7c..bcfad9d 100644 --- a/upctl/lib/client.go +++ b/upctl/lib/client.go @@ -43,6 +43,22 @@ type ListParams struct { Apicontext string `json:"apicontext"` } +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 Timestamp `json:"uploaded"` + Context string `json:"context"` +} + +type Uploads struct { + Entries []*Upload `json:"uploads"` + Success bool `json:"success"` + Message string `json:"message"` + Code int `json:"code"` +} + func Setup(c *cfg.Config, path string) *Request { client := req.C() if c.Debug { @@ -107,7 +123,7 @@ func GatherFiles(rq *Request, args []string) error { return nil } -func Upload(c *cfg.Config, args []string) error { +func UploadFiles(c *cfg.Config, args []string) error { // setup url, req.Request, timeout handling etc rq := Setup(c, "/file/") @@ -165,7 +181,10 @@ func HandleResponse(c *cfg.Config, resp *req.Response) error { } // all right - fmt.Println(r.Message) + if r.Message != "" { + fmt.Println(r.Message) + } + return nil } @@ -177,11 +196,46 @@ func List(c *cfg.Config, args []string) error { SetBodyJsonMarshal(params). Get(rq.Url) - fmt.Println("") - if err != nil { return err } - return HandleResponse(c, resp) + uploads := Uploads{} + + if err := json.Unmarshal([]byte(resp.String()), &uploads); err != nil { + return errors.New("Could not unmarshall JSON response: " + err.Error()) + } + + if !uploads.Success { + return errors.New(uploads.Message) + } + + // tablewriter + data := [][]string{} + for _, entry := range uploads.Entries { + data = append(data, []string{ + entry.Id, entry.Expire, entry.Context, entry.Uploaded.Format("2006-01-02 15:04:05"), + }) + } + return WriteTable([]string{"ID", "EXPIRE", "CONTEXT", "UPLOADED"}, data) +} + +func Delete(c *cfg.Config, args []string) error { + for _, id := range args { + rq := Setup(c, "/file/"+id+"/") + + resp, err := rq.R.Delete(rq.Url) + + if err != nil { + return err + } + + if err := HandleResponse(c, resp); err != nil { + return err + } + + fmt.Printf("Upload %s successfully deleted.\n", id) + } + + return nil } diff --git a/upctl/lib/output.go b/upctl/lib/output.go new file mode 100644 index 0000000..bfd4d6e --- /dev/null +++ b/upctl/lib/output.go @@ -0,0 +1,50 @@ +/* +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 ( + "github.com/olekukonko/tablewriter" + "os" +) + +func WriteTable(headers []string, data [][]string) error { + table := tablewriter.NewWriter(os.Stdout) + + table.SetHeader(headers) + table.AppendBulk(data) + + // for _, row := range data.entries { + // table.Append(trimRow(row)) + // } + + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") + table.SetNoWhiteSpace(true) + + table.Render() + + return nil +} diff --git a/upctl/lib/timestamp.go b/upctl/lib/timestamp.go new file mode 100644 index 0000000..4a0694f --- /dev/null +++ b/upctl/lib/timestamp.go @@ -0,0 +1,65 @@ +/* +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 + +// FIXME: import from upd!!!! + +import ( + "strconv" + "time" +) + +// https://gist.github.com/rhcarvalho/9338c3ff8850897c68bc74797c5dc25b + +// Timestamp is like time.Time, but knows how to unmarshal from JSON +// Unix timestamp numbers or RFC3339 strings, and marshal back into +// the same JSON representation. +type Timestamp struct { + time.Time + rfc3339 bool +} + +func (t Timestamp) MarshalJSON() ([]byte, error) { + if t.rfc3339 { + return t.Time.MarshalJSON() + } + return t.formatUnix() +} + +func (t *Timestamp) UnmarshalJSON(data []byte) error { + err := t.Time.UnmarshalJSON(data) + if err != nil { + return t.parseUnix(data) + } + t.rfc3339 = true + return nil +} + +func (t Timestamp) formatUnix() ([]byte, error) { + sec := float64(t.Time.UnixNano()) * float64(time.Nanosecond) / float64(time.Second) + return strconv.AppendFloat(nil, sec, 'f', -1, 64), nil +} + +func (t *Timestamp) parseUnix(data []byte) error { + f, err := strconv.ParseFloat(string(data), 64) + if err != nil { + return err + } + t.Time = time.Unix(0, int64(f*float64(time.Second/time.Nanosecond))) + return nil +} diff --git a/upd/api/cleaner.go b/upd/api/cleaner.go new file mode 100644 index 0000000..94de962 --- /dev/null +++ b/upd/api/cleaner.go @@ -0,0 +1,85 @@ +/* +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 api + +import ( + "fmt" + //"github.com/alecthomas/repr" + "encoding/json" + "github.com/tlinden/up/upd/cfg" + bolt "go.etcd.io/bbolt" + "path/filepath" + "time" +) + +func DeleteExpiredUploads(conf *cfg.Config, db *Db) error { + err := db.bolt.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(Bucket)) + + if bucket == nil { + return nil // nothin to delete so far + } + + err := bucket.ForEach(func(id, j []byte) error { + upload := &Upload{} + if err := json.Unmarshal(j, &upload); err != nil { + return fmt.Errorf("unable to unmarshal json: %s", err) + } + + if IsExpired(conf, upload.Uploaded.Time, upload.Expire) { + if err := bucket.Delete([]byte(id)); err != nil { + return nil + } + + cleanup(filepath.Join(conf.StorageDir, upload.Id)) + + Log("Cleaned up upload " + upload.Id) + } + + return nil + }) + + return err + }) + + if err != nil { + Log("DB error: %s", err.Error()) + } + + return err +} + +func BackgroundCleaner(conf *cfg.Config, db *Db) chan bool { + ticker := time.NewTicker(conf.CleanInterval) + fmt.Println(conf.CleanInterval) + done := make(chan bool) + + go func() { + for { + select { + case <-ticker.C: + DeleteExpiredUploads(conf, db) + case <-done: + ticker.Stop() + return + } + } + }() + + return done +} diff --git a/upd/api/common.go b/upd/api/common.go index a3f4a0a..f66865d 100644 --- a/upd/api/common.go +++ b/upd/api/common.go @@ -20,6 +20,7 @@ package api import ( "errors" "fmt" + "github.com/tlinden/up/upd/cfg" "regexp" "strconv" "time" @@ -39,6 +40,29 @@ type Meta struct { Expire string `json:"expire" 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 Timestamp `json:"uploaded"` + Context string `json:"context"` +} + +// this one is also used for marshalling to the client +type Uploads struct { + Entries []*Upload `json:"uploads"` + + // integrate the Result struct so we can signal success + Result +} + +// incoming id +type Id struct { + Id string `json:"name" xml:"name" form:"name"` +} + // vaious helbers func Log(format string, values ...any) { fmt.Printf("[DEBUG] "+format+"\n", values...) @@ -49,12 +73,6 @@ func Ts() string { return t.Format("2006-01-02-15-04-") } -func NormalizeFilename(file string) string { - r := regexp.MustCompile(`[^\w\d\-_\\.]`) - - return Ts() + r.ReplaceAllString(file, "") -} - /* We could use time.ParseDuration(), but this doesn't support days. @@ -96,9 +114,16 @@ func duration2int(duration string) int { aka: if(now - start) >= duration { time is up} */ -func IsExpired(start time.Time, duration string) bool { +func IsExpired(conf *cfg.Config, start time.Time, duration string) bool { + var expiretime int // seconds + now := time.Now() - expiretime := duration2int(duration) + + if duration == "asap" { + expiretime = conf.DefaultExpire + } else { + expiretime = duration2int(duration) + } if now.Unix()-start.Unix() >= int64(expiretime) { return true @@ -120,9 +145,8 @@ func IsExpired(start time.Time, duration string) bool { it. You may ignore the error and use the untainted string or bail out. */ -func Untaint(input string, wanted string) (string, error) { - re := regexp.MustCompile(wanted) - untainted := re.ReplaceAllString(input, "") +func Untaint(input string, wanted *regexp.Regexp) (string, error) { + untainted := wanted.ReplaceAllString(input, "") if len(untainted) != len(input) { return untainted, errors.New("Invalid input string!") diff --git a/upd/api/db.go b/upd/api/db.go index 4936d3c..db6a361 100644 --- a/upd/api/db.go +++ b/upd/api/db.go @@ -20,7 +20,7 @@ package api import ( "encoding/json" "fmt" - "github.com/alecthomas/repr" + //"github.com/alecthomas/repr" bolt "go.etcd.io/bbolt" ) @@ -31,20 +31,6 @@ type Db struct { bolt *bolt.DB } -// 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 Timestamp `json:"uploaded"` - Context string `json:"context"` -} - -type Uploads struct { - Entries []*Upload `json:"uploads"` -} - func NewDb(file string) (*Db, error) { b, err := bolt.Open(file, 0600, nil) db := Db{bolt: b} @@ -89,6 +75,11 @@ func (db *Db) Lookup(id string) (Upload, error) { err := db.bolt.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(Bucket)) + + if bucket == nil { + return fmt.Errorf("id %s not found", id) + } + j := bucket.Get([]byte(id)) if len(j) == 0 { @@ -114,6 +105,10 @@ func (db *Db) Delete(id string) error { err := db.bolt.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(Bucket)) + if bucket == nil { + return fmt.Errorf("id %s not found", id) + } + j := bucket.Get([]byte(id)) if len(j) == 0 { @@ -136,6 +131,10 @@ func (db *Db) List(apicontext string) (*Uploads, error) { err := db.bolt.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(Bucket)) + if bucket == nil { + return nil + } + err := bucket.ForEach(func(id, j []byte) error { upload := &Upload{} if err := json.Unmarshal(j, &upload); err != nil { diff --git a/upd/api/fileio.go b/upd/api/fileio.go index 35fc801..bcc093e 100644 --- a/upd/api/fileio.go +++ b/upd/api/fileio.go @@ -43,7 +43,7 @@ func cleanup(dir string) { func SaveFormFiles(c *fiber.Ctx, cfg *cfg.Config, files []*multipart.FileHeader, id string) ([]string, error) { members := []string{} for _, file := range files { - filename := NormalizeFilename(filepath.Base(file.Filename)) + filename, _ := Untaint(filepath.Base(file.Filename), cfg.RegNormalizedFilename) path := filepath.Join(cfg.StorageDir, id, filename) members = append(members, filename) Log("Received: %s => %s/%s", file.Filename, id, filename) diff --git a/upd/api/handlers.go b/upd/api/handlers.go index 72a3557..0b509ca 100644 --- a/upd/api/handlers.go +++ b/upd/api/handlers.go @@ -23,7 +23,6 @@ import ( "github.com/google/uuid" "github.com/tlinden/up/upd/cfg" - "encoding/json" "os" "path/filepath" "time" @@ -77,7 +76,7 @@ func FilePut(c *fiber.Ctx, cfg *cfg.Config, db *Db) (string, error) { if len(formdata.Expire) == 0 { entry.Expire = "asap" } else { - ex, err := Untaint(formdata.Expire, `[^dhms0-9]`) // duration or asap allowed + ex, err := Untaint(formdata.Expire, cfg.RegDuration) // duration or asap allowed if err != nil { return "", err } @@ -119,7 +118,7 @@ func FileGet(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) error { // we ignore c.Params("file"), cause it may be malign. Also we've // got it in the db anyway - id, err := Untaint(c.Params("id"), `[^a-zA-Z0-9\-]`) + id, err := Untaint(c.Params("id"), cfg.RegKey) if err != nil { return fiber.NewError(403, "Invalid id provided!") } @@ -157,14 +156,10 @@ func FileGet(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) error { return err } -type Id struct { - Id string `json:"name" xml:"name" form:"name"` -} +// delete file, id dir and db entry +func DeleteUpload(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { -func FileDelete(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { - // delete file, id dir and db entry - - id, err := Untaint(c.Params("id"), `[^a-zA-Z0-9\-]`) + id, err := Untaint(c.Params("id"), cfg.RegKey) if err != nil { return fiber.NewError(403, "Invalid id provided!") } @@ -184,23 +179,24 @@ func FileDelete(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { return nil } -func List(c *fiber.Ctx, cfg *cfg.Config, db *Db) (string, error) { - apicontext, err := Untaint(c.Params("apicontext"), `[^a-zA-Z0-9\-]`) +// returns the whole list + error code, no post processing by server +func List(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { + apicontext, err := Untaint(c.Params("apicontext"), cfg.RegKey) if err != nil { - return "", fiber.NewError(403, "Invalid api context provided!") + return JsonStatus(c, fiber.StatusForbidden, + "Invalid api context provided!") } uploads, err := db.List(apicontext) repr.Print(uploads) if err != nil { - return "", fiber.NewError(500, "Unable to list uploads: "+err.Error()) + return JsonStatus(c, fiber.StatusForbidden, + "Unable to list uploads: "+err.Error()) } - jsonlist, err := json.Marshal(uploads) - if err != nil { - return "", fiber.NewError(500, "json marshalling failure: "+err.Error()) - } + // if we reached this point we can signal success + uploads.Success = true + uploads.Code = fiber.StatusOK - Log(string(jsonlist)) - return string(jsonlist), nil + return c.Status(fiber.StatusOK).JSON(uploads) } diff --git a/upd/api/server.go b/upd/api/server.go index d4dbaf7..9bb8cad 100644 --- a/upd/api/server.go +++ b/upd/api/server.go @@ -20,6 +20,8 @@ package api import ( "errors" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/compress" + "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/requestid" "github.com/gofiber/fiber/v2/middleware/session" @@ -30,84 +32,140 @@ import ( // sessions are context specific and can be global savely var Sessionstore *session.Store -func Runserver(cfg *cfg.Config, args []string) error { +const shallExpire = true + +func Runserver(conf *cfg.Config, args []string) error { + // required for authenticated routes, used to store the api context Sessionstore = session.New() - router := fiber.New(fiber.Config{ - CaseSensitive: true, - StrictRouting: true, - Immutable: true, - Prefork: cfg.Prefork, - ServerHeader: "upd", - AppName: cfg.AppName, - BodyLimit: cfg.BodyLimit, - Network: cfg.Network, - }) - - router.Use(requestid.New()) - router.Use(logger.New(logger.Config{ - Format: "${pid} ${locals:requestid} ${status} - ${method} ${path}​\n", - })) - - db, err := NewDb(cfg.DbFile) + // bbolt db setup + db, err := NewDb(conf.DbFile) if err != nil { return err } defer db.Close() - AuthSetEndpoints(cfg.ApiPrefix, ApiVersion, []string{"/file"}) - AuthSetApikeys(cfg.Apicontext) + // setup authenticated endpoints + auth := SetupAuthStore(conf) - auth := keyauth.New(keyauth.Config{ - Validator: AuthValidateAPIKey, - ErrorHandler: AuthErrHandler, - }) + // setup api server + router := SetupServer(conf) - shallExpire := true - - api := router.Group(cfg.ApiPrefix + ApiVersion) + // authenticated routes + api := router.Group(conf.ApiPrefix + ApiVersion) { // authenticated routes api.Post("/file/", auth, func(c *fiber.Ctx) error { - msg, err := FilePut(c, cfg, db) + msg, err := FilePut(c, conf, db) return SendResponse(c, msg, err) }) api.Get("/file/:id/:file", auth, func(c *fiber.Ctx) error { - return FileGet(c, cfg, db) + return FileGet(c, conf, db) }) api.Get("/file/:id/", auth, func(c *fiber.Ctx) error { - return FileGet(c, cfg, db) + return FileGet(c, conf, db) }) api.Delete("/file/:id/", auth, func(c *fiber.Ctx) error { - return FileDelete(c, cfg, db) + err := DeleteUpload(c, conf, db) + return SendResponse(c, "", err) }) api.Get("/list/", auth, func(c *fiber.Ctx) error { - msg, err := List(c, cfg, db) - return SendResponse(c, msg, err) + return List(c, conf, db) }) } // public routes - router.Get("/", func(c *fiber.Ctx) error { - return c.Send([]byte("welcome to upload api, use /api enpoint!")) + { + router.Get("/", func(c *fiber.Ctx) error { + return c.Send([]byte("welcome to upload api, use /api enpoint!")) + }) + + router.Get("/download/:id/:file", func(c *fiber.Ctx) error { + return FileGet(c, conf, db, shallExpire) + }) + + router.Get("/download/:id/", func(c *fiber.Ctx) error { + return FileGet(c, conf, db, shallExpire) + }) + } + + // setup cleaner + quitcleaner := BackgroundCleaner(conf, db) + + router.Hooks().OnShutdown(func() error { + Log("Shutting down cleaner") + close(quitcleaner) + return nil }) - router.Get("/download/:id/:file", func(c *fiber.Ctx) error { - return FileGet(c, cfg, db, shallExpire) - }) - - router.Get("/download/:id/", func(c *fiber.Ctx) error { - return FileGet(c, cfg, db, shallExpire) - }) - - return router.Listen(cfg.Listen) - + return router.Listen(conf.Listen) } +func SetupAuthStore(conf *cfg.Config) func(*fiber.Ctx) error { + AuthSetEndpoints(conf.ApiPrefix, ApiVersion, []string{"/file"}) + AuthSetApikeys(conf.Apicontext) + + return keyauth.New(keyauth.Config{ + Validator: AuthValidateAPIKey, + ErrorHandler: AuthErrHandler, + }) +} + +func SetupServer(conf *cfg.Config) *fiber.App { + router := fiber.New(fiber.Config{ + CaseSensitive: true, + StrictRouting: true, + Immutable: true, + Prefork: conf.Prefork, + ServerHeader: "upd", + AppName: conf.AppName, + BodyLimit: conf.BodyLimit, + Network: conf.Network, + }) + + router.Use(requestid.New()) + + router.Use(logger.New(logger.Config{ + Format: "${pid} ${locals:requestid} ${status} - ${method} ${path}​\n", + })) + + router.Use(cors.New(cors.Config{ + AllowMethods: "GET,PUT,POST,DELETE", + ExposeHeaders: "Content-Type,Authorization,Accept", + })) + + router.Use(compress.New(compress.Config{ + Level: compress.LevelBestSpeed, + })) + + return router +} + +/* + Wrapper to respond with proper json status, message and code, + shall be prepared and called by the handlers directly. +*/ +func JsonStatus(c *fiber.Ctx, code int, msg string) error { + success := true + + if code != fiber.StatusOK { + success = false + } + + return c.Status(code).JSON(Result{ + Code: code, + Message: msg, + Success: success, + }) +} + +/* + Used for non json-aware handlers, called by server +*/ func SendResponse(c *fiber.Ctx, msg string, err error) error { if err != nil { code := fiber.StatusInternalServerError diff --git a/upd/cfg/config.go b/upd/cfg/config.go index c62c723..dbf02ed 100644 --- a/upd/cfg/config.go +++ b/upd/cfg/config.go @@ -18,7 +18,9 @@ package cfg import ( "fmt" + "regexp" "strings" + "time" ) const Version string = "v0.0.1" @@ -50,6 +52,13 @@ type Config struct { // only settable via config Apicontext []Apicontext `koanf:"apicontext"` + + // Internals only + RegNormalizedFilename *regexp.Regexp + RegDuration *regexp.Regexp + RegKey *regexp.Regexp + CleanInterval time.Duration + DefaultExpire int } func Getversion() string { @@ -88,4 +97,11 @@ func (c *Config) ApplyDefaults() { c.Network = "tcp" // dual stack } } + + c.RegNormalizedFilename = regexp.MustCompile(`[^\w\d\-_\.]`) + c.RegDuration = regexp.MustCompile(`[^dhms0-9]`) + c.RegKey = regexp.MustCompile(`[^a-zA-Z0-9\-]`) + + c.CleanInterval = 10 * time.Second + c.DefaultExpire = 30 * 86400 // 1 month }