- added describe command
- fixed v4+v6 handling
This commit is contained in:
2023-03-09 20:24:20 +01:00
parent 01a0dc054d
commit d6792dd6c8
15 changed files with 215 additions and 21 deletions

View File

@@ -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

View File

@@ -26,7 +26,7 @@ import (
func DeleteCommand(conf *cfg.Config) *cobra.Command {
var deleteCmd = &cobra.Command{
Use: "delete [options] <id>",
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 {

48
upctl/cmd/describe.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -18,8 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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()
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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)
}

View File

@@ -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

View File

@@ -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:

View File

@@ -1,6 +1,3 @@
# -*-ruby-*-
listen = ":8080"
bodylimit = 10000
bodylimit = 10001

View File

@@ -13,4 +13,4 @@ apicontext = [
}
]
url = "https://sokrates.daemon.de"