From d6792dd6c82ffe32e21550d397707d4189860e53 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Thu, 9 Mar 2023 20:24:20 +0100 Subject: [PATCH] Changes: - added describe command - fixed v4+v6 handling --- README.md | 4 +++- upctl/cmd/delete.go | 2 +- upctl/cmd/describe.go | 48 ++++++++++++++++++++++++++++++++++++++++++ upctl/cmd/list.go | 2 +- upctl/cmd/root.go | 1 + upctl/cmd/upload.go | 2 +- upctl/lib/client.go | 27 ++++++++++++++++++++++++ upctl/lib/output.go | 28 ++++++++++++++++++++++++ upctl/lib/timestamp.go | 34 ++++++++++++++++++++++++++++++ upd/api/db.go | 28 ++++++++++++++++++++++++ upd/api/handlers.go | 33 ++++++++++++++++++++++++----- upd/api/server.go | 4 ++++ upd/cfg/config.go | 16 ++++++++------ upd/nokeys.hcl | 5 +---- upd/upd.hcl | 2 +- 15 files changed, 215 insertions(+), 21 deletions(-) create mode 100644 upctl/cmd/describe.go diff --git a/README.md b/README.md index e6d3aed..028aade 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ Simple standalone file upload server with api and cli - also serve a html upload page - add metrics -- create cobra client commands (upload, list, delete, edit) - add authorization checks for delete and list based on apicontext +- change output of upload, use the same as list +- do not manually generate output urls, use fiber.GetRoute() +- import code from upd into upctl to avoid duplicates, like the time stuff we've now ## BUGS diff --git a/upctl/cmd/delete.go b/upctl/cmd/delete.go index 6b03f64..53e067c 100644 --- a/upctl/cmd/delete.go +++ b/upctl/cmd/delete.go @@ -26,7 +26,7 @@ import ( func DeleteCommand(conf *cfg.Config) *cobra.Command { var deleteCmd = &cobra.Command{ Use: "delete [options] ", - Short: "delete an upload", + Short: "Delete an upload", Long: `Delete an upload identified by its id`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { diff --git a/upctl/cmd/describe.go b/upctl/cmd/describe.go new file mode 100644 index 0000000..70858da --- /dev/null +++ b/upctl/cmd/describe.go @@ -0,0 +1,48 @@ +/* +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 DescribeCommand(conf *cfg.Config) *cobra.Command { + var listCmd = &cobra.Command{ + Use: "describe [options] upload-id", + Long: "Show detailed informations about an upload object.", + Short: `Describe an upload.`, + 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.Describe(conf, args) + }, + } + + listCmd.Aliases = append(listCmd.Aliases, "des") + listCmd.Aliases = append(listCmd.Aliases, "info") + listCmd.Aliases = append(listCmd.Aliases, "i") + + return listCmd +} diff --git a/upctl/cmd/list.go b/upctl/cmd/list.go index e1a8920..d141196 100644 --- a/upctl/cmd/list.go +++ b/upctl/cmd/list.go @@ -25,7 +25,7 @@ import ( func ListCommand(conf *cfg.Config) *cobra.Command { var listCmd = &cobra.Command{ Use: "list [options] [file ..]", - Short: "list uploads", + Short: "List uploads", Long: `List uploads.`, RunE: func(cmd *cobra.Command, args []string) error { // errors at this stage do not cause the usage to be shown diff --git a/upctl/cmd/root.go b/upctl/cmd/root.go index 4381664..3a40327 100644 --- a/upctl/cmd/root.go +++ b/upctl/cmd/root.go @@ -89,6 +89,7 @@ func Execute() { rootCmd.AddCommand(UploadCommand(&conf)) rootCmd.AddCommand(ListCommand(&conf)) rootCmd.AddCommand(DeleteCommand(&conf)) + rootCmd.AddCommand(DescribeCommand(&conf)) err := rootCmd.Execute() if err != nil { diff --git a/upctl/cmd/upload.go b/upctl/cmd/upload.go index 980eee3..79cb168 100644 --- a/upctl/cmd/upload.go +++ b/upctl/cmd/upload.go @@ -26,7 +26,7 @@ import ( func UploadCommand(conf *cfg.Config) *cobra.Command { var uploadCmd = &cobra.Command{ Use: "upload [options] [file ..]", - Short: "upload files", + Short: "Upload files", Long: `Upload files to an upload api.`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { diff --git a/upctl/lib/client.go b/upctl/lib/client.go index bcfad9d..24ba55a 100644 --- a/upctl/lib/client.go +++ b/upctl/lib/client.go @@ -59,6 +59,8 @@ type Uploads struct { Code int `json:"code"` } +const Maxwidth = 10 + func Setup(c *cfg.Config, path string) *Request { client := req.C() if c.Debug { @@ -239,3 +241,28 @@ func Delete(c *cfg.Config, args []string) error { return nil } + +func Describe(c *cfg.Config, args []string) error { + id := args[0] // we describe only 1 object + + rq := Setup(c, "/upload/"+id+"/") + resp, err := rq.R.Get(rq.Url) + + if err != nil { + return err + } + + 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) + } + + WriteExtended(&uploads) + + return nil +} diff --git a/upctl/lib/output.go b/upctl/lib/output.go index bfd4d6e..f988e7f 100644 --- a/upctl/lib/output.go +++ b/upctl/lib/output.go @@ -18,8 +18,10 @@ along with this program. If not, see . package lib import ( + "fmt" "github.com/olekukonko/tablewriter" "os" + "time" ) func WriteTable(headers []string, data [][]string) error { @@ -48,3 +50,29 @@ func WriteTable(headers []string, data [][]string) error { return nil } + +func prepareExpire(expire string, start Timestamp) string { + switch expire { + case "asap": + return "On first access" + default: + return time.Unix(start.Unix()+int64(duration2int(expire)), 0).Format("2006-01-02 15:04:05") + } + + return "" +} + +func WriteExtended(uploads *Uploads) { + format := fmt.Sprintf("%%%ds: %%s\n", Maxwidth) + + // we shall only have 1 element, however, if we ever support more, here we go + for _, entry := range uploads.Entries { + expire := prepareExpire(entry.Expire, entry.Uploaded) + fmt.Printf(format, "Id", entry.Id) + fmt.Printf(format, "Expire", expire) + fmt.Printf(format, "Context", entry.Context) + fmt.Printf(format, "Uploaded", entry.Uploaded) + fmt.Printf(format, "Filename", entry.File) + fmt.Println() + } +} diff --git a/upctl/lib/timestamp.go b/upctl/lib/timestamp.go index 4a0694f..3350bd9 100644 --- a/upctl/lib/timestamp.go +++ b/upctl/lib/timestamp.go @@ -20,6 +20,7 @@ package lib // FIXME: import from upd!!!! import ( + "regexp" "strconv" "time" ) @@ -63,3 +64,36 @@ func (t *Timestamp) parseUnix(data []byte) error { t.Time = time.Unix(0, int64(f*float64(time.Second/time.Nanosecond))) return nil } + +/* + We could use time.ParseDuration(), but this doesn't support days. + + We could also use github.com/xhit/go-str2duration/v2, which does + the job, but it's just another dependency, just for this little + gem. And we don't need a time.Time value. + + Convert a duration into seconds (int). + Valid time units are "s", "m", "h" and "d". +*/ +func duration2int(duration string) int { + re := regexp.MustCompile(`(\d+)([dhms])`) + seconds := 0 + + for _, match := range re.FindAllStringSubmatch(duration, -1) { + if len(match) == 3 { + v, _ := strconv.Atoi(match[1]) + switch match[2][0] { + case 'd': + seconds += v * 86400 + case 'h': + seconds += v * 3600 + case 'm': + seconds += v * 60 + case 's': + seconds += v + } + } + } + + return seconds +} diff --git a/upd/api/db.go b/upd/api/db.go index db6a361..8cb5109 100644 --- a/upd/api/db.go +++ b/upd/api/db.go @@ -157,3 +157,31 @@ func (db *Db) List(apicontext string) (*Uploads, error) { return uploads, err } + +// we only return one obj here, but could return more later +func (db *Db) Get(id string) (*Uploads, error) { + uploads := &Uploads{} + + err := db.bolt.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(Bucket)) + if bucket == nil { + return nil + } + + j := bucket.Get([]byte(id)) + if j == nil { + return fmt.Errorf("No upload object found with id %s", id) + } + + upload := &Upload{} + if err := json.Unmarshal(j, &upload); err != nil { + return fmt.Errorf("unable to unmarshal json: %s", err) + } + + uploads.Entries = append(uploads.Entries, upload) + + return nil + }) + + return uploads, err +} diff --git a/upd/api/handlers.go b/upd/api/handlers.go index 0b509ca..3ea4447 100644 --- a/upd/api/handlers.go +++ b/upd/api/handlers.go @@ -18,7 +18,7 @@ along with this program. If not, see . package api import ( - "github.com/alecthomas/repr" + //"github.com/alecthomas/repr" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "github.com/tlinden/up/upd/cfg" @@ -161,11 +161,13 @@ func DeleteUpload(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { id, err := Untaint(c.Params("id"), cfg.RegKey) if err != nil { - return fiber.NewError(403, "Invalid id provided!") + return JsonStatus(c, fiber.StatusForbidden, + "Invalid id provided!") } if len(id) == 0 { - return fiber.NewError(403, "No id given!") + return JsonStatus(c, fiber.StatusForbidden, + "No id specified!") } cleanup(filepath.Join(cfg.StorageDir, id)) @@ -173,7 +175,8 @@ func DeleteUpload(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { err = db.Delete(id) if err != nil { // non existent db entry with that id, or other db error, see logs - return fiber.NewError(404, "No upload with that id could be found!") + return JsonStatus(c, fiber.StatusForbidden, + "No upload with that id could be found!") } return nil @@ -188,7 +191,6 @@ func List(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } uploads, err := db.List(apicontext) - repr.Print(uploads) if err != nil { return JsonStatus(c, fiber.StatusForbidden, "Unable to list uploads: "+err.Error()) @@ -200,3 +202,24 @@ func List(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { return c.Status(fiber.StatusOK).JSON(uploads) } + +// returns just one upload obj + error code, no post processing by server +func Describe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { + id, err := Untaint(c.Params("id"), cfg.RegKey) + if err != nil { + return JsonStatus(c, fiber.StatusForbidden, + "Invalid id provided!") + } + + uploads, err := db.Get(id) + if err != nil { + return JsonStatus(c, fiber.StatusForbidden, + "No upload with that id could be found!") + } + + // if we reached this point we can signal success + uploads.Success = true + uploads.Code = fiber.StatusOK + + return c.Status(fiber.StatusOK).JSON(uploads) +} diff --git a/upd/api/server.go b/upd/api/server.go index 9bb8cad..b6194c7 100644 --- a/upd/api/server.go +++ b/upd/api/server.go @@ -76,6 +76,10 @@ func Runserver(conf *cfg.Config, args []string) error { api.Get("/list/", auth, func(c *fiber.Ctx) error { return List(c, conf, db) }) + + api.Get("/upload/:id/", auth, func(c *fiber.Ctx) error { + return Describe(c, conf, db) + }) } // public routes diff --git a/upd/cfg/config.go b/upd/cfg/config.go index dbf02ed..324ec0a 100644 --- a/upd/cfg/config.go +++ b/upd/cfg/config.go @@ -34,18 +34,18 @@ type Apicontext struct { // holds the whole configs, filled by commandline flags, env and config file type Config struct { - ApiPrefix string `koanf:"apiprefix"` + ApiPrefix string `koanf:"apiprefix"` // path prefix Debug bool `koanf:"debug"` - Listen string `koanf:"listen"` - StorageDir string `koanf:"storagedir"` - Url string `koanf:"url"` + Listen string `koanf:"listen"` // [host]:port + StorageDir string `koanf:"storagedir"` // db and uploads go there + Url string `koanf:"url"` // public visible url, might be different from Listen DbFile string `koanf:"dbfile"` // fiber settings, see: // https://docs.gofiber.io/api/fiber/#config - Prefork bool `koanf:"prefork"` - AppName string `koanf:"appname"` - BodyLimit int `koanf:"bodylimit"` + Prefork bool `koanf:"prefork"` // default: nope + AppName string `koanf:"appname"` // "upd" + BodyLimit int `koanf:"bodylimit"` // much V4only bool `koanf:"ipv4"` V6only bool `koanf:"ipv6"` Network string @@ -86,6 +86,8 @@ func (c *Config) ApplyDefaults() { } switch { + case c.V4only && c.V6only: + c.Network = "tcp" // dual stack case c.V4only: c.Network = "tcp4" case c.V6only: diff --git a/upd/nokeys.hcl b/upd/nokeys.hcl index 42fbd25..df89c42 100644 --- a/upd/nokeys.hcl +++ b/upd/nokeys.hcl @@ -1,6 +1,3 @@ # -*-ruby-*- listen = ":8080" -bodylimit = 10000 - - - +bodylimit = 10001 diff --git a/upd/upd.hcl b/upd/upd.hcl index ef61dbb..f6fce6b 100644 --- a/upd/upd.hcl +++ b/upd/upd.hcl @@ -13,4 +13,4 @@ apicontext = [ } ] - +url = "https://sokrates.daemon.de"