diff --git a/upctl/cmd/download.go b/upctl/cmd/download.go new file mode 100644 index 0000000..f322011 --- /dev/null +++ b/upctl/cmd/download.go @@ -0,0 +1,49 @@ +/* +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 DownloadCommand(conf *cfg.Config) *cobra.Command { + var listCmd = &cobra.Command{ + Use: "download [options] upload-id", + Long: "Download the file associated with an upload object.", + Short: `Download a file.`, + 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.Download(conf, args) + }, + } + + listCmd.Aliases = append(listCmd.Aliases, "down") + listCmd.Aliases = append(listCmd.Aliases, "get") + listCmd.Aliases = append(listCmd.Aliases, "g") + listCmd.Aliases = append(listCmd.Aliases, "fetch") + + return listCmd +} diff --git a/upctl/cmd/root.go b/upctl/cmd/root.go index 3a40327..1fcf178 100644 --- a/upctl/cmd/root.go +++ b/upctl/cmd/root.go @@ -90,6 +90,7 @@ func Execute() { rootCmd.AddCommand(ListCommand(&conf)) rootCmd.AddCommand(DeleteCommand(&conf)) rootCmd.AddCommand(DescribeCommand(&conf)) + rootCmd.AddCommand(DownloadCommand(&conf)) err := rootCmd.Execute() if err != nil { diff --git a/upctl/go.mod b/upctl/go.mod index 6e4c1d9..7c76a88 100644 --- a/upctl/go.mod +++ b/upctl/go.mod @@ -10,6 +10,7 @@ require ( ) require ( + github.com/alecthomas/repr v0.2.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/golang/mock v1.6.0 // indirect diff --git a/upctl/go.sum b/upctl/go.sum index ddd8280..f0fc9cd 100644 --- a/upctl/go.sum +++ b/upctl/go.sum @@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= diff --git a/upctl/lib/client.go b/upctl/lib/client.go index 4b8c546..93562c7 100644 --- a/upctl/lib/client.go +++ b/upctl/lib/client.go @@ -21,11 +21,14 @@ import ( "encoding/json" "errors" "fmt" + //"github.com/alecthomas/repr" "github.com/imroc/req/v3" "github.com/schollz/progressbar/v3" "github.com/tlinden/up/upctl/cfg" + "mime" "os" "path/filepath" + "regexp" "time" ) @@ -51,6 +54,7 @@ type Upload struct { Members []string `json:"members"` // contains multiple files, so File is an archive Uploaded Timestamp `json:"uploaded"` Context string `json:"context"` + Url string `json:"url"` } type Uploads struct { @@ -147,8 +151,6 @@ func UploadFiles(c *cfg.Config, args []string) error { SetUploadCallbackWithInterval(func(info req.UploadInfo) { left = float64(info.UploadedSize) / float64(info.FileSize) * 100.0 bar.Add(int(left)) - //fmt.Printf("\r%q uploaded %.2f%%", info.FileName, float64(info.UploadedSize)/float64(info.FileSize)*100.0) - //fmt.Println() }, 10*time.Millisecond). Post(rq.Url) @@ -242,3 +244,72 @@ func Describe(c *cfg.Config, args []string) error { return RespondExtended(resp) } + +func Download(c *cfg.Config, args []string) error { + id := args[0] + + // progres bar + bar := progressbar.Default(100) + + callback := func(info req.DownloadInfo) { + if info.Response.Response != nil { + bar.Add(1) + } + } + + rq := Setup(c, "/file/"+id+"/") + resp, err := rq.R. + SetOutputFile(id). + SetDownloadCallback(callback). + Get(rq.Url) + + if err != nil { + return err + } + + _, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition")) + if err != nil { + os.Remove(id) + return err + } + + filename := params["filename"] + if filename == "" { + os.Remove(id) + return fmt.Errorf("No filename provided!") + } + + cleanfilename, _ := Untaint(filename, regexp.MustCompile(`[^a-zA-Z0-9\-\._]`)) + + if err := os.Rename(id, cleanfilename); err != nil { + os.Remove(id) + return fmt.Errorf("\nUnable to rename file: " + err.Error()) + } + + fmt.Printf("%s successfully downloaded to file %s.", id, cleanfilename) + + return nil +} + +/* + Untaint user input, that is: remove all non supported chars. + + wanted is a regexp matching chars we shall leave. Everything else + will be removed. Eg: + + untainted := Untaint(input, `[^a-zA-Z0-9\-]`) + + Returns a new string and an error if the input string has been + modified. It's the callers choice to decide what to do about + it. You may ignore the error and use the untainted string or bail + out. +*/ +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!") + } + + return untainted, nil +} diff --git a/upctl/lib/output.go b/upctl/lib/output.go index 27d201f..58c6e97 100644 --- a/upctl/lib/output.go +++ b/upctl/lib/output.go @@ -73,6 +73,7 @@ func WriteExtended(uploads *Uploads) { fmt.Printf(format, "Context", entry.Context) fmt.Printf(format, "Uploaded", entry.Uploaded) fmt.Printf(format, "Filename", entry.File) + fmt.Printf(format, "Url", entry.Url) fmt.Println() } } diff --git a/upd/api/common.go b/upd/api/common.go index d81cd2b..b9dbd34 100644 --- a/upd/api/common.go +++ b/upd/api/common.go @@ -49,6 +49,7 @@ type Upload struct { Members []string `json:"members"` // contains multiple files, so File is an archive Uploaded Timestamp `json:"uploaded"` Context string `json:"context"` + Url string `json:"url"` } // this one is also used for marshalling to the client @@ -170,5 +171,10 @@ func GetApicontext(c *fiber.Ctx) (string, error) { return "", fmt.Errorf("Unable to initialize session store from context: " + err.Error()) } - return sess.Get("apicontext").(string), nil + apicontext := sess.Get("apicontext") + if apicontext != nil { + return apicontext.(string), nil + } + + return "", nil } diff --git a/upd/api/handlers.go b/upd/api/handlers.go index 5ae3738..ca09fa7 100644 --- a/upd/api/handlers.go +++ b/upd/api/handlers.go @@ -25,6 +25,7 @@ import ( "os" "path/filepath" + "strings" "time" ) @@ -260,6 +261,10 @@ func Describe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { "No upload with that id could be found!") } + for _, upload := range uploads.Entries { + upload.Url = strings.Join([]string{cfg.Url, "download", id, upload.File}, "/") + } + // if we reached this point we can signal success uploads.Success = true uploads.Code = fiber.StatusOK diff --git a/upd/api/server.go b/upd/api/server.go index bc28d94..105aefe 100644 --- a/upd/api/server.go +++ b/upd/api/server.go @@ -54,28 +54,31 @@ func Runserver(conf *cfg.Config, args []string) error { // authenticated routes api := router.Group(conf.ApiPrefix + ApiVersion) { - // authenticated routes + // upload api.Post("/file/", auth, func(c *fiber.Ctx) error { return FilePut(c, conf, db) }) + // download w/o expire api.Get("/file/:id/:file", auth, func(c *fiber.Ctx) error { return FileGet(c, conf, db) }) - api.Get("/file/:id/", auth, func(c *fiber.Ctx) error { return FileGet(c, conf, db) }) + // remove api.Delete("/file/:id/", auth, func(c *fiber.Ctx) error { err := DeleteUpload(c, conf, db) return SendResponse(c, "", err) }) + // listing api.Get("/list/", auth, func(c *fiber.Ctx) error { return List(c, conf, db) }) + // info api.Get("/upload/:id/", auth, func(c *fiber.Ctx) error { return Describe(c, conf, db) }) diff --git a/upd/upd.hcl b/upd/upd.hcl index ea3df2d..1c4e7e9 100644 --- a/upd/upd.hcl +++ b/upd/upd.hcl @@ -13,7 +13,7 @@ apicontext = [ } ] -url = "https://sokrates.daemon.de" +#url = "https://sokrates.daemon.de" # this is the root context with all permissions super = "root"