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
}