Merge pull request #2 from TLINDEN/development

Features and Fixes
This commit is contained in:
T.v.Dein
2023-03-30 10:34:19 +02:00
committed by GitHub
28 changed files with 549 additions and 121 deletions

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: "[bug-report]"
labels: bug
assignees: TLINDEN
---
**Describtion**
<!-- Please provide a clear and concise description of the issue: -->
**Steps To Reproduce**
<!-- Please detail the steps to reproduce the behavior: -->
**Expected behavior**
<!-- What do you expected to happen instead? -->
**Version information**
<!--
Please provide as much version information as possible:
- if you have just installed a binary, provide the output of: tablizer --version
- if you installed from source, provide the output of: make show-version
- provide additional details: operating system and version and shell environment
-->
**Additional informations**

View File

@@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest a feature
title: "[feature-request]"
labels: feature-request
assignees: TLINDEN
---
**Describtion**
<!-- Please provide a clear and concise description of the feature you desire: -->
**Version information**
<!--
Just in case the feature is already present, please provide as
much version information as possible:
- if you have just installed a binary, provide the output of: tablizer --version
- if you installed from source, provide the output of: make show-version
- provide additional details: operating system and version and shell environment
-->

41
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: build-and-test-ephemerup
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
version: [1.18]
#os: [ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-latest]
name: Build
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.version }}
id: go
- name: checkout
uses: actions/checkout@v3
- name: build
run: make
- name: test ephemerup
run: make test
- name: test upctl
run: make -C upctl test
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.version }}
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3

View File

@@ -27,9 +27,15 @@ COMMIT = $(shell git rev-parse --short=8 HEAD)
BUILD = $(shell date +%Y.%m.%d.%H%M%S) BUILD = $(shell date +%Y.%m.%d.%H%M%S)
VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version)) VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version))
HAVE_POD := $(shell pod2text -h 2>/dev/null) HAVE_POD := $(shell pod2text -h 2>/dev/null)
HAVE_LINT:= $(shell golangci-lint -h 2>/dev/null)
DAEMON := ephemerupd DAEMON := ephemerupd
all: cmd/formtemplate.go buildlocal buildlocalctl all: cmd/formtemplate.go lint buildlocal buildlocalctl
lint:
ifdef HAVE_LINT
golangci-lint run
endif
buildlocalctl: buildlocalctl:
make -C upctl make -C upctl

View File

@@ -1,3 +1,7 @@
[![Actions](https://github.com/tlinden/ephemerup/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/ephemerup/actions)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/ephemerup/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/ephemerup)](https://goreportcard.com/report/github.com/tlinden/ephemerup)
# ephemerup # ephemerup
Simple standalone file upload server with expiration and commandline client. Simple standalone file upload server with expiration and commandline client.
@@ -232,6 +236,7 @@ Available Commands:
delete Delete an upload delete Delete an upload
describe Describe an upload. describe Describe an upload.
download Download a file. download Download a file.
form Form commands
help Help about any command help Help about any command
list List uploads list List uploads
upload Upload files upload Upload files
@@ -264,14 +269,10 @@ The `endpoint` is the **ephemerup** server running somewhere and the
## TODO ## TODO
- also serve a html upload page
- add metrics (as in https://github.com/ansrivas/fiberprometheus) - add metrics (as in https://github.com/ansrivas/fiberprometheus)
- do not manually generate output urls, use fiber.GetRoute() - do not manually generate output urls, use fiber.GetRoute()
- upd: https://docs.gofiber.io/guide/error-handling/ to always use json output - upd: https://docs.gofiber.io/guide/error-handling/ to always use json output
- upctl: get rid of HandleResponse(), used only once anyway - add (default by time!) sorting to list outputs, and add sort flag
- add form so that public users can upload
- use Writer for output.go so we can unit test the stuff in there
## BUGS ## BUGS

View File

@@ -71,6 +71,9 @@ func AuthValidateOnetimeKey(c *fiber.Ctx, key string, db *Db) (bool, error) {
} }
sess, err := Sessionstore.Get(c) sess, err := Sessionstore.Get(c)
if err != nil {
return false, errors.New("Could not retrieve session from Sessionstore: " + err.Error())
}
// store the result into the session, the 'formid' key tells the // store the result into the session, the 'formid' key tells the
// upload handler that the apicontext it sees is in fact a form id // upload handler that the apicontext it sees is in fact a form id

View File

@@ -58,10 +58,6 @@ func DeleteExpiredUploads(conf *cfg.Config, db *Db) error {
return err return err
}) })
if err != nil {
Log("DB error: %s", err.Error())
}
return err return err
} }
@@ -74,7 +70,9 @@ func BackgroundCleaner(conf *cfg.Config, db *Db) chan bool {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
DeleteExpiredUploads(conf, db) if err := DeleteExpiredUploads(conf, db); err != nil {
Log("Failed to delete eypired uploads: %s", err.Error())
}
case <-done: case <-done:
ticker.Stop() ticker.Stop()
return return

View File

@@ -23,6 +23,7 @@ import (
"github.com/tlinden/ephemerup/common" "github.com/tlinden/ephemerup/common"
//"github.com/alecthomas/repr" //"github.com/alecthomas/repr"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
"regexp"
) )
const Bucket string = "data" const Bucket string = "data"
@@ -102,8 +103,9 @@ func (db *Db) Delete(apicontext string, id string) error {
return err return err
} }
func (db *Db) List(apicontext string, filter string, t int) (*common.Response, error) { func (db *Db) List(apicontext string, filter string, query string, t int) (*common.Response, error) {
response := &common.Response{} response := &common.Response{}
qr := regexp.MustCompile(query)
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))
@@ -112,11 +114,17 @@ func (db *Db) List(apicontext string, filter string, t int) (*common.Response, e
} }
err := bucket.ForEach(func(id, j []byte) error { err := bucket.ForEach(func(id, j []byte) error {
allowed := false
entry, err := common.Unmarshal(j, t) entry, err := common.Unmarshal(j, t)
if err != nil { if err != nil {
return fmt.Errorf("unable to unmarshal json: %s", err) return fmt.Errorf("unable to unmarshal json: %s", err)
} }
if !entry.IsType(t) {
return nil
}
var entryContext string var entryContext string
if t == common.TypeUpload { if t == common.TypeUpload {
entryContext = entry.(*common.Upload).Context entryContext = entry.(*common.Upload).Context
@@ -124,22 +132,42 @@ func (db *Db) List(apicontext string, filter string, t int) (*common.Response, e
entryContext = entry.(*common.Form).Context entryContext = entry.(*common.Form).Context
} }
//fmt.Printf("apicontext: %s, filter: %s\n", apicontext, filter) // check if the user is allowed to list this entry
if apicontext != "" && db.cfg.Super != apicontext { if apicontext != "" && db.cfg.Super != apicontext {
// only return the uploads for this context // authenticated user but not member of super
// only return the uploads matching her context
if apicontext == entryContext { if apicontext == entryContext {
// unless a filter needed OR no filter specified // unless a filter OR no filter specified
if (filter != "" && entryContext == filter) || filter == "" { if (filter != "" && entryContext == filter) || filter == "" {
response.Append(entry) allowed = true
} }
} }
} 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 != "" && entryContext == filter) || filter == "" { if (filter != "" && entryContext == filter) || filter == "" {
response.Append(entry) allowed = true
} }
} }
if allowed {
// user is allowed to view this entry, check if she also wants to see it
if query != "" {
if entry.MatchDescription(qr) ||
entry.MatchExpire(qr) ||
entry.MatchCreated(qr) ||
entry.MatchFile(qr) {
allowed = true
} else {
allowed = false
}
}
}
if allowed {
// ok, legit and wanted
response.Append(entry)
}
return nil return nil
}) })

View File

@@ -72,24 +72,25 @@ var dbtests = []struct {
context string context string
ts string ts string
filter string filter string
query string
upload common.Upload upload common.Upload
form common.Form form common.Form
}{ }{
{ {
"upload", "test.db", false, "1", "foo", "upload", "test.db", false, "1", "foo",
"2023-03-10T11:45:00.000Z", "", "2023-03-10T11:45:00.000Z", "", "",
common.Upload{ common.Upload{
Id: "1", Expire: "asap", File: "none", Context: "foo", Id: "1", Expire: "asap", File: "none", Context: "foo",
Created: common.Timestamp{}}, Created: common.Timestamp{}, Type: common.TypeUpload},
common.Form{}, common.Form{},
}, },
{ {
"form", "test.db", false, "2", "foo", "form", "test.db", false, "2", "foo",
"2023-03-10T11:45:00.000Z", "", "2023-03-10T11:45:00.000Z", "", "",
common.Upload{}, common.Upload{},
common.Form{ common.Form{
Id: "1", Expire: "asap", Description: "none", Context: "foo", Id: "1", Expire: "asap", Description: "none", Context: "foo",
Created: common.Timestamp{}}, Created: common.Timestamp{}, Type: common.TypeForm},
}, },
} }
@@ -112,6 +113,10 @@ func TestDboperation(t *testing.T) {
if tt.upload.Id != "" { if tt.upload.Id != "" {
// set ts // set ts
ts, err := time.Parse(timeformat, tt.ts) ts, err := time.Parse(timeformat, tt.ts)
if err != nil {
t.Errorf("Could not parse time: " + err.Error())
}
tt.upload.Created = common.Timestamp{Time: ts} tt.upload.Created = common.Timestamp{Time: ts}
// create new upload db object // create new upload db object
@@ -145,7 +150,7 @@ func TestDboperation(t *testing.T) {
td.Cmp(t, response.Uploads[0], &tt.upload, tt.name) td.Cmp(t, response.Uploads[0], &tt.upload, tt.name)
// fetch list // fetch list
response, err = db.List(tt.context, tt.filter, common.TypeUpload) response, err = db.List(tt.context, tt.filter, tt.query, common.TypeUpload)
if err != nil { if err != nil {
t.Errorf("Could not fetch uploads list: " + err.Error()) t.Errorf("Could not fetch uploads list: " + err.Error())
} }
@@ -162,7 +167,7 @@ func TestDboperation(t *testing.T) {
} }
// fetch again, shall return empty // fetch again, shall return empty
response, err = db.Get(tt.context, tt.id, common.TypeUpload) _, err = db.Get(tt.context, tt.id, common.TypeUpload)
if err == nil { if err == nil {
t.Errorf("Could fetch upload object again although we deleted it") t.Errorf("Could fetch upload object again although we deleted it")
} }
@@ -171,6 +176,9 @@ func TestDboperation(t *testing.T) {
if tt.form.Id != "" { if tt.form.Id != "" {
// set ts // set ts
ts, err := time.Parse(timeformat, tt.ts) ts, err := time.Parse(timeformat, tt.ts)
if err != nil {
t.Errorf("Could not parse time: " + err.Error())
}
tt.form.Created = common.Timestamp{Time: ts} tt.form.Created = common.Timestamp{Time: ts}
// create new form db object // create new form db object
@@ -204,7 +212,7 @@ func TestDboperation(t *testing.T) {
td.Cmp(t, response.Forms[0], &tt.form, tt.name) td.Cmp(t, response.Forms[0], &tt.form, tt.name)
// fetch list // fetch list
response, err = db.List(tt.context, tt.filter, common.TypeForm) response, err = db.List(tt.context, tt.filter, tt.query, common.TypeForm)
if err != nil { if err != nil {
t.Errorf("Could not fetch forms list: " + err.Error()) t.Errorf("Could not fetch forms list: " + err.Error())
} }
@@ -221,7 +229,7 @@ func TestDboperation(t *testing.T) {
} }
// fetch again, shall return empty // fetch again, shall return empty
response, err = db.Get(tt.context, tt.id, common.TypeForm) _, err = db.Get(tt.context, tt.id, common.TypeForm)
if err == nil { if err == nil {
t.Errorf("Could fetch form object again although we deleted it") t.Errorf("Could fetch form object again although we deleted it")
} }

View File

@@ -114,17 +114,23 @@ func ZipDir(directory, zipfilename string) error {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
failure := make(chan string)
// don't chdir the server itself // don't chdir the server itself
go func() { go func() {
defer wg.Done() defer wg.Done()
os.Chdir(directory) if err := os.Chdir(directory); err != nil {
failure <- "Failed to changedir: " + err.Error()
return
}
newDir, err := os.Getwd() newDir, err := os.Getwd()
if err != nil { if err != nil {
failure <- "Failed to get cwd: " + err.Error()
} }
if newDir != directory { if newDir != directory {
err = errors.New("Failed to changedir!") failure <- "Failed to changedir!"
return
} }
err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error { err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
@@ -171,9 +177,21 @@ func ZipDir(directory, zipfilename string) error {
_, err = io.Copy(headerWriter, f) _, err = io.Copy(headerWriter, f)
return err return err
}) })
if err != nil {
failure <- "Failed to zip directory: " + err.Error()
} else {
close(failure)
}
}() }()
wg.Wait() wg.Wait()
goterr := <-failure
if goterr != "" {
return errors.New(goterr)
}
return err return err
} }

View File

@@ -36,7 +36,7 @@ func FormCreate(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
var formdata common.Form var formdata common.Form
// init form obj // init form obj
entry := &common.Form{Id: id, Created: common.Timestamp{Time: time.Now()}} entry := &common.Form{Id: id, Created: common.Timestamp{Time: time.Now()}, Type: common.TypeForm}
// retrieve the API Context name from the session // retrieve the API Context name from the session
apicontext, err := SessionGetApicontext(c) apicontext, err := SessionGetApicontext(c)
@@ -73,6 +73,15 @@ func FormCreate(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
entry.Notify = nt entry.Notify = nt
} }
if len(formdata.Description) != 0 {
des, err := common.Untaint(formdata.Description, cfg.RegText)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid description: "+err.Error())
}
entry.Description = des
}
// get url [and zip if there are multiple files] // get url [and zip if there are multiple files]
returnUrl := strings.Join([]string{cfg.Url, "form", id}, "/") returnUrl := strings.Join([]string{cfg.Url, "form", id}, "/")
entry.Url = returnUrl entry.Url = returnUrl
@@ -82,7 +91,11 @@ func FormCreate(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
Log("Form created with API-Context %s", entry.Context) Log("Form created with API-Context %s", entry.Context)
// we do this in the background to not thwart the server // we do this in the background to not thwart the server
go db.Insert(id, entry) go func() {
if err := db.Insert(id, entry); err != nil {
Log("Failed to insert: " + err.Error())
}
}()
// everything went well so far // everything went well so far
res := &common.Response{Forms: []*common.Form{entry}} res := &common.Response{Forms: []*common.Form{entry}}
@@ -136,6 +149,12 @@ func FormsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
"Invalid api context filter provided!") "Invalid api context filter provided!")
} }
query, err := common.Untaint(setcontext.Query, cfg.RegQuery)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid query provided!")
}
// retrieve the API Context name from the session // retrieve the API Context name from the session
apicontext, err := SessionGetApicontext(c) apicontext, err := SessionGetApicontext(c)
if err != nil { if err != nil {
@@ -144,7 +163,7 @@ func FormsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
} }
// get list // get list
response, err := db.List(apicontext, filter, common.TypeForm) response, err := db.List(apicontext, filter, query, common.TypeForm)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusForbidden, return JsonStatus(c, fiber.StatusForbidden,
"Unable to list forms: "+err.Error()) "Unable to list forms: "+err.Error())

View File

@@ -33,6 +33,7 @@ import (
type SetContext struct { type SetContext struct {
Apicontext string `json:"apicontext" form:"apicontext"` Apicontext string `json:"apicontext" form:"apicontext"`
Query string `json:"query" form:"query"`
} }
func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
@@ -53,7 +54,10 @@ func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
var returnUrl string var returnUrl string
var formdata Meta var formdata Meta
os.MkdirAll(filepath.Join(cfg.StorageDir, id), os.ModePerm) if err := os.MkdirAll(filepath.Join(cfg.StorageDir, id), os.ModePerm); err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize directories: "+err.Error())
}
// fetch auxiliary form data // fetch auxiliary form data
form, err := c.MultipartForm() form, err := c.MultipartForm()
@@ -63,7 +67,7 @@ func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
} }
// init upload obj // init upload obj
entry := &common.Upload{Id: id, Created: common.Timestamp{Time: time.Now()}} entry := &common.Upload{Id: id, Created: common.Timestamp{Time: time.Now()}, Type: common.TypeUpload}
// retrieve the API Context name from the session // retrieve the API Context name from the session
apicontext, err := SessionGetApicontext(c) apicontext, err := SessionGetApicontext(c)
@@ -114,7 +118,11 @@ func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
Log("Uploaded with API-Context %s", entry.Context) Log("Uploaded with API-Context %s", entry.Context)
// we do this in the background to not thwart the server // we do this in the background to not thwart the server
go db.Insert(id, entry) go func() {
if err := db.Insert(id, entry); err != nil {
Log("Failed to insert: " + err.Error())
}
}()
// everything went well so far // everything went well so far
res := &common.Response{Uploads: []*common.Upload{entry}} res := &common.Response{Uploads: []*common.Upload{entry}}
@@ -131,7 +139,9 @@ func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
if err == nil { if err == nil {
if len(r.Forms) == 1 { if len(r.Forms) == 1 {
if r.Forms[0].Expire == "asap" { if r.Forms[0].Expire == "asap" {
db.Delete(apicontext, formid) if err := db.Delete(apicontext, formid); err != nil {
Log("Failed to delete formid %s: %s", formid, err.Error())
}
} }
// email notification to form creator // email notification to form creator
@@ -184,7 +194,11 @@ func UploadFetch(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) err
if _, err := os.Stat(filename); err != nil { if _, err := os.Stat(filename); err != nil {
// db entry is there, but file isn't (anymore?) // db entry is there, but file isn't (anymore?)
go db.Delete(apicontext, id) go func() {
if err := db.Delete(apicontext, id); err != nil {
Log("Unable to delete entry id %s: %s", id, err.Error())
}
}()
return fiber.NewError(404, "No download with that id could be found!") return fiber.NewError(404, "No download with that id could be found!")
} }
@@ -192,12 +206,14 @@ func UploadFetch(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) err
err = c.Download(filename, file) err = c.Download(filename, file)
if len(shallExpire) > 0 { if len(shallExpire) > 0 {
if shallExpire[0] == true { if shallExpire[0] {
go func() { go func() {
// check if we need to delete the file now and do it in the background // check if we need to delete the file now and do it in the background
if upload.Expire == "asap" { if upload.Expire == "asap" {
cleanup(filepath.Join(cfg.StorageDir, id)) cleanup(filepath.Join(cfg.StorageDir, id))
db.Delete(apicontext, id) if err := db.Delete(apicontext, id); err != nil {
Log("Unable to delete entry id %s: %s", id, err.Error())
}
} }
}() }()
} }
@@ -241,17 +257,23 @@ func UploadDelete(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
// returns the whole list + error code, no post processing by server // returns the whole list + error code, no post processing by server
func UploadsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { func UploadsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
// fetch filter from body(json expected) // fetch apifilter+query from body(json expected)
setcontext := new(SetContext) setcontext := new(SetContext)
if err := c.BodyParser(setcontext); err != nil { if err := c.BodyParser(setcontext); err != nil {
return JsonStatus(c, fiber.StatusForbidden, return JsonStatus(c, fiber.StatusForbidden,
"Unable to parse body: "+err.Error()) "Unable to parse body: "+err.Error())
} }
filter, err := common.Untaint(setcontext.Apicontext, cfg.RegKey) apifilter, err := common.Untaint(setcontext.Apicontext, cfg.RegKey)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusForbidden, return JsonStatus(c, fiber.StatusForbidden,
"Invalid api context filter provided!") "Invalid api context apifilter provided!")
}
query, err := common.Untaint(setcontext.Query, cfg.RegQuery)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid query provided!")
} }
// retrieve the API Context name from the session // retrieve the API Context name from the session
@@ -262,7 +284,7 @@ func UploadsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
} }
// get list // get list
uploads, err := db.List(apicontext, filter, common.TypeUpload) uploads, err := db.List(apicontext, apifilter, query, 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())

View File

@@ -62,16 +62,19 @@ type Config struct {
Network string Network string
// only settable via config // only settable via config
Apicontexts []Apicontext `koanf:"apicontext"` Apicontexts []Apicontext `koanf:"apicontexts"`
// smtp settings // smtp settings
Mail Mailsettings `koanf:mail` Mail Mailsettings `koanf:"mail"`
// Internals only // Internals only
RegNormalizedFilename *regexp.Regexp RegNormalizedFilename *regexp.Regexp
RegDuration *regexp.Regexp RegDuration *regexp.Regexp
RegKey *regexp.Regexp RegKey *regexp.Regexp
RegEmail *regexp.Regexp RegEmail *regexp.Regexp
RegText *regexp.Regexp
RegQuery *regexp.Regexp
CleanInterval time.Duration CleanInterval time.Duration
DefaultExpire int DefaultExpire int
} }
@@ -118,8 +121,9 @@ func (c *Config) ApplyDefaults() {
c.RegNormalizedFilename = regexp.MustCompile(`[^\w\d\-_\.]`) c.RegNormalizedFilename = regexp.MustCompile(`[^\w\d\-_\.]`)
c.RegDuration = regexp.MustCompile(`[^dhms0-9]`) c.RegDuration = regexp.MustCompile(`[^dhms0-9]`)
c.RegKey = regexp.MustCompile(`[^a-zA-Z0-9\-]`) c.RegKey = regexp.MustCompile(`[^a-zA-Z0-9\-]`)
c.RegEmail = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) c.RegEmail = regexp.MustCompile(`[^a-zA-Z0-9._%+\-@0-9]`)
c.RegEmail = regexp.MustCompile(`[^a-z0-9._%+\-@0-9]`) c.RegText = regexp.MustCompile(`[^a-zA-Z0-9_%+\-@0-9 #/\.]`)
c.RegQuery = regexp.MustCompile(`[^a-zA-Z0-9_%+\-@0-9 #/\.\*\[\]\(\)\\]`)
c.CleanInterval = 10 * time.Second c.CleanInterval = 10 * time.Second
c.DefaultExpire = 30 * 86400 // 1 month c.DefaultExpire = 30 * 86400 // 1 month

View File

@@ -30,6 +30,10 @@ const formtemplate = `
Use this form to upload one or more files. The creator of the form will automatically get notified. Use this form to upload one or more files. The creator of the form will automatically get notified.
</p> </p>
</div> </div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">Description</label>
<label class="col-sm-10 col-form-label">{{ .Description}} </label>
</div>
<div class="mb-3 row"> <div class="mb-3 row">
<label for="file" class="col-sm-2 col-form-label">Select</label> <label for="file" class="col-sm-2 col-form-label">Select</label>
<div class="col-sm-10"> <div class="col-sm-10">
@@ -74,8 +78,8 @@ const formtemplate = `
$('.statusMsg').html(''); $('.statusMsg').html('');
if(response.success){ if(response.success){
$('#UploadForm')[0].reset(); $('#UploadForm')[0].reset();
$('.statusMsg').html('<p class="alert alert-success">Your upload is available at <code>' $('.statusMsg').html('<p class="alert alert-success">Your upload is available for download.<!-- '
+response.uploads[0].url+'</code> for download</p>'); +response.uploads[0].url+' -->');
$('#UploadForm').hide(); $('#UploadForm').hide();
}else{ }else{
$('.statusMsg').html('<p class="alert alert-danger">'+response.message+'</p>'); $('.statusMsg').html('<p class="alert alert-danger">'+response.message+'</p>');

View File

@@ -75,7 +75,9 @@ func Execute() error {
f.StringVarP(&conf.AppName, "appname", "n", "ephemerupd "+conf.GetVersion(), "App name to say hi as") f.StringVarP(&conf.AppName, "appname", "n", "ephemerupd "+conf.GetVersion(), "App name to say hi as")
f.IntVarP(&conf.BodyLimit, "bodylimit", "b", 10250000000, "Max allowed upload size in bytes") f.IntVarP(&conf.BodyLimit, "bodylimit", "b", 10250000000, "Max allowed upload size in bytes")
f.Parse(os.Args[1:]) if err := f.Parse(os.Args[1:]); err != nil {
return err
}
// exclude -6 and -4 // exclude -6 and -4
if conf.V4only && conf.V6only { if conf.V4only && conf.V6only {
@@ -86,19 +88,19 @@ func Execute() error {
var k = koanf.New(".") var k = koanf.New(".")
// Load the config files provided in the commandline or the default locations // Load the config files provided in the commandline or the default locations
configfiles := []string{} var configfiles []string
configfile, _ := f.GetString("config") configfile, _ := f.GetString("config")
if configfile != "" { if configfile != "" {
configfiles = []string{configfile} configfiles = []string{configfile}
} else { } else {
configfiles = []string{ configfiles = []string{
"/etc/ephemerupd.hcl", "/usr/local/etc/ephemerupd.hcl", // unix variants "/etc/ephemerup.hcl", "/usr/local/etc/ephemerup.hcl", // unix variants
filepath.Join(os.Getenv("HOME"), ".config", "ephemerupd", "ephemerupd.hcl"), filepath.Join(os.Getenv("HOME"), ".config", "ephemerup", "ephemerup.hcl"),
filepath.Join(os.Getenv("HOME"), ".ephemerupd"), filepath.Join(os.Getenv("HOME"), ".ephemerup"),
"ephemerupd.hcl", "ephemerup.hcl",
} }
} }
repr.Print(configfiles)
for _, cfgfile := range configfiles { for _, cfgfile := range configfiles {
if _, err := os.Stat(cfgfile); err == nil { if _, err := os.Stat(cfgfile); err == nil {
if err := k.Load(file.Provider(cfgfile), hcl.Parser(true)); err != nil { if err := k.Load(file.Provider(cfgfile), hcl.Parser(true)); err != nil {
@@ -109,10 +111,12 @@ func Execute() error {
} }
// env overrides config file // env overrides config file
k.Load(env.Provider("EPHEMERUPD_", ".", func(s string) string { if err := k.Load(env.Provider("EPHEMERUPD_", ".", func(s string) string {
return strings.Replace(strings.ToLower( return strings.Replace(strings.ToLower(
strings.TrimPrefix(s, "EPHEMERUPD_")), "_", ".", -1) strings.TrimPrefix(s, "EPHEMERUPD_")), "_", ".", -1)
}), nil) }), nil); err != nil {
return errors.New("error loading environment: " + err.Error())
}
// command line overrides env // command line overrides env
if err := k.Load(posflag.Provider(f, ".", k), nil); err != nil { if err := k.Load(posflag.Provider(f, ".", k), nil); err != nil {
@@ -120,7 +124,9 @@ func Execute() error {
} }
// fetch values // fetch values
k.Unmarshal("", &conf) if err := k.Unmarshal("", &conf); err != nil {
return errors.New("error unmarshalling: " + err.Error())
}
// there may exist some api context variables // there may exist some api context variables
GetApicontextsFromEnv(&conf) GetApicontextsFromEnv(&conf)
@@ -197,9 +203,7 @@ func GetApicontextsFromEnv(conf *cfg.Config) {
} }
} }
for _, ap := range conf.Apicontexts { contexts = append(contexts, conf.Apicontexts...)
contexts = append(contexts, ap)
}
conf.Apicontexts = contexts conf.Apicontexts = contexts
} }

View File

@@ -20,6 +20,7 @@ package common
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"regexp"
) )
// used to return to the api client // used to return to the api client
@@ -33,15 +34,22 @@ type Result struct {
type Dbentry interface { type Dbentry interface {
Getcontext(j []byte) (string, error) Getcontext(j []byte) (string, error)
Marshal() ([]byte, error) Marshal() ([]byte, error)
MatchExpire(r *regexp.Regexp) bool
MatchDescription(r *regexp.Regexp) bool
MatchFile(r *regexp.Regexp) bool
MatchCreated(r *regexp.Regexp) bool
IsType(t int) bool
} }
type Upload struct { type Upload struct {
Type int `json:"type"`
Id string `json:"id"` Id string `json:"id"`
Expire string `json:"expire"` Expire string `json:"expire"`
File string `json:"file"` // final filename (visible to the downloader) File string `json:"file"` // final filename (visible to the downloader)
Members []string `json:"members"` // contains multiple files, so File is an archive Members []string `json:"members"` // contains multiple files, so File is an archive
Created Timestamp `json:"uploaded"` Created Timestamp `json:"uploaded"`
Context string `json:"context"` Context string `json:"context"`
Description string `json:"description"`
Url string `json:"url"` Url string `json:"url"`
} }
@@ -60,6 +68,7 @@ type Form struct {
// that the upload handler is able to check if the form object has // that the upload handler is able to check if the form object has
// to be deleted immediately (if its expire field has been set to // to be deleted immediately (if its expire field has been set to
// asap) // asap)
Type int `json:"type"`
Id string `json:"id"` Id string `json:"id"`
Expire string `json:"expire"` Expire string `json:"expire"`
Description string `json:"description"` Description string `json:"description"`
@@ -111,6 +120,52 @@ func (form Form) Marshal() ([]byte, error) {
return jsonentry, nil return jsonentry, nil
} }
func (upload Upload) MatchExpire(r *regexp.Regexp) bool {
return r.MatchString(upload.Expire)
}
func (upload Upload) MatchDescription(r *regexp.Regexp) bool {
return r.MatchString(upload.Description)
}
func (upload Upload) MatchCreated(r *regexp.Regexp) bool {
return r.MatchString(upload.Created.Time.String())
}
func (upload Upload) MatchFile(r *regexp.Regexp) bool {
return r.MatchString(upload.File)
}
func (form Form) MatchExpire(r *regexp.Regexp) bool {
return r.MatchString(form.Expire)
}
func (form Form) MatchDescription(r *regexp.Regexp) bool {
return r.MatchString(form.Description)
}
func (form Form) MatchCreated(r *regexp.Regexp) bool {
return r.MatchString(form.Created.Time.String())
}
func (form Form) MatchFile(r *regexp.Regexp) bool {
return false
}
func (upload Upload) IsType(t int) bool {
if upload.Type == t {
return true
}
return false
}
func (form Form) IsType(t int) bool {
if form.Type == t {
return true
}
return false
}
/* /*
Response methods Response methods
*/ */

View File

@@ -2,7 +2,7 @@
listen = ":8080" listen = ":8080"
bodylimit = 10000 bodylimit = 10000
apicontext = [ apicontexts = [
{ {
context = "root" context = "root"
key = "0fddbff5d8010f81cd28a7d77f3e38981b13d6164c2fd6e1c3f60a4287630c37", key = "0fddbff5d8010f81cd28a7d77f3e38981b13d6164c2fd6e1c3f60a4287630c37",

View File

@@ -27,6 +27,10 @@
Use this form to upload one or more files. The creator of the form will automatically get notified. Use this form to upload one or more files. The creator of the form will automatically get notified.
</p> </p>
</div> </div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">Description</label>
<label class="col-sm-10 col-form-label">{{ .Description}} </label>
</div>
<div class="mb-3 row"> <div class="mb-3 row">
<label for="file" class="col-sm-2 col-form-label">Select</label> <label for="file" class="col-sm-2 col-form-label">Select</label>
<div class="col-sm-10"> <div class="col-sm-10">
@@ -71,8 +75,8 @@
$('.statusMsg').html(''); $('.statusMsg').html('');
if(response.success){ if(response.success){
$('#UploadForm')[0].reset(); $('#UploadForm')[0].reset();
$('.statusMsg').html('<p class="alert alert-success">Your upload is available at <code>' $('.statusMsg').html('<p class="alert alert-success">Your upload is available for download.<!-- '
+response.uploads[0].url+'</code> for download</p>'); +response.uploads[0].url+' -->');
$('#UploadForm').hide(); $('#UploadForm').hide();
}else{ }else{
$('.statusMsg').html('<p class="alert alert-danger">'+response.message+'</p>'); $('.statusMsg').html('<p class="alert alert-danger">'+response.message+'</p>');

View File

@@ -28,9 +28,14 @@ COMMIT = $(shell git rev-parse --short=8 HEAD)
BUILD = $(shell date +%Y.%m.%d.%H%M%S) BUILD = $(shell date +%Y.%m.%d.%H%M%S)
VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version)) VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version))
HAVE_POD := $(shell pod2text -h 2>/dev/null) HAVE_POD := $(shell pod2text -h 2>/dev/null)
HAVE_LINT:= $(shell golangci-lint -h 2>/dev/null)
all: buildlocal all: lint buildlocal
lint:
ifdef HAVE_LINT
golangci-lint run
endif
buildlocal: buildlocal:
go build -ldflags "-X 'github.com/tlinden/ephemerup/upctl/cfg.VERSION=$(VERSION)'" go build -ldflags "-X 'github.com/tlinden/ephemerup/upctl/cfg.VERSION=$(VERSION)'"

View File

@@ -44,6 +44,9 @@ type Config struct {
// required to intercept requests using httpmock in tests // required to intercept requests using httpmock in tests
Mock bool Mock bool
// used to filter lists
Query string
// required for forms // required for forms
Description string Description string
Notify string Notify string

View File

@@ -18,7 +18,9 @@ package cmd
import ( import (
//"errors" //"errors"
"errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/tlinden/ephemerup/common"
"github.com/tlinden/ephemerup/upctl/cfg" "github.com/tlinden/ephemerup/upctl/cfg"
"github.com/tlinden/ephemerup/upctl/lib" "github.com/tlinden/ephemerup/upctl/lib"
"os" "os"
@@ -33,8 +35,7 @@ func FormCommand(conf *cfg.Config) *cobra.Command {
// errors at this stage do not cause the usage to be shown // errors at this stage do not cause the usage to be shown
//cmd.SilenceUsage = true //cmd.SilenceUsage = true
if len(args) == 0 { if len(args) == 0 {
cmd.Help() return cmd.Help()
os.Exit(0)
} }
return nil return nil
}, },
@@ -44,6 +45,9 @@ func FormCommand(conf *cfg.Config) *cobra.Command {
formCmd.Aliases = append(formCmd.Aliases, "f") formCmd.Aliases = append(formCmd.Aliases, "f")
formCmd.AddCommand(FormCreateCommand(conf)) formCmd.AddCommand(FormCreateCommand(conf))
formCmd.AddCommand(FormListCommand(conf))
formCmd.AddCommand(FormDeleteCommand(conf))
formCmd.AddCommand(FormDescribeCommand(conf))
return formCmd return formCmd
} }
@@ -71,3 +75,73 @@ func FormCreateCommand(conf *cfg.Config) *cobra.Command {
return formCreateCmd return formCreateCmd
} }
func FormListCommand(conf *cfg.Config) *cobra.Command {
var listCmd = &cobra.Command{
Use: "list [options]",
Short: "List formss",
Long: `List formss.`,
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(os.Stdout, conf, nil, common.TypeForm)
},
}
// options
listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context")
listCmd.PersistentFlags().StringVarP(&conf.Query, "query", "q", "", "Filter by given query regexp")
listCmd.Aliases = append(listCmd.Aliases, "ls")
listCmd.Aliases = append(listCmd.Aliases, "l")
return listCmd
}
func FormDeleteCommand(conf *cfg.Config) *cobra.Command {
var deleteCmd = &cobra.Command{
Use: "delete [options] <id>",
Short: "Delete an form",
Long: `Delete an form 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(os.Stdout, conf, args, common.TypeForm)
},
}
deleteCmd.Aliases = append(deleteCmd.Aliases, "rm")
deleteCmd.Aliases = append(deleteCmd.Aliases, "d")
return deleteCmd
}
func FormDescribeCommand(conf *cfg.Config) *cobra.Command {
var listCmd = &cobra.Command{
Use: "describe [options] form-id",
Long: "Show detailed informations about an form object.",
Short: `Describe an form.`,
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(os.Stdout, conf, args, common.TypeForm)
},
}
listCmd.Aliases = append(listCmd.Aliases, "des")
listCmd.Aliases = append(listCmd.Aliases, "info")
listCmd.Aliases = append(listCmd.Aliases, "i")
return listCmd
}

View File

@@ -19,6 +19,7 @@ package cmd
import ( import (
"errors" "errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/tlinden/ephemerup/common"
"github.com/tlinden/ephemerup/upctl/cfg" "github.com/tlinden/ephemerup/upctl/cfg"
"github.com/tlinden/ephemerup/upctl/lib" "github.com/tlinden/ephemerup/upctl/lib"
"os" "os"
@@ -43,6 +44,7 @@ func UploadCommand(conf *cfg.Config) *cobra.Command {
// options // options
uploadCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)") uploadCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)")
uploadCmd.PersistentFlags().StringVarP(&conf.Description, "description", "D", "", "Description of the form")
uploadCmd.Aliases = append(uploadCmd.Aliases, "up") uploadCmd.Aliases = append(uploadCmd.Aliases, "up")
uploadCmd.Aliases = append(uploadCmd.Aliases, "u") uploadCmd.Aliases = append(uploadCmd.Aliases, "u")
@@ -54,17 +56,18 @@ func ListCommand(conf *cfg.Config) *cobra.Command {
var listCmd = &cobra.Command{ var listCmd = &cobra.Command{
Use: "list [options] [file ..]", Use: "list [options] [file ..]",
Short: "List uploads", Short: "List uploads",
Long: `List uploads.`, Long: `List uploads`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
// errors at this stage do not cause the usage to be shown // errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true cmd.SilenceUsage = true
return lib.List(os.Stdout, conf, args) return lib.List(os.Stdout, conf, args, common.TypeUpload)
}, },
} }
// options // options
listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context") listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context")
listCmd.PersistentFlags().StringVarP(&conf.Query, "query", "q", "", "Filter by given query regexp")
listCmd.Aliases = append(listCmd.Aliases, "ls") listCmd.Aliases = append(listCmd.Aliases, "ls")
listCmd.Aliases = append(listCmd.Aliases, "l") listCmd.Aliases = append(listCmd.Aliases, "l")
@@ -85,7 +88,7 @@ func DeleteCommand(conf *cfg.Config) *cobra.Command {
// errors at this stage do not cause the usage to be shown // errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true cmd.SilenceUsage = true
return lib.Delete(os.Stdout, conf, args) return lib.Delete(os.Stdout, conf, args, common.TypeUpload)
}, },
} }
@@ -99,7 +102,7 @@ func DescribeCommand(conf *cfg.Config) *cobra.Command {
var listCmd = &cobra.Command{ var listCmd = &cobra.Command{
Use: "describe [options] upload-id", Use: "describe [options] upload-id",
Long: "Show detailed informations about an upload object.", Long: "Show detailed informations about an upload object.",
Short: `Describe an upload.`, Short: `Describe an upload`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return errors.New("No id specified to delete!") return errors.New("No id specified to delete!")
@@ -108,7 +111,7 @@ func DescribeCommand(conf *cfg.Config) *cobra.Command {
// errors at this stage do not cause the usage to be shown // errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true cmd.SilenceUsage = true
return lib.Describe(os.Stdout, conf, args) return lib.Describe(os.Stdout, conf, args, common.TypeUpload)
}, },
} }
@@ -123,7 +126,7 @@ func DownloadCommand(conf *cfg.Config) *cobra.Command {
var listCmd = &cobra.Command{ var listCmd = &cobra.Command{
Use: "download [options] upload-id", Use: "download [options] upload-id",
Long: "Download the file associated with an upload object.", Long: "Download the file associated with an upload object.",
Short: `Download a file.`, Short: `Download a file`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return errors.New("No id specified to delete!") return errors.New("No id specified to delete!")

View File

@@ -130,13 +130,12 @@ func initConfig(cmd *cobra.Command, cfg *cfg.Config) error {
v.SetEnvPrefix("upctl") v.SetEnvPrefix("upctl")
// map flags to viper // map flags to viper
bindFlags(cmd, v) return bindFlags(cmd, v)
return nil
} }
// bind flags to viper settings (env+cfgfile) // bind flags to viper settings (env+cfgfile)
func bindFlags(cmd *cobra.Command, v *viper.Viper) { func bindFlags(cmd *cobra.Command, v *viper.Viper) error {
var fail error
cmd.Flags().VisitAll(func(f *pflag.Flag) { cmd.Flags().VisitAll(func(f *pflag.Flag) {
// map flag name to config variable // map flag name to config variable
configName := f.Name configName := f.Name
@@ -144,7 +143,11 @@ func bindFlags(cmd *cobra.Command, v *viper.Viper) {
// use config variable if flag is not set and config is set // use config variable if flag is not set and config is set
if !f.Changed && v.IsSet(configName) { if !f.Changed && v.IsSet(configName) {
val := v.Get(configName) val := v.Get(configName)
cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)) if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
fail = err
}
} }
}) })
return fail
} }

View File

@@ -48,6 +48,7 @@ type Request struct {
type ListParams struct { type ListParams struct {
Apicontext string `json:"apicontext"` Apicontext string `json:"apicontext"`
Query string `json:"query"`
} }
const Maxwidth = 12 const Maxwidth = 12
@@ -181,7 +182,9 @@ func UploadFiles(w io.Writer, c *cfg.Config, args []string) error {
var left float64 var left float64
rq.R.SetUploadCallbackWithInterval(func(info req.UploadInfo) { rq.R.SetUploadCallbackWithInterval(func(info req.UploadInfo) {
left = float64(info.UploadedSize) / float64(info.FileSize) * 100.0 left = float64(info.UploadedSize) / float64(info.FileSize) * 100.0
bar.Add(int(left)) if err := bar.Add(int(left)); err != nil {
fmt.Print("\r")
}
}, 10*time.Millisecond) }, 10*time.Millisecond)
} }
@@ -189,6 +192,7 @@ func UploadFiles(w io.Writer, c *cfg.Config, args []string) error {
resp, err := rq.R. resp, err := rq.R.
SetFormData(map[string]string{ SetFormData(map[string]string{
"expire": c.Expire, "expire": c.Expire,
"description": c.Description,
}). }).
Post(rq.Url) Post(rq.Url)
@@ -203,10 +207,17 @@ func UploadFiles(w io.Writer, c *cfg.Config, args []string) error {
return RespondExtended(w, resp) return RespondExtended(w, resp)
} }
func List(w io.Writer, c *cfg.Config, args []string) error { func List(w io.Writer, c *cfg.Config, args []string, typ int) error {
rq := Setup(c, "/uploads") var rq *Request
params := &ListParams{Apicontext: c.Apicontext} switch typ {
case common.TypeUpload:
rq = Setup(c, "/uploads")
case common.TypeForm:
rq = Setup(c, "/forms")
}
params := &ListParams{Apicontext: c.Apicontext, Query: c.Query}
resp, err := rq.R. resp, err := rq.R.
SetBodyJsonMarshal(params). SetBodyJsonMarshal(params).
Get(rq.Url) Get(rq.Url)
@@ -219,12 +230,28 @@ func List(w io.Writer, c *cfg.Config, args []string) error {
return err return err
} }
switch typ {
case common.TypeUpload:
return UploadsRespondTable(w, resp) return UploadsRespondTable(w, resp)
case common.TypeForm:
return FormsRespondTable(w, resp)
}
return nil
} }
func Delete(w io.Writer, c *cfg.Config, args []string) error { func Delete(w io.Writer, c *cfg.Config, args []string, typ int) error {
for _, id := range args { for _, id := range args {
rq := Setup(c, "/uploads/"+id+"/") var rq *Request
caption := "Upload"
switch typ {
case common.TypeUpload:
rq = Setup(c, "/uploads/"+id)
case common.TypeForm:
rq = Setup(c, "/forms/"+id)
caption = "Form"
}
resp, err := rq.R.Delete(rq.Url) resp, err := rq.R.Delete(rq.Url)
@@ -236,20 +263,27 @@ func Delete(w io.Writer, c *cfg.Config, args []string) error {
return err return err
} }
fmt.Fprintf(w, "Upload %s successfully deleted.\n", id) fmt.Fprintf(w, "%s %s successfully deleted.\n", caption, id)
} }
return nil return nil
} }
func Describe(w io.Writer, c *cfg.Config, args []string) error { func Describe(w io.Writer, c *cfg.Config, args []string, typ int) error {
if len(args) == 0 { if len(args) == 0 {
return errors.New("No id provided!") return errors.New("No id provided!")
} }
var rq *Request
id := args[0] // we describe only 1 object id := args[0] // we describe only 1 object
rq := Setup(c, "/uploads/"+id) switch typ {
case common.TypeUpload:
rq = Setup(c, "/uploads/"+id)
case common.TypeForm:
rq = Setup(c, "/forms/"+id)
}
resp, err := rq.R.Get(rq.Url) resp, err := rq.R.Get(rq.Url)
if err != nil { if err != nil {
@@ -278,7 +312,9 @@ func Download(w io.Writer, c *cfg.Config, args []string) error {
callback := func(info req.DownloadInfo) { callback := func(info req.DownloadInfo) {
if info.Response.Response != nil { if info.Response.Response != nil {
bar.Add(1) if err := bar.Add(1); err != nil {
fmt.Print("\r")
}
} }
} }
@@ -328,10 +364,10 @@ func CreateForm(w io.Writer, c *cfg.Config) error {
// actual post w/ settings // actual post w/ settings
resp, err := rq.R. resp, err := rq.R.
SetFormData(map[string]string{ SetBody(&common.Form{
"expire": c.Expire, Expire: c.Expire,
"description": c.Description, Description: c.Description,
"notify": c.Notify, Notify: c.Notify,
}). }).
Post(rq.Url) Post(rq.Url)
@@ -344,6 +380,4 @@ func CreateForm(w io.Writer, c *cfg.Config) error {
} }
return RespondExtended(w, resp) return RespondExtended(w, resp)
return nil
} }

View File

@@ -22,6 +22,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/jarcoal/httpmock" "github.com/jarcoal/httpmock"
"github.com/tlinden/ephemerup/common"
"github.com/tlinden/ephemerup/upctl/cfg" "github.com/tlinden/ephemerup/upctl/cfg"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@@ -205,7 +206,7 @@ func TestList(t *testing.T) {
sendjson: listing, sendjson: listing,
files: []string{}, files: []string{},
method: "GET", method: "GET",
expect: `cc2c965a\s*asap\s*foo\s*2023-03-21 12:06:54`, // expect tabular output expect: `cc2c965a\s*asap\s*foo\s*2023-03-21`, // expect tabular output
}, },
{ {
name: "list-catch-empty-json", name: "list-catch-empty-json",
@@ -232,7 +233,7 @@ func TestList(t *testing.T) {
for _, unit := range tests { for _, unit := range tests {
var w bytes.Buffer var w bytes.Buffer
Intercept(unit) Intercept(unit)
Check(t, unit, &w, List(&w, conf, []string{})) Check(t, unit, &w, List(&w, conf, []string{}, common.TypeUpload))
} }
} }
@@ -266,7 +267,7 @@ func TestDescribe(t *testing.T) {
sendjson: listing, sendjson: listing,
files: []string{"cc2c965a"}, files: []string{"cc2c965a"},
method: "GET", method: "GET",
expect: `Created: 2023-03-21 12:06:54.890501888`, expect: `Created: 2023-03-21`,
}, },
{ {
name: "describe-catch-empty-json", name: "describe-catch-empty-json",
@@ -294,7 +295,7 @@ func TestDescribe(t *testing.T) {
var w bytes.Buffer var w bytes.Buffer
unit.route += unit.files[0] unit.route += unit.files[0]
Intercept(unit) Intercept(unit)
Check(t, unit, &w, Describe(&w, conf, unit.files)) Check(t, unit, &w, Describe(&w, conf, unit.files, common.TypeUpload))
} }
} }
@@ -344,9 +345,9 @@ func TestDelete(t *testing.T) {
for _, unit := range tests { for _, unit := range tests {
var w bytes.Buffer var w bytes.Buffer
unit.route += unit.files[0] + "/" unit.route += unit.files[0]
Intercept(unit) Intercept(unit)
Check(t, unit, &w, Delete(&w, conf, unit.files)) Check(t, unit, &w, Delete(&w, conf, unit.files, common.TypeUpload))
} }
} }

View File

@@ -21,10 +21,12 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
//"github.com/alecthomas/repr"
"github.com/imroc/req/v3" "github.com/imroc/req/v3"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"github.com/tlinden/ephemerup/common" "github.com/tlinden/ephemerup/common"
"io" "io"
"sort"
"strings" "strings"
"time" "time"
) )
@@ -38,8 +40,6 @@ func prepareExpire(expire string, start common.Timestamp) string {
return time.Unix(start.Unix()+int64(common.Duration2int(expire)), 0). return time.Unix(start.Unix()+int64(common.Duration2int(expire)), 0).
Format("2006-01-02 15:04:05") Format("2006-01-02 15:04:05")
} }
return ""
} }
// generic table writer // generic table writer
@@ -79,7 +79,8 @@ func WriteExtended(w io.Writer, response *common.Response) {
// we shall only have 1 element, however, if we ever support more, here we go // we shall only have 1 element, however, if we ever support more, here we go
for _, entry := range response.Uploads { for _, entry := range response.Uploads {
expire := prepareExpire(entry.Expire, entry.Created) expire := prepareExpire(entry.Expire, entry.Created)
fmt.Fprintf(w, format, "Id", entry.Id) fmt.Fprintf(w, format, "Upload-Id", entry.Id)
fmt.Fprintf(w, format, "Description", entry.Id)
fmt.Fprintf(w, format, "Expire", expire) fmt.Fprintf(w, format, "Expire", expire)
fmt.Fprintf(w, format, "Context", entry.Context) fmt.Fprintf(w, format, "Context", entry.Context)
fmt.Fprintf(w, format, "Created", entry.Created) fmt.Fprintf(w, format, "Created", entry.Created)
@@ -90,18 +91,18 @@ func WriteExtended(w io.Writer, response *common.Response) {
for _, entry := range response.Forms { for _, entry := range response.Forms {
expire := prepareExpire(entry.Expire, entry.Created) expire := prepareExpire(entry.Expire, entry.Created)
fmt.Fprintf(w, format, "Id", entry.Id) fmt.Fprintf(w, format, "Form-Id", entry.Id)
fmt.Fprintf(w, format, "Description", entry.Description)
fmt.Fprintf(w, format, "Expire", expire) fmt.Fprintf(w, format, "Expire", expire)
fmt.Fprintf(w, format, "Context", entry.Context) fmt.Fprintf(w, format, "Context", entry.Context)
fmt.Fprintf(w, format, "Created", entry.Created) fmt.Fprintf(w, format, "Created", entry.Created)
fmt.Fprintf(w, format, "Description", entry.Description)
fmt.Fprintf(w, format, "Notify", entry.Notify) fmt.Fprintf(w, format, "Notify", entry.Notify)
fmt.Fprintf(w, format, "Url", entry.Url) fmt.Fprintf(w, format, "Url", entry.Url)
fmt.Fprintln(w) fmt.Fprintln(w)
} }
} }
// extract an common.Uploads{} struct from json response // extract an common.Response{} struct from json response
func GetResponse(resp *req.Response) (*common.Response, error) { func GetResponse(resp *req.Response) (*common.Response, error) {
response := common.Response{} response := common.Response{}
@@ -127,15 +128,49 @@ func UploadsRespondTable(w io.Writer, resp *req.Response) error {
fmt.Fprintln(w, response.Message) fmt.Fprintln(w, response.Message)
} }
sort.SliceStable(response.Uploads, func(i, j int) bool {
return response.Uploads[i].Created.Time.Unix() < response.Uploads[j].Created.Time.Unix()
})
// tablewriter // tablewriter
data := [][]string{} data := [][]string{}
for _, entry := range response.Uploads { for _, entry := range response.Uploads {
data = append(data, []string{ data = append(data, []string{
entry.Id, entry.Expire, entry.Context, entry.Created.Format("2006-01-02 15:04:05"), entry.Id, entry.Description, entry.Expire, entry.Context,
entry.Created.Format("2006-01-02 15:04:05"), entry.File,
}) })
} }
WriteTable(w, []string{"ID", "EXPIRE", "CONTEXT", "CREATED"}, data) WriteTable(w, []string{"UPLOAD-ID", "DESCRIPTION", "EXPIRE", "CONTEXT", "CREATED", "FILE"}, data)
return nil
}
// turn the Forms{} struct into a table and print it
func FormsRespondTable(w io.Writer, resp *req.Response) error {
response, err := GetResponse(resp)
if err != nil {
return err
}
if response.Message != "" {
fmt.Fprintln(w, response.Message)
}
sort.SliceStable(response.Forms, func(i, j int) bool {
return response.Forms[i].Created.Time.Unix() < response.Forms[j].Created.Time.Unix()
})
// tablewriter
data := [][]string{}
for _, entry := range response.Forms {
data = append(data, []string{
entry.Id, entry.Description, entry.Expire, entry.Context,
entry.Created.Format("2006-01-02 15:04:05"), entry.Notify,
})
}
WriteTable(w, []string{"FORM-ID", "DESCRIPTION", "EXPIRE", "CONTEXT", "CREATED", "NOTIFY"}, data)
return nil return nil
} }

View File

@@ -1 +1 @@
Mon Mar 20 12:16:26 PM CET 2023 Wed Mar 29 03:01:21 PM CEST 2023

View File

@@ -1,2 +1,3 @@
endpoint = "http://localhost:8080/v1" endpoint = "http://localhost:8080/v1"
apikey = "970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a" apikey = "970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a"
notify = "root@localhost"