mirror of
https://codeberg.org/scip/ephemerup.git
synced 2025-12-16 20:20:58 +01:00
fixed error handling, added list command, switched to koanf
This commit is contained in:
@@ -13,6 +13,11 @@ Simple standalone file upload server with api and cli
|
||||
- use global map of api endpoints like /file/get/ etc
|
||||
- create cobra client commands (upload, list, delete, edit)
|
||||
|
||||
## BUGS
|
||||
|
||||
### upctl HTTP 413 weird behavior
|
||||
|
||||
- with -d reports correctly the 413, w/o it, it reports the timeout before.
|
||||
|
||||
## curl commands
|
||||
|
||||
|
||||
@@ -26,11 +26,17 @@ const Version string = "v0.0.1"
|
||||
var VERSION string // maintained by -x
|
||||
|
||||
type Config struct {
|
||||
// globals
|
||||
Endpoint string
|
||||
Debug bool
|
||||
Retries int
|
||||
Expire string
|
||||
Apikey string
|
||||
|
||||
// upload
|
||||
Expire string
|
||||
|
||||
// list
|
||||
Apicontext string
|
||||
}
|
||||
|
||||
func Getversion() string {
|
||||
|
||||
42
upctl/cmd/list.go
Normal file
42
upctl/cmd/list.go
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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 (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tlinden/up/upctl/cfg"
|
||||
"github.com/tlinden/up/upctl/lib"
|
||||
)
|
||||
|
||||
func ListCommand(conf *cfg.Config) *cobra.Command {
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list [options] [file ..]",
|
||||
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
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
return lib.List(conf, args)
|
||||
},
|
||||
}
|
||||
|
||||
// options
|
||||
listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context")
|
||||
|
||||
return listCmd
|
||||
}
|
||||
@@ -87,6 +87,7 @@ func Execute() {
|
||||
rootCmd.PersistentFlags().StringVarP(&conf.Apikey, "apikey", "a", "", "Api key to use")
|
||||
|
||||
rootCmd.AddCommand(UploadCommand(&conf))
|
||||
rootCmd.AddCommand(ListCommand(&conf))
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
|
||||
@@ -39,6 +39,10 @@ type Request struct {
|
||||
Url string
|
||||
}
|
||||
|
||||
type ListParams struct {
|
||||
Apicontext string `json:"apicontext"`
|
||||
}
|
||||
|
||||
func Setup(c *cfg.Config, path string) *Request {
|
||||
client := req.C()
|
||||
if c.Debug {
|
||||
@@ -59,7 +63,7 @@ func Setup(c *cfg.Config, path string) *Request {
|
||||
AddRetryHook(func(resp *req.Response, err error) {
|
||||
req := resp.Request.RawRequest
|
||||
if c.Debug {
|
||||
fmt.Println("Retrying endpoint request:", req.Method, req.URL)
|
||||
fmt.Println("Retrying endpoint request:", req.Method, req.URL, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -68,7 +72,7 @@ func Setup(c *cfg.Config, path string) *Request {
|
||||
client.SetCommonBearerAuthToken(c.Apikey)
|
||||
}
|
||||
|
||||
return &Request{Url: c.Endpoint + "/file/", R: R}
|
||||
return &Request{Url: c.Endpoint + path, R: R}
|
||||
|
||||
}
|
||||
|
||||
@@ -129,22 +133,55 @@ func Upload(c *cfg.Config, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return HandleResponse(c, resp)
|
||||
}
|
||||
|
||||
func HandleResponse(c *cfg.Config, resp *req.Response) error {
|
||||
// we expect a json response
|
||||
r := Response{}
|
||||
json.Unmarshal([]byte(resp.String()), &r)
|
||||
|
||||
if err := json.Unmarshal([]byte(resp.String()), &r); err != nil {
|
||||
// text output!
|
||||
r.Message = resp.String()
|
||||
}
|
||||
|
||||
if c.Debug {
|
||||
trace := resp.TraceInfo() // Use `resp.Request.TraceInfo()` to avoid unnecessary struct copy in production.
|
||||
fmt.Println(trace.Blame()) // Print out exactly where the http request is slowing down.
|
||||
trace := resp.Request.TraceInfo()
|
||||
fmt.Println(trace.Blame())
|
||||
fmt.Println("----------")
|
||||
fmt.Println(trace)
|
||||
}
|
||||
|
||||
if !r.Success {
|
||||
return errors.New(r.Message)
|
||||
if len(r.Message) == 0 {
|
||||
if resp.Err != nil {
|
||||
return resp.Err
|
||||
} else {
|
||||
return errors.New("Unknown error")
|
||||
}
|
||||
} else {
|
||||
return errors.New(r.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// all right
|
||||
fmt.Println(r.Message)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func List(c *cfg.Config, args []string) error {
|
||||
rq := Setup(c, "/list/")
|
||||
|
||||
params := &ListParams{Apicontext: c.Apicontext}
|
||||
resp, err := rq.R.
|
||||
SetBodyJsonMarshal(params).
|
||||
Get(rq.Url)
|
||||
|
||||
fmt.Println("")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return HandleResponse(c, resp)
|
||||
}
|
||||
|
||||
@@ -20,45 +20,88 @@ package api
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
//"github.com/gofiber/keyauth/v2"
|
||||
"github.com/gofiber/keyauth/v2"
|
||||
"github.com/tlinden/up/upd/cfg"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var Authurls []*regexp.Regexp
|
||||
var Apikeys []string
|
||||
// these vars can be savely global, since they don't change ever
|
||||
var (
|
||||
errMissing = &fiber.Error{
|
||||
Code: 403000,
|
||||
Message: "Missing API key",
|
||||
}
|
||||
|
||||
func AuthSetApikeys(keys []string) {
|
||||
errInvalid = &fiber.Error{
|
||||
Code: 403001,
|
||||
Message: "Invalid API key",
|
||||
}
|
||||
|
||||
Authurls []*regexp.Regexp
|
||||
Apikeys []cfg.Apicontext
|
||||
)
|
||||
|
||||
// fill from server: accepted keys
|
||||
func AuthSetApikeys(keys []cfg.Apicontext) {
|
||||
Apikeys = keys
|
||||
}
|
||||
|
||||
// fill from server: endpoints we need to authenticate
|
||||
func AuthSetEndpoints(prefix string, version string, endpoints []string) {
|
||||
for _, endpoint := range endpoints {
|
||||
Authurls = append(Authurls, regexp.MustCompile("^"+prefix+version+endpoint))
|
||||
}
|
||||
}
|
||||
|
||||
func validateAPIKey(c *fiber.Ctx, key string) (bool, error) {
|
||||
for _, apiKey := range Apikeys {
|
||||
hashedAPIKey := sha256.Sum256([]byte(apiKey))
|
||||
// make sure we always return JSON encoded errors
|
||||
func AuthErrHandler(ctx *fiber.Ctx, err error) error {
|
||||
ctx.Status(fiber.StatusForbidden)
|
||||
|
||||
if err == errMissing {
|
||||
return ctx.JSON(errMissing)
|
||||
}
|
||||
|
||||
return ctx.JSON(errInvalid)
|
||||
}
|
||||
|
||||
// validator hook, called by fiber via server keyauth.New()
|
||||
func AuthValidateAPIKey(c *fiber.Ctx, key string) (bool, error) {
|
||||
// create a new session, it will be thrown away if something fails
|
||||
sess, err := Sessionstore.Get(c)
|
||||
if err != nil {
|
||||
return false, errors.New("Unable to initialize session store from context!")
|
||||
}
|
||||
|
||||
// if Apikeys is empty, the server works unauthenticated
|
||||
// FIXME: maybe always reject?
|
||||
if len(Apikeys) == 0 {
|
||||
sess.Set("apicontext", "default")
|
||||
|
||||
if err := sess.Save(); err != nil {
|
||||
return false, errors.New("Unable to save session store!")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// actual key comparision
|
||||
for _, apicontext := range Apikeys {
|
||||
hashedAPIKey := sha256.Sum256([]byte(apicontext.Key))
|
||||
hashedKey := sha256.Sum256([]byte(key))
|
||||
|
||||
if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 {
|
||||
// apikey matches, register apicontext for later use by the handlers
|
||||
sess.Set("apicontext", apicontext.Context)
|
||||
|
||||
if err := sess.Save(); err != nil {
|
||||
return false, errors.New("Unable to save session store!")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
//return false, keyauth.ErrMissingOrMalformedAPIKey
|
||||
}
|
||||
|
||||
func authFilter(c *fiber.Ctx) bool {
|
||||
originalURL := strings.ToLower(c.OriginalURL())
|
||||
|
||||
for _, pattern := range Authurls {
|
||||
if pattern.MatchString(originalURL) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return false, keyauth.ErrMissingOrMalformedAPIKey
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/alecthomas/repr"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
@@ -37,6 +38,11 @@ type Upload struct {
|
||||
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) {
|
||||
@@ -125,22 +131,30 @@ func (db *Db) Delete(id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Db) Iterate(iterator func(id string, upload Upload)) error {
|
||||
var upload Upload
|
||||
func (db *Db) List(apicontext string) (*Uploads, error) {
|
||||
uploads := &Uploads{}
|
||||
|
||||
err := db.bolt.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(Bucket))
|
||||
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)
|
||||
}
|
||||
|
||||
iterator(string(id), upload)
|
||||
if apicontext != "" {
|
||||
if apicontext == upload.Context {
|
||||
uploads.Entries = append(uploads.Entries, upload)
|
||||
}
|
||||
} else {
|
||||
uploads.Entries = append(uploads.Entries, upload)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err // might be nil as well
|
||||
})
|
||||
|
||||
return err
|
||||
return uploads, err
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ 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/up/upd/cfg"
|
||||
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -89,8 +91,22 @@ func FilePut(c *fiber.Ctx, cfg *cfg.Config, db *Db) (string, error) {
|
||||
}
|
||||
entry.File = Newfilename
|
||||
|
||||
// retrieve the API Context name from the session, assuming is has
|
||||
// been successfully authenticated. However, if there are no api
|
||||
// contexts defined, we'll use 'default' (set in
|
||||
// auth.validateAPIKey())
|
||||
sess, err := Sessionstore.Get(c)
|
||||
if err != nil {
|
||||
return "", fiber.NewError(500, "Unable to initialize session store from context!")
|
||||
}
|
||||
apicontext := sess.Get("apicontext")
|
||||
if apicontext != nil {
|
||||
entry.Context = apicontext.(string)
|
||||
}
|
||||
|
||||
Log("Now serving %s from %s/%s", returnUrl, cfg.StorageDir, id)
|
||||
Log("Expire set to: %s", entry.Expire)
|
||||
Log("Uploaded with API-Context %s", entry.Context)
|
||||
|
||||
// we do this in the background to not thwart the server
|
||||
go db.Insert(id, entry)
|
||||
@@ -167,3 +183,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\-]`)
|
||||
if err != nil {
|
||||
return "", fiber.NewError(403, "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())
|
||||
}
|
||||
|
||||
jsonlist, err := json.Marshal(uploads)
|
||||
if err != nil {
|
||||
return "", fiber.NewError(500, "json marshalling failure: "+err.Error())
|
||||
}
|
||||
|
||||
Log(string(jsonlist))
|
||||
return string(jsonlist), nil
|
||||
}
|
||||
|
||||
@@ -18,14 +18,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
"github.com/gofiber/fiber/v2/middleware/session"
|
||||
"github.com/gofiber/keyauth/v2"
|
||||
"github.com/tlinden/up/upd/cfg"
|
||||
)
|
||||
|
||||
// sessions are context specific and can be global savely
|
||||
var Sessionstore *session.Store
|
||||
|
||||
func Runserver(cfg *cfg.Config, args []string) error {
|
||||
Sessionstore = session.New()
|
||||
|
||||
router := fiber.New(fiber.Config{
|
||||
CaseSensitive: true,
|
||||
StrictRouting: true,
|
||||
@@ -49,10 +56,11 @@ func Runserver(cfg *cfg.Config, args []string) error {
|
||||
defer db.Close()
|
||||
|
||||
AuthSetEndpoints(cfg.ApiPrefix, ApiVersion, []string{"/file"})
|
||||
AuthSetApikeys(cfg.Apikeys)
|
||||
AuthSetApikeys(cfg.Apicontext)
|
||||
|
||||
auth := keyauth.New(keyauth.Config{
|
||||
Validator: validateAPIKey,
|
||||
Validator: AuthValidateAPIKey,
|
||||
ErrorHandler: AuthErrHandler,
|
||||
})
|
||||
|
||||
shallExpire := true
|
||||
@@ -76,6 +84,11 @@ func Runserver(cfg *cfg.Config, args []string) error {
|
||||
api.Delete("/file/:id/", auth, func(c *fiber.Ctx) error {
|
||||
return FileDelete(c, cfg, db)
|
||||
})
|
||||
|
||||
api.Get("/list/", auth, func(c *fiber.Ctx) error {
|
||||
msg, err := List(c, cfg, db)
|
||||
return SendResponse(c, msg, err)
|
||||
})
|
||||
}
|
||||
|
||||
// public routes
|
||||
@@ -96,11 +109,16 @@ func Runserver(cfg *cfg.Config, args []string) error {
|
||||
}
|
||||
|
||||
func SendResponse(c *fiber.Ctx, msg string, err error) error {
|
||||
// FIXME:
|
||||
// respect fiber.NewError(500, "Could not process uploaded file[s]!")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(Result{
|
||||
Code: fiber.StatusBadRequest,
|
||||
code := fiber.StatusInternalServerError
|
||||
|
||||
var e *fiber.Error
|
||||
if errors.As(err, &e) {
|
||||
code = e.Code
|
||||
}
|
||||
|
||||
return c.Status(code).JSON(Result{
|
||||
Code: code,
|
||||
Message: err.Error(),
|
||||
Success: false,
|
||||
})
|
||||
|
||||
@@ -25,25 +25,31 @@ const Version string = "v0.0.1"
|
||||
|
||||
var VERSION string // maintained by -x
|
||||
|
||||
type Apicontext struct {
|
||||
Context string `koanf:"context"` // aka name or tenant
|
||||
Key string `koanf:"key"`
|
||||
}
|
||||
|
||||
// holds the whole configs, filled by commandline flags, env and config file
|
||||
type Config struct {
|
||||
ApiPrefix string
|
||||
Debug bool
|
||||
Listen string
|
||||
StorageDir string
|
||||
Url string
|
||||
DbFile string
|
||||
ApiPrefix string `koanf:"apiprefix"`
|
||||
Debug bool `koanf:"debug"`
|
||||
Listen string `koanf:"listen"`
|
||||
StorageDir string `koanf:"storagedir"`
|
||||
Url string `koanf:"url"`
|
||||
DbFile string `koanf:"dbfile"`
|
||||
|
||||
// fiber settings, see:
|
||||
// https://docs.gofiber.io/api/fiber/#config
|
||||
Prefork bool
|
||||
AppName string
|
||||
BodyLimit int
|
||||
V4only bool
|
||||
V6only bool
|
||||
Prefork bool `koanf:"prefork"`
|
||||
AppName string `koanf:"appname"`
|
||||
BodyLimit int `koanf:"bodylimit"`
|
||||
V4only bool `koanf:"ipv4"`
|
||||
V6only bool `koanf:"ipv6"`
|
||||
Network string
|
||||
|
||||
// only settable via config
|
||||
Apikeys []string
|
||||
Apicontext []Apicontext `koanf:"apicontext"`
|
||||
}
|
||||
|
||||
func Getversion() string {
|
||||
|
||||
186
upd/cmd/root.go
186
upd/cmd/root.go
@@ -19,12 +19,21 @@ package cmd
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/knadh/koanf/parsers/hcl"
|
||||
"github.com/knadh/koanf/providers/env"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/providers/posflag"
|
||||
"github.com/knadh/koanf/v2"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
"github.com/alecthomas/repr"
|
||||
"github.com/tlinden/up/upd/api"
|
||||
"github.com/tlinden/up/upd/cfg"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -32,126 +41,93 @@ var (
|
||||
cfgFile string
|
||||
)
|
||||
|
||||
func completion(cmd *cobra.Command, mode string) error {
|
||||
switch mode {
|
||||
case "bash":
|
||||
return cmd.Root().GenBashCompletion(os.Stdout)
|
||||
case "zsh":
|
||||
return cmd.Root().GenZshCompletion(os.Stdout)
|
||||
case "fish":
|
||||
return cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||
case "powershell":
|
||||
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
|
||||
default:
|
||||
return errors.New("Invalid shell parameter! Valid ones: bash|zsh|fish|powershell")
|
||||
}
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
func Execute() error {
|
||||
var (
|
||||
conf cfg.Config
|
||||
ShowVersion bool
|
||||
ShowCompletion string
|
||||
conf cfg.Config
|
||||
ShowVersion bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "upd [options]",
|
||||
Short: "upload daemon",
|
||||
Long: `Run an upload daemon reachable via REST API`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if ShowVersion {
|
||||
fmt.Println(cfg.Getversion())
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(ShowCompletion) > 0 {
|
||||
return completion(cmd, ShowCompletion)
|
||||
}
|
||||
|
||||
conf.ApplyDefaults()
|
||||
|
||||
// actual execution starts here
|
||||
return api.Runserver(&conf, args)
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return initConfig(cmd)
|
||||
},
|
||||
f := flag.NewFlagSet("config", flag.ContinueOnError)
|
||||
f.Usage = func() {
|
||||
fmt.Println(f.FlagUsages())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// options
|
||||
rootCmd.PersistentFlags().BoolVarP(&ShowVersion, "version", "v", false, "Print program version")
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "custom config file")
|
||||
rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging")
|
||||
rootCmd.PersistentFlags().StringVarP(&conf.Listen, "listen", "l", ":8080", "listen to custom ip:port (use [ip]:port for ipv6)")
|
||||
rootCmd.PersistentFlags().StringVarP(&conf.StorageDir, "storagedir", "s", "/tmp", "storage directory for uploaded files")
|
||||
rootCmd.PersistentFlags().StringVarP(&conf.ApiPrefix, "apiprefix", "a", "/api", "API endpoint path")
|
||||
rootCmd.PersistentFlags().StringVarP(&conf.Url, "url", "u", "", "HTTP endpoint w/o path")
|
||||
rootCmd.PersistentFlags().StringVarP(&conf.DbFile, "dbfile", "D", "/tmp/uploads.db", "Bold database file to use")
|
||||
f.BoolVarP(&ShowVersion, "version", "v", false, "Print program version")
|
||||
f.StringVarP(&cfgFile, "config", "c", "", "custom config file")
|
||||
f.BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging")
|
||||
f.StringVarP(&conf.Listen, "listen", "l", ":8080", "listen to custom ip:port (use [ip]:port for ipv6)")
|
||||
f.StringVarP(&conf.StorageDir, "storagedir", "s", "/tmp", "storage directory for uploaded files")
|
||||
f.StringVarP(&conf.ApiPrefix, "apiprefix", "a", "/api", "API endpoint path")
|
||||
f.StringVarP(&conf.Url, "url", "u", "", "HTTP endpoint w/o path")
|
||||
f.StringVarP(&conf.DbFile, "dbfile", "D", "/tmp/uploads.db", "Bold database file to use")
|
||||
|
||||
// server settings
|
||||
rootCmd.PersistentFlags().BoolVarP(&conf.V4only, "ipv4", "4", false, "Only listen on ipv4")
|
||||
rootCmd.PersistentFlags().BoolVarP(&conf.V6only, "ipv6", "6", false, "Only listen on ipv6")
|
||||
rootCmd.MarkFlagsMutuallyExclusive("ipv4", "ipv6")
|
||||
f.BoolVarP(&conf.V4only, "ipv4", "4", false, "Only listen on ipv4")
|
||||
f.BoolVarP(&conf.V6only, "ipv6", "6", false, "Only listen on ipv6")
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(&conf.Prefork, "prefork", "p", false, "Prefork server threads")
|
||||
rootCmd.PersistentFlags().StringVarP(&conf.AppName, "appname", "n", "upd "+conf.GetVersion(), "App name to say hi as")
|
||||
rootCmd.PersistentFlags().IntVarP(&conf.BodyLimit, "bodylimit", "b", 10250000000, "Max allowed upload size in bytes")
|
||||
rootCmd.PersistentFlags().StringSliceVarP(&conf.Apikeys, "apikey", "", []string{}, "Api key[s] to allow access")
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
f.BoolVarP(&conf.Prefork, "prefork", "p", false, "Prefork server threads")
|
||||
f.StringVarP(&conf.AppName, "appname", "n", "upd "+conf.GetVersion(), "App name to say hi as")
|
||||
f.IntVarP(&conf.BodyLimit, "bodylimit", "b", 10250000000, "Max allowed upload size in bytes")
|
||||
f.StringSliceP("apikeys", "", []string{}, "Api key[s] to allow access")
|
||||
|
||||
f.Parse(os.Args[1:])
|
||||
|
||||
// exclude -6 and -4
|
||||
if conf.V4only && conf.V6only {
|
||||
return errors.New("You cannot mix -4 and -6!")
|
||||
}
|
||||
}
|
||||
|
||||
// initialize viper, read config and ENV, bind flags
|
||||
func initConfig(cmd *cobra.Command) error {
|
||||
v := viper.New()
|
||||
viper.SetConfigType("hcl")
|
||||
// config provider
|
||||
var k = koanf.New(".")
|
||||
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
v.SetConfigFile(cfgFile)
|
||||
// Load the config files provided in the commandline or the default locations
|
||||
configfiles := []string{}
|
||||
configfile, _ := f.GetString("config")
|
||||
if configfile != "" {
|
||||
configfiles = []string{configfile}
|
||||
} else {
|
||||
v.SetConfigName("upd")
|
||||
|
||||
// default location[s]
|
||||
v.AddConfigPath(".")
|
||||
v.AddConfigPath("$HOME/.config/upd")
|
||||
v.AddConfigPath("/etc")
|
||||
v.AddConfigPath("/usr/local/etc")
|
||||
|
||||
configfiles = []string{
|
||||
"/etc/upd.hcl", "/usr/local/etc/upd.hcl", // unix variants
|
||||
filepath.Join(os.Getenv("HOME"), ".config", "upd", "upd.hcl"),
|
||||
filepath.Join(os.Getenv("HOME"), ".upd"),
|
||||
"upd.hcl",
|
||||
}
|
||||
}
|
||||
|
||||
// ignore read errors, report all others
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
return err
|
||||
for _, cfgfile := range configfiles {
|
||||
if _, err := os.Stat(cfgfile); err == nil {
|
||||
if err := k.Load(file.Provider(cfgfile), hcl.Parser(true)); err != nil {
|
||||
return errors.New("error loading config file: " + err.Error())
|
||||
}
|
||||
}
|
||||
fmt.Println(err)
|
||||
// else: we ignore the file if it doesn't exists
|
||||
}
|
||||
|
||||
fmt.Println("Using config file:", v.ConfigFileUsed())
|
||||
// env overrides config file
|
||||
k.Load(env.Provider("UPD_", ".", func(s string) string {
|
||||
return strings.Replace(strings.ToLower(
|
||||
strings.TrimPrefix(s, "UPD_")), "_", ".", -1)
|
||||
}), nil)
|
||||
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvPrefix("upd")
|
||||
// command line overrides env
|
||||
if err := k.Load(posflag.Provider(f, ".", k), nil); err != nil {
|
||||
return errors.New("error loading config: " + err.Error())
|
||||
}
|
||||
|
||||
// map flags to viper
|
||||
bindFlags(cmd, v)
|
||||
// fetch values
|
||||
k.Unmarshal("", &conf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// bind flags to viper settings (env+cfgfile)
|
||||
func bindFlags(cmd *cobra.Command, v *viper.Viper) {
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
// map flag name to config variable
|
||||
configName := f.Name
|
||||
|
||||
// use config variable if flag is not set and config is set
|
||||
if !f.Changed && v.IsSet(configName) {
|
||||
val := v.Get(configName)
|
||||
cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
|
||||
}
|
||||
})
|
||||
if conf.Debug {
|
||||
repr.Print(conf)
|
||||
}
|
||||
|
||||
switch {
|
||||
case ShowVersion:
|
||||
fmt.Println(cfg.Getversion())
|
||||
return nil
|
||||
default:
|
||||
conf.ApplyDefaults()
|
||||
return api.Runserver(&conf, flag.Args())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/repr v0.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
@@ -24,12 +25,20 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.15.9 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.1 // indirect
|
||||
github.com/knadh/koanf/parsers/hcl v0.1.0 // indirect
|
||||
github.com/knadh/koanf/providers/env v0.1.0 // indirect
|
||||
github.com/knadh/koanf/providers/file v0.1.0 // indirect
|
||||
github.com/knadh/koanf/providers/posflag v0.1.0 // indirect
|
||||
github.com/knadh/koanf/v2 v2.0.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
|
||||
18
upd/go.sum
18
upd/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/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -158,6 +160,18 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/parsers/hcl v0.1.0 h1:PuAAdRMXbxmhwzZftiQBEtWIKc3EbRHk/Fi+olo02z4=
|
||||
github.com/knadh/koanf/parsers/hcl v0.1.0/go.mod h1:7ClRvH1oP5ne8SfaDZZBK28/o9r4rek0PC4Vrc8qdvE=
|
||||
github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg=
|
||||
github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ=
|
||||
github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
|
||||
github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
|
||||
github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U=
|
||||
github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0=
|
||||
github.com/knadh/koanf/v2 v2.0.0 h1:XPQ5ilNnwnNaHrfQ1YpTVhUAjcGHnEKA+lRpipQv02Y=
|
||||
github.com/knadh/koanf/v2 v2.0.0/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@@ -179,8 +193,12 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
||||
@@ -19,8 +19,12 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/tlinden/up/upd/cmd"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
6
upd/nokeys.hcl
Normal file
6
upd/nokeys.hcl
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*-ruby-*-
|
||||
listen = ":8080"
|
||||
bodylimit = 10000
|
||||
|
||||
|
||||
|
||||
16
upd/upd.hcl
16
upd/upd.hcl
@@ -1,6 +1,16 @@
|
||||
# -*-ruby-*-
|
||||
listen = ":8080"
|
||||
apikeys = [
|
||||
"0fddbff5d8010f81cd28a7d77f3e38981b13d6164c2fd6e1c3f60a4287630c37",
|
||||
"970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a"
|
||||
bodylimit = 10000
|
||||
|
||||
apicontext = [
|
||||
{
|
||||
context = "default"
|
||||
key = "0fddbff5d8010f81cd28a7d77f3e38981b13d6164c2fd6e1c3f60a4287630c37",
|
||||
},
|
||||
{
|
||||
context = "foo",
|
||||
key = "970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user