From 07bb5569a78b4a4c5094d8edd508c9942aaf3c18 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Sun, 26 Mar 2023 20:19:31 +0200 Subject: [PATCH] added more form functions --- api/form_handlers.go | 121 ++++++++---------- api/server.go | 37 +++--- cfg/config.go | 2 + cmd/root.go | 15 +++ upctl/cfg/config.go | 3 + upctl/cmd/formcommands.go | 72 +++++++++++ upctl/cmd/{subcommands.go => maincommands.go} | 0 upctl/cmd/root.go | 1 + upctl/lib/client.go | 26 ++++ upctl/lib/output.go | 14 +- 10 files changed, 200 insertions(+), 91 deletions(-) create mode 100644 upctl/cmd/formcommands.go rename upctl/cmd/{subcommands.go => maincommands.go} (100%) diff --git a/api/form_handlers.go b/api/form_handlers.go index de62a9e..9d9807f 100644 --- a/api/form_handlers.go +++ b/api/form_handlers.go @@ -24,6 +24,8 @@ import ( "github.com/tlinden/cenophane/cfg" "github.com/tlinden/cenophane/common" + "bytes" + "html/template" "strings" "time" ) @@ -80,60 +82,8 @@ func FormCreate(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { return c.Status(fiber.StatusOK).JSON(res) } -/* -func FormFetch(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) error { - // deliver a file and delete it if expire is set to asap - - // we ignore c.Params("file"), cause it may be malign. Also we've - // got it in the db anyway - id, err := common.Untaint(c.Params("id"), cfg.RegKey) - if err != nil { - return fiber.NewError(403, "Invalid id provided!") - } - - // retrieve the API Context name from the session - apicontext, err := GetApicontext(c) - if err != nil { - return JsonStatus(c, fiber.StatusInternalServerError, - "Unable to initialize session store from context: "+err.Error()) - } - - upload, err := db.Lookup(apicontext, id) - if err != nil { - // non existent db entry with that id, or other db error, see logs - return fiber.NewError(404, "No download with that id could be found!") - } - - file := upload.File - filename := filepath.Join(cfg.StorageDir, id, file) - - if _, err := os.Stat(filename); err != nil { - // db entry is there, but file isn't (anymore?) - go db.Delete(apicontext, id) - return fiber.NewError(404, "No download with that id could be found!") - } - - // finally put the file to the client - err = c.Download(filename, file) - - if len(shallExpire) > 0 { - if shallExpire[0] == true { - go func() { - // check if we need to delete the file now and do it in the background - if upload.Expire == "asap" { - cleanup(filepath.Join(cfg.StorageDir, id)) - db.Delete(apicontext, id) - } - }() - } - } - - return err -} - -// delete file, id dir and db entry +// delete form func FormDelete(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { - id, err := common.Untaint(c.Params("id"), cfg.RegKey) if err != nil { return JsonStatus(c, fiber.StatusForbidden, @@ -156,11 +106,9 @@ func FormDelete(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { if err != nil { // non existent db entry with that id, or other db error, see logs return JsonStatus(c, fiber.StatusForbidden, - "No upload with that id could be found!") + "No form with that id could be found!") } - cleanup(filepath.Join(cfg.StorageDir, id)) - return nil } @@ -187,20 +135,20 @@ func FormsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { } // get list - uploads, err := db.FormsList(apicontext, filter) + response, err := db.List(apicontext, filter, common.TypeForm) if err != nil { return JsonStatus(c, fiber.StatusForbidden, - "Unable to list uploads: "+err.Error()) + "Unable to list forms: "+err.Error()) } // if we reached this point we can signal success - uploads.Success = true - uploads.Code = fiber.StatusOK + response.Success = true + response.Code = fiber.StatusOK - return c.Status(fiber.StatusOK).JSON(uploads) + return c.Status(fiber.StatusOK).JSON(response) } -// returns just one upload obj + error code, no post processing by server +// returns just one form obj + error code func FormDescribe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { id, err := common.Untaint(c.Params("id"), cfg.RegKey) if err != nil { @@ -215,20 +163,55 @@ func FormDescribe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { "Unable to initialize session store from context: "+err.Error()) } - uploads, err := db.Get(apicontext, id) + response, err := db.Get(apicontext, id, common.TypeForm) if err != nil { return JsonStatus(c, fiber.StatusForbidden, - "No upload with that id could be found!") + "No form with that id could be found!") } - for _, upload := range uploads.Entries { - upload.Url = strings.Join([]string{cfg.Url, "download", id, upload.File}, "/") + for _, form := range response.Forms { + form.Url = strings.Join([]string{cfg.Url, "form", id}, "/") } // if we reached this point we can signal success - uploads.Success = true - uploads.Code = fiber.StatusOK + response.Success = true + response.Code = fiber.StatusOK - return c.Status(fiber.StatusOK).JSON(uploads) + return c.Status(fiber.StatusOK).JSON(response) } + +/* + Render the upload html form. Template given by --formpage, stored + as text in cfg.Formpage. It will be rendered using golang's + template engine, data to be filled in is the form matching the + given id. */ +func FormPage(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallexpire bool) error { + id, err := common.Untaint(c.Params("id"), cfg.RegKey) + if err != nil { + return c.Status(fiber.StatusForbidden).SendString("Invalid id provided!") + } + + apicontext, err := GetApicontext(c) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Unable to initialize session store from context:" + err.Error()) + } + + response, err := db.Get(apicontext, id, common.TypeForm) + if err != nil || len(response.Forms) == 0 { + return c.Status(fiber.StatusForbidden).SendString("No form with that id could be found!") + } + + t := template.New("form") + if t, err = t.Parse(cfg.Formpage); err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Unable to load form template: " + err.Error()) + } + + var out bytes.Buffer + if err := t.Execute(&out, response.Forms[0]); err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Unable to render form template: " + err.Error()) + } + + c.Set("Content-type", "text/html; charset=utf-8") + return c.Status(fiber.StatusOK).SendString(out.String()) +} diff --git a/api/server.go b/api/server.go index ee3f20f..697dd24 100644 --- a/api/server.go +++ b/api/server.go @@ -86,23 +86,21 @@ func Runserver(conf *cfg.Config, args []string) error { return FormCreate(c, conf, db) }) - /* - // remove - api.Delete("/forms/:id", auth, func(c *fiber.Ctx) error { - err := FormDelete(c, conf, db) - return SendResponse(c, "", err) - }) + // remove + api.Delete("/forms/:id", auth, func(c *fiber.Ctx) error { + err := FormDelete(c, conf, db) + return SendResponse(c, "", err) + }) - // listing - api.Get("/forms", auth, func(c *fiber.Ctx) error { - return FormsList(c, conf, db) - }) + // listing + api.Get("/forms", auth, func(c *fiber.Ctx) error { + return FormsList(c, conf, db) + }) - // info/describe - api.Get("/forms/:id", auth, func(c *fiber.Ctx) error { - return FormDescribe(c, conf, db) - }) - */ + // info/describe + api.Get("/forms/:id", auth, func(c *fiber.Ctx) error { + return FormDescribe(c, conf, db) + }) } // public routes @@ -119,11 +117,10 @@ func Runserver(conf *cfg.Config, args []string) error { return UploadFetch(c, conf, db, shallExpire) }) - /* - router.Get("/form/:id", func(c *fiber.Ctx) error { - return FormFetch(c, conf, db, shallExpire) - }) - */ + router.Get("/form/:id", func(c *fiber.Ctx) error { + return FormPage(c, conf, db, shallExpire) + }) + } // setup cleaner diff --git a/cfg/config.go b/cfg/config.go index 5f582c1..59e6f15 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -34,6 +34,7 @@ type Apicontext struct { // holds the whole configs, filled by commandline flags, env and config file type Config struct { + // Flags+config file settings ApiPrefix string `koanf:"apiprefix"` // path prefix Debug bool `koanf:"debug"` Listen string `koanf:"listen"` // [host]:port @@ -42,6 +43,7 @@ type Config struct { DbFile string `koanf:"dbfile"` Super string `koanf:"super"` // the apicontext which has all permissions Frontpage string `koanf:"frontpage"` // a html file + Formpage string `koanf:"formpage"` // a html file // fiber settings, see: // https://docs.gofiber.io/api/fiber/#config diff --git a/cmd/root.go b/cmd/root.go index 2fe9daf..c965b9c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -65,6 +65,7 @@ func Execute() error { f.StringVarP(&conf.Super, "super", "", "", "The API Context which has permissions on all contexts") f.StringVarP(&conf.Frontpage, "frontpage", "", "welcome to upload api, use /api enpoint!", "Content or filename to be displayed on / in case someone visits") + f.StringVarP(&conf.Formpage, "formpage", "", "", "Content or filename to be displayed for forms (must be a go template)") // server settings f.BoolVarP(&conf.V4only, "ipv4", "4", false, "Only listen on ipv4") @@ -142,6 +143,20 @@ func Execute() error { } } + // Formpage? + if conf.Formpage != "" { + if _, err := os.Stat(conf.Formpage); err == nil { + // it's a filename, try to use it + content, err := ioutil.ReadFile(conf.Formpage) + if err != nil { + return errors.New("error loading config: " + err.Error()) + } + + // replace the filename + conf.Formpage = string(content) + } + } + switch { case ShowVersion: fmt.Println(cfg.Getversion()) diff --git a/upctl/cfg/config.go b/upctl/cfg/config.go index 85cf230..8be2fb4 100644 --- a/upctl/cfg/config.go +++ b/upctl/cfg/config.go @@ -43,6 +43,9 @@ type Config struct { // required to intercept requests using httpmock in tests Mock bool + + // required for forms + Description string } func Getversion() string { diff --git a/upctl/cmd/formcommands.go b/upctl/cmd/formcommands.go new file mode 100644 index 0000000..ef62c36 --- /dev/null +++ b/upctl/cmd/formcommands.go @@ -0,0 +1,72 @@ +/* +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/cenophane/upctl/cfg" + "github.com/tlinden/cenophane/upctl/lib" + "os" +) + +func FormCommand(conf *cfg.Config) *cobra.Command { + var formCmd = &cobra.Command{ + Use: "form {create|delete|modify|list}", + Short: "Form commands", + Long: `Manage upload forms.`, + RunE: func(cmd *cobra.Command, args []string) error { + // errors at this stage do not cause the usage to be shown + //cmd.SilenceUsage = true + if len(args) == 0 { + cmd.Help() + os.Exit(0) + } + return nil + }, + } + + formCmd.Aliases = append(formCmd.Aliases, "frm") + formCmd.Aliases = append(formCmd.Aliases, "f") + + formCmd.AddCommand(FormCreateCommand(conf)) + + return formCmd +} + +func FormCreateCommand(conf *cfg.Config) *cobra.Command { + var formCreateCmd = &cobra.Command{ + Use: "create [options]", + Short: "Create a new form", + Long: `Create a new form for consumers so they can upload something.`, + RunE: func(cmd *cobra.Command, args []string) error { + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + return lib.CreateForm(os.Stdout, conf) + }, + } + + // options + formCreateCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)") + formCreateCmd.PersistentFlags().StringVarP(&conf.Description, "description", "D", "", "Description of the form") + + formCreateCmd.Aliases = append(formCreateCmd.Aliases, "add") + formCreateCmd.Aliases = append(formCreateCmd.Aliases, "+") + + return formCreateCmd +} diff --git a/upctl/cmd/subcommands.go b/upctl/cmd/maincommands.go similarity index 100% rename from upctl/cmd/subcommands.go rename to upctl/cmd/maincommands.go diff --git a/upctl/cmd/root.go b/upctl/cmd/root.go index d1f953d..6f9ec06 100644 --- a/upctl/cmd/root.go +++ b/upctl/cmd/root.go @@ -92,6 +92,7 @@ func Execute() { rootCmd.AddCommand(DeleteCommand(&conf)) rootCmd.AddCommand(DescribeCommand(&conf)) rootCmd.AddCommand(DownloadCommand(&conf)) + rootCmd.AddCommand(FormCommand(&conf)) err := rootCmd.Execute() if err != nil { diff --git a/upctl/lib/client.go b/upctl/lib/client.go index 9c165f5..0d170ff 100644 --- a/upctl/lib/client.go +++ b/upctl/lib/client.go @@ -320,3 +320,29 @@ func Download(w io.Writer, c *cfg.Config, args []string) error { return nil } + +/**** Forms stuff ****/ +func CreateForm(w io.Writer, c *cfg.Config) error { + // setup url, req.Request, timeout handling etc + rq := Setup(c, "/forms") + + // actual post w/ settings + resp, err := rq.R. + SetFormData(map[string]string{ + "expire": c.Expire, + "description": c.Description, + }). + Post(rq.Url) + + if err != nil { + return err + } + + if err := HandleResponse(c, resp); err != nil { + return err + } + + return RespondExtended(w, resp) + + return nil +} diff --git a/upctl/lib/output.go b/upctl/lib/output.go index 5f06aee..1590769 100644 --- a/upctl/lib/output.go +++ b/upctl/lib/output.go @@ -35,7 +35,8 @@ func prepareExpire(expire string, start common.Timestamp) string { case "asap": return "On first access" default: - return time.Unix(start.Unix()+int64(common.Duration2int(expire)), 0).Format("2006-01-02 15:04:05") + return time.Unix(start.Unix()+int64(common.Duration2int(expire)), 0). + Format("2006-01-02 15:04:05") } return "" @@ -87,7 +88,16 @@ func WriteExtended(w io.Writer, response *common.Response) { fmt.Fprintln(w) } - // FIXME: add response.Forms loop here + for _, entry := range response.Forms { + expire := prepareExpire(entry.Expire, entry.Created) + fmt.Fprintf(w, format, "Id", entry.Id) + fmt.Fprintf(w, format, "Expire", expire) + fmt.Fprintf(w, format, "Context", entry.Context) + fmt.Fprintf(w, format, "Created", entry.Created) + fmt.Fprintf(w, format, "Description", entry.Description) + fmt.Fprintf(w, format, "Url", entry.Url) + fmt.Fprintln(w) + } } // extract an common.Uploads{} struct from json response