generalized DB engine to support multiple types (upload+form)

This commit is contained in:
2023-03-25 16:20:42 +01:00
parent b6dfafd1c1
commit d028fb2db1
6 changed files with 432 additions and 44 deletions

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package api package api
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/tlinden/cenophane/cfg" "github.com/tlinden/cenophane/cfg"
"github.com/tlinden/cenophane/common" "github.com/tlinden/cenophane/common"
@@ -26,7 +25,7 @@ import (
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
const Bucket string = "uploads" const Bucket string = "data"
// wrapper for bolt db // wrapper for bolt db
type Db struct { type Db struct {
@@ -44,14 +43,14 @@ func (db *Db) Close() {
db.bolt.Close() db.bolt.Close()
} }
func (db *Db) Insert(id string, entry *common.Upload) error { func (db *Db) Insert(id string, entry common.Dbentry) error {
err := db.bolt.Update(func(tx *bolt.Tx) error { err := db.bolt.Update(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte(Bucket)) bucket, err := tx.CreateBucketIfNotExists([]byte(Bucket))
if err != nil { if err != nil {
return fmt.Errorf("create bucket: %s", err) return fmt.Errorf("create bucket: %s", err)
} }
jsonentry, err := json.Marshal(entry) jsonentry, err := entry.Marshal()
if err != nil { if err != nil {
return fmt.Errorf("json marshalling failure: %s", err) return fmt.Errorf("json marshalling failure: %s", err)
} }
@@ -61,9 +60,6 @@ func (db *Db) Insert(id string, entry *common.Upload) error {
return fmt.Errorf("insert data: %s", err) return fmt.Errorf("insert data: %s", err)
} }
// results in:
// bbolt get /tmp/uploads.db uploads fb242922-86cb-43a8-92bc-b027c35f0586
// {"id":"fb242922-86cb-43a8-92bc-b027c35f0586","expire":"1d","file":"2023-02-17-13-09-data.zip"}
return nil return nil
}) })
if err != nil { if err != nil {
@@ -87,12 +83,12 @@ func (db *Db) Delete(apicontext string, id string) error {
return fmt.Errorf("id %s not found", id) return fmt.Errorf("id %s not found", id)
} }
upload := &common.Upload{} entryContext, err := common.GetContext(j)
if err := json.Unmarshal(j, &upload); err != nil { if err != nil {
return fmt.Errorf("unable to unmarshal json: %s", err) return fmt.Errorf("unable to unmarshal json: %s", err)
} }
if (apicontext != "" && (db.cfg.Super == apicontext || upload.Context == apicontext)) || apicontext == "" { if (apicontext != "" && (db.cfg.Super == apicontext || entryContext == apicontext)) || apicontext == "" {
return bucket.Delete([]byte(id)) return bucket.Delete([]byte(id))
} }
@@ -106,8 +102,8 @@ func (db *Db) Delete(apicontext string, id string) error {
return err return err
} }
func (db *Db) UploadsList(apicontext string, filter string) (*common.Uploads, error) { func (db *Db) UploadsList(apicontext string, filter string, t int) (*common.Response, error) {
uploads := &common.Uploads{} response := &common.Response{}
err := db.bolt.View(func(tx *bolt.Tx) error { err := db.bolt.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(Bucket)) bucket := tx.Bucket([]byte(Bucket))
@@ -116,24 +112,39 @@ func (db *Db) UploadsList(apicontext string, filter string) (*common.Uploads, er
} }
err := bucket.ForEach(func(id, j []byte) error { err := bucket.ForEach(func(id, j []byte) error {
upload := &common.Upload{} entry, err := common.Unmarshal(j, t)
if err := json.Unmarshal(j, &upload); err != nil { if err != nil {
return fmt.Errorf("unable to unmarshal json: %s", err) return fmt.Errorf("unable to unmarshal json: %s", err)
} }
var entryContext string
if t == common.TypeUpload {
entryContext = entry.(common.Upload).Context
} else {
entryContext = entry.(common.Form).Context
}
fmt.Printf("apicontext: %s, filter: %s\n", apicontext, filter) fmt.Printf("apicontext: %s, filter: %s\n", apicontext, filter)
if apicontext != "" && db.cfg.Super != apicontext { if apicontext != "" && db.cfg.Super != apicontext {
// only return the uploads for this context // only return the uploads for this context
if apicontext == upload.Context { if apicontext == entryContext {
// unless a filter needed OR no filter specified // unless a filter needed OR no filter specified
if (filter != "" && upload.Context == filter) || filter == "" { if (filter != "" && entryContext == filter) || filter == "" {
uploads.Entries = append(uploads.Entries, upload) if t == common.TypeUpload {
response.Uploads = append(response.Uploads, entry.(*common.Upload))
} else {
response.Forms = append(response.Forms, entry.(*common.Form))
}
} }
} }
} else { } else {
// return all, because we operate a public service or current==super // return all, because we operate a public service or current==super
if (filter != "" && upload.Context == filter) || filter == "" { if (filter != "" && entryContext == filter) || filter == "" {
uploads.Entries = append(uploads.Entries, upload) if t == common.TypeUpload {
response.Uploads = append(response.Uploads, entry.(*common.Upload))
} else {
response.Forms = append(response.Forms, entry.(*common.Form))
}
} }
} }
@@ -143,12 +154,12 @@ func (db *Db) UploadsList(apicontext string, filter string) (*common.Uploads, er
return err // might be nil as well return err // might be nil as well
}) })
return uploads, err return response, err
} }
// we only return one obj here, but could return more later // we only return one obj here, but could return more later
func (db *Db) Get(apicontext string, id string) (*common.Uploads, error) { func (db *Db) Get(apicontext string, id string, t int) (*common.Response, error) {
uploads := &common.Uploads{} response := &common.Response{}
err := db.bolt.View(func(tx *bolt.Tx) error { err := db.bolt.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(Bucket)) bucket := tx.Bucket([]byte(Bucket))
@@ -161,35 +172,46 @@ func (db *Db) Get(apicontext string, id string) (*common.Uploads, error) {
return fmt.Errorf("No upload object found with id %s", id) return fmt.Errorf("No upload object found with id %s", id)
} }
upload := &common.Upload{} entry, err := common.Unmarshal(j, t)
if err := json.Unmarshal(j, &upload); err != nil { if err != nil {
return fmt.Errorf("unable to unmarshal json: %s", err) return fmt.Errorf("unable to unmarshal json: %s", err)
} }
if (apicontext != "" && (db.cfg.Super == apicontext || upload.Context == apicontext)) || apicontext == "" { var entryContext string
if t == common.TypeUpload {
entryContext = entry.(common.Upload).Context
} else {
entryContext = entry.(common.Form).Context
}
if (apicontext != "" && (db.cfg.Super == apicontext || entryContext == apicontext)) || apicontext == "" {
// allowed if no context (public or download) // allowed if no context (public or download)
// or if context matches or if context==super // or if context matches or if context==super
uploads.Entries = append(uploads.Entries, upload) if t == common.TypeUpload {
response.Uploads = append(response.Uploads, entry.(*common.Upload))
} else {
response.Forms = append(response.Forms, entry.(*common.Form))
}
} }
return nil return nil
}) })
return uploads, err return response, err
} }
// a wrapper around Lookup() which extracts the 1st upload, if any // a wrapper around Lookup() which extracts the 1st upload, if any
func (db *Db) Lookup(apicontext string, id string) (*common.Upload, error) { func (db *Db) Lookup(apicontext string, id string, t int) (*common.Response, error) {
uploads, err := db.Get(apicontext, id) response, err := db.Get(apicontext, id, t)
if err != nil { if err != nil {
// non existent db entry with that id, or other db error, see logs // non existent db entry with that id, or other db error, see logs
return &common.Upload{}, fmt.Errorf("No upload object found with id %s", id) return &common.Response{}, fmt.Errorf("No upload object found with id %s", id)
} }
if len(uploads.Entries) == 0 { if len(response.Uploads) == 0 {
return &common.Upload{}, fmt.Errorf("No upload object found with id %s", id) return &common.Response{}, fmt.Errorf("No upload object found with id %s", id)
} }
return uploads.Entries[0], nil return response, nil
} }

234
api/form_handlers.go Normal file
View File

@@ -0,0 +1,234 @@
/*
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 api
import (
//"github.com/alecthomas/repr"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/tlinden/cenophane/cfg"
"github.com/tlinden/cenophane/common"
"strings"
"time"
)
func FormCreate(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
id := uuid.NewString()
var formdata common.Form
// init form obj
entry := &common.Form{Id: id, Created: common.Timestamp{Time: time.Now()}}
// 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())
}
entry.Context = apicontext
// extract auxilliary form data (expire field et al)
if err := c.BodyParser(&formdata); err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"bodyparser error : "+err.Error())
}
// post process expire
if len(formdata.Expire) == 0 {
entry.Expire = "asap"
} else {
ex, err := common.Untaint(formdata.Expire, cfg.RegDuration) // duration or asap allowed
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid data: "+err.Error())
}
entry.Expire = ex
}
// get url [and zip if there are multiple files]
returnUrl := strings.Join([]string{cfg.Url, "form", id}, "/")
entry.Url = returnUrl
Log("Now serving %s", returnUrl)
Log("Expire set to: %s", entry.Expire)
Log("Form created with API-Context %s", entry.Context)
// we do this in the background to not thwart the server
go db.Insert(id, entry)
// everything went well so far
res := &common.Response{Forms: []*common.Form{entry}}
res.Success = true
res.Code = fiber.StatusOK
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
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,
"Invalid id provided!")
}
if len(id) == 0 {
return JsonStatus(c, fiber.StatusForbidden,
"No id specified!")
}
// 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())
}
err = db.Delete(apicontext, id)
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!")
}
cleanup(filepath.Join(cfg.StorageDir, id))
return nil
}
// returns the whole list + error code, no post processing by server
func FormsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
// fetch filter from body(json expected)
setcontext := new(SetContext)
if err := c.BodyParser(setcontext); err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to parse body: "+err.Error())
}
filter, err := common.Untaint(setcontext.Apicontext, cfg.RegKey)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid api context filter 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())
}
// get list
uploads, err := db.FormsList(apicontext, filter)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to list uploads: "+err.Error())
}
// if we reached this point we can signal success
uploads.Success = true
uploads.Code = fiber.StatusOK
return c.Status(fiber.StatusOK).JSON(uploads)
}
// returns just one upload obj + error code, no post processing by server
func FormDescribe(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,
"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())
}
uploads, err := db.Get(apicontext, id)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"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
return c.Status(fiber.StatusOK).JSON(uploads)
}
*/

View File

@@ -80,6 +80,29 @@ func Runserver(conf *cfg.Config, args []string) error {
api.Get("/uploads/:id/file", auth, func(c *fiber.Ctx) error { api.Get("/uploads/:id/file", auth, func(c *fiber.Ctx) error {
return UploadFetch(c, conf, db) return UploadFetch(c, conf, db)
}) })
// same for forms ************
api.Post("/forms", auth, func(c *fiber.Ctx) 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)
})
// 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)
})
*/
} }
// public routes // public routes
@@ -95,6 +118,12 @@ func Runserver(conf *cfg.Config, args []string) error {
router.Get("/download/:id", func(c *fiber.Ctx) error { router.Get("/download/:id", func(c *fiber.Ctx) error {
return UploadFetch(c, conf, db, shallExpire) return UploadFetch(c, conf, db, shallExpire)
}) })
/*
router.Get("/form/:id", func(c *fiber.Ctx) error {
return FormFetch(c, conf, db, shallExpire)
})
*/
} }
// setup cleaner // setup cleaner

View File

@@ -116,7 +116,7 @@ func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
go db.Insert(id, entry) go db.Insert(id, entry)
// everything went well so far // everything went well so far
res := &common.Uploads{Entries: []*common.Upload{entry}} res := &common.Response{Uploads: []*common.Upload{entry}}
res.Success = true res.Success = true
res.Code = fiber.StatusOK res.Code = fiber.StatusOK
return c.Status(fiber.StatusOK).JSON(res) return c.Status(fiber.StatusOK).JSON(res)
@@ -139,12 +139,17 @@ func UploadFetch(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) err
"Unable to initialize session store from context: "+err.Error()) "Unable to initialize session store from context: "+err.Error())
} }
upload, err := db.Lookup(apicontext, id) response, err := db.Lookup(apicontext, id, common.TypeUpload)
if err != nil { if err != nil {
// non existent db entry with that id, or other db error, see logs // 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!") return fiber.NewError(404, "No download with that id could be found!")
} }
var upload *common.Upload
if len(response.Uploads) > 0 {
upload = response.Uploads[0]
}
file := upload.File file := upload.File
filename := filepath.Join(cfg.StorageDir, id, file) filename := filepath.Join(cfg.StorageDir, id, file)
@@ -228,7 +233,7 @@ func UploadsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
} }
// get list // get list
uploads, err := db.UploadsList(apicontext, filter) uploads, err := db.UploadsList(apicontext, filter, common.TypeUpload)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusForbidden, return JsonStatus(c, fiber.StatusForbidden,
"Unable to list uploads: "+err.Error()) "Unable to list uploads: "+err.Error())
@@ -256,19 +261,19 @@ func UploadDescribe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
"Unable to initialize session store from context: "+err.Error()) "Unable to initialize session store from context: "+err.Error())
} }
uploads, err := db.Get(apicontext, id) response, err := db.Get(apicontext, id, common.TypeUpload)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusForbidden, return JsonStatus(c, fiber.StatusForbidden,
"No upload with that id could be found!") "No upload with that id could be found!")
} }
for _, upload := range uploads.Entries { for _, upload := range response.Uploads {
upload.Url = strings.Join([]string{cfg.Url, "download", id, upload.File}, "/") upload.Url = strings.Join([]string{cfg.Url, "download", id, upload.File}, "/")
} }
// if we reached this point we can signal success // if we reached this point we can signal success
uploads.Success = true response.Success = true
uploads.Code = fiber.StatusOK response.Code = fiber.StatusOK
return c.Status(fiber.StatusOK).JSON(uploads) return c.Status(fiber.StatusOK).JSON(response)
} }

View File

@@ -17,6 +17,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package common package common
import (
"encoding/json"
"fmt"
)
// used to return to the api client // used to return to the api client
type Result struct { type Result struct {
Success bool `json:"success"` Success bool `json:"success"`
@@ -24,6 +29,12 @@ type Result struct {
Code int `json:"code"` Code int `json:"code"`
} }
// upload or form structs
type Dbentry interface {
Getcontext(j []byte) (string, error)
Marshal() ([]byte, error)
}
type Upload struct { type Upload struct {
Id string `json:"id"` Id string `json:"id"`
Expire string `json:"expire"` Expire string `json:"expire"`
@@ -35,9 +46,96 @@ type Upload struct {
} }
// this one is also used for marshalling to the client // this one is also used for marshalling to the client
type Uploads struct { type Response struct {
Entries []*Upload `json:"uploads"` Uploads []*Upload `json:"uploads"`
Forms []*Form `json:"forms"`
// integrate the Result struct so we can signal success // integrate the Result struct so we can signal success
Result Result
} }
type Form struct {
Id string `json:"id"`
Expire string `json:"expire"`
Description string `json:"description"`
Created Timestamp `json:"uploaded"`
Context string `json:"context"`
Url string `json:"url"`
}
const (
TypeUpload = iota
TypeForm
)
/*
implement Dbentry interface
*/
func (upload Upload) Getcontext(j []byte) (string, error) {
if err := json.Unmarshal(j, &upload); err != nil {
return "", fmt.Errorf("unable to unmarshal json: %s", err)
}
return upload.Context, nil
}
func (form Form) Getcontext(j []byte) (string, error) {
if err := json.Unmarshal(j, &form); err != nil {
return "", fmt.Errorf("unable to unmarshal json: %s", err)
}
return form.Context, nil
}
func (upload Upload) Marshal() ([]byte, error) {
jsonentry, err := json.Marshal(upload)
if err != nil {
return nil, fmt.Errorf("json marshalling failure: %s", err)
}
return jsonentry, nil
}
func (form Form) Marshal() ([]byte, error) {
jsonentry, err := json.Marshal(form)
if err != nil {
return nil, fmt.Errorf("json marshalling failure: %s", err)
}
return jsonentry, nil
}
/*
Extract context, whatever kind entry is, but we don't know in
advance, only after unmarshalling. So try Upload first, if that
fails, try Form.
*/
func GetContext(j []byte) (string, error) {
upload := &Upload{}
entryContext, err := upload.Getcontext(j)
if err != nil {
form := &Form{}
entryContext, err = form.Getcontext(j)
if err != nil {
return "", fmt.Errorf("unable to unmarshal json: %s", err)
}
}
return entryContext, nil
}
func Unmarshal(j []byte, t int) (Dbentry, error) {
if t == TypeUpload {
upload := &Upload{}
if err := json.Unmarshal(j, &upload); err != nil {
return upload, fmt.Errorf("unable to unmarshal json: %s", err)
}
return upload, nil
} else {
form := &Form{}
if err := json.Unmarshal(j, &form); err != nil {
return form, fmt.Errorf("unable to unmarshal json: %s", err)
}
return form, nil
}
}

View File

@@ -168,7 +168,7 @@ func HandleResponse(c *cfg.Config, resp *req.Response) error {
func UploadFiles(w io.Writer, c *cfg.Config, args []string) error { func UploadFiles(w io.Writer, c *cfg.Config, args []string) error {
// setup url, req.Request, timeout handling etc // setup url, req.Request, timeout handling etc
rq := Setup(c, "/uploads/") rq := Setup(c, "/uploads")
// collect files to upload from @argv // collect files to upload from @argv
if err := GatherFiles(rq, args); err != nil { if err := GatherFiles(rq, args); err != nil {