mirror of
https://codeberg.org/scip/ephemerup.git
synced 2025-12-16 20:20:58 +01:00
changes:
- added unit tests
- put all subcmds into one file
- use io.Writer for output, better for testing
- added upload form support
- added api docs
- generalized db engine
- added mail notify support for forms
- enhanced server/SetupAuthStore() to also look up form ids
- added form template (put into .go file by Makefile
- renamed project
This commit is contained in:
14
Dockerfile
14
Dockerfile
@@ -12,7 +12,7 @@ WORKDIR /work
|
|||||||
COPY go.mod .
|
COPY go.mod .
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
RUN make && strip cenod
|
RUN make && strip ephemerupd
|
||||||
|
|
||||||
FROM alpine:3.17
|
FROM alpine:3.17
|
||||||
LABEL maintainer="Uploads Author <info@daemon.de>"
|
LABEL maintainer="Uploads Author <info@daemon.de>"
|
||||||
@@ -20,14 +20,14 @@ LABEL maintainer="Uploads Author <info@daemon.de>"
|
|||||||
RUN install -o 1001 -g 1001 -d /data
|
RUN install -o 1001 -g 1001 -d /data
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /work/cenod /app/cenod
|
COPY --from=builder /work/ephemerupd /app/ephemerupd
|
||||||
|
|
||||||
ENV CENOD_LISTEN=:8080
|
ENV EPHEMERUPD_LISTEN=:8080
|
||||||
ENV CENOD_STORAGEDIR=/data
|
ENV EPHEMERUPD_STORAGEDIR=/data
|
||||||
ENV CENOD_DBFILE=/data/bbolt.db
|
ENV EPHEMERUPD_DBFILE=/data/bbolt.db
|
||||||
ENV CENOD_DEBUG=1
|
ENV EPHEMERUPD_DEBUG=1
|
||||||
|
|
||||||
USER 1001:1001
|
USER 1001:1001
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
CMD ["/app/cenod"]
|
CMD ["/app/ephemerupd"]
|
||||||
|
|||||||
19
Makefile
19
Makefile
@@ -27,15 +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)
|
||||||
DAEMON := cenod
|
DAEMON := ephemerupd
|
||||||
|
|
||||||
all: buildlocal buildlocalctl
|
all: cmd/formtemplate.go buildlocal buildlocalctl
|
||||||
|
|
||||||
buildlocalctl:
|
buildlocalctl:
|
||||||
make -C upctl
|
make -C upctl
|
||||||
|
|
||||||
buildlocal:
|
buildlocal:
|
||||||
go build -ldflags "-X 'github.com/tlinden/cenophane/cfg.VERSION=$(VERSION)'" -o $(DAEMON)
|
go build -ldflags "-X 'github.com/tlinden/ephemerup/cfg.VERSION=$(VERSION)'" -o $(DAEMON)
|
||||||
|
|
||||||
buildimage: clean
|
buildimage: clean
|
||||||
docker-compose --verbose build
|
docker-compose --verbose build
|
||||||
@@ -60,15 +60,15 @@ test:
|
|||||||
|
|
||||||
singletest:
|
singletest:
|
||||||
@echo "Call like this: ''make singletest TEST=TestX1 MOD=lib"
|
@echo "Call like this: ''make singletest TEST=TestX1 MOD=lib"
|
||||||
go test -run $(TEST) github.com/tlinden/cenophane/$(MOD)
|
go test -run $(TEST) github.com/tlinden/ephemerup/$(MOD)
|
||||||
|
|
||||||
cover-report:
|
cover-report:
|
||||||
go test ./... -cover -coverprofile=coverage.out
|
go test ./... -cover -coverprofile=coverage.out
|
||||||
go tool cover -html=coverage.out
|
go tool cover -html=coverage.out
|
||||||
|
|
||||||
show-versions: buildlocal
|
show-versions: buildlocal
|
||||||
@echo "### cenod version:"
|
@echo "### ephemerupd version:"
|
||||||
@./cenod --version
|
@./ephemerupd --version
|
||||||
|
|
||||||
@echo
|
@echo
|
||||||
@echo "### go module versions:"
|
@echo "### go module versions:"
|
||||||
@@ -80,3 +80,10 @@ show-versions: buildlocal
|
|||||||
|
|
||||||
goupdate:
|
goupdate:
|
||||||
go get -t -u=patch ./...
|
go get -t -u=patch ./...
|
||||||
|
|
||||||
|
cmd/%.go: templates/%.html
|
||||||
|
echo "package cmd" > cmd/$*.go
|
||||||
|
echo >> cmd/$*.go
|
||||||
|
echo "const formtemplate = \`" >> cmd/$*.go
|
||||||
|
cat templates/$*.html >> cmd/$*.go
|
||||||
|
echo "\`" >> cmd/$*.go
|
||||||
|
|||||||
115
README.md
115
README.md
@@ -1,9 +1,9 @@
|
|||||||
# Cenophane
|
# ephemerup
|
||||||
Simple standalone file upload server with expiration and commandline client.
|
Simple standalone file upload server with expiration and commandline client.
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
**Cenophane** is a simple standalone file server where every uploaded
|
**ephemerup** is a simple standalone file server where every uploaded
|
||||||
file expires sooner or later. The server provides a RESTful API and
|
file expires sooner or later. The server provides a RESTful API and
|
||||||
can be used easily with the commandline client `upctl`.
|
can be used easily with the commandline client `upctl`.
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ important enough to keep them around. Think of this szenario: you're
|
|||||||
working for the network departement and there's a problem with your
|
working for the network departement and there's a problem with your
|
||||||
routing. Tech support asks you to create a network trace and send it
|
routing. Tech support asks you to create a network trace and send it
|
||||||
to them. But you can't because the trace file is too large and
|
to them. But you can't because the trace file is too large and
|
||||||
sensitive to be sent by email. This is where **Cenophane** comes to
|
sensitive to be sent by email. This is where **ephemerup** comes to
|
||||||
the rescue.
|
the rescue.
|
||||||
|
|
||||||
You upload the file, send the download url to the other party and -
|
You upload the file, send the download url to the other party and -
|
||||||
@@ -21,11 +21,11 @@ assuming you've utilized the defaults - when they download it, it is
|
|||||||
being deleted immediately from the server. But you can also set an
|
being deleted immediately from the server. But you can also set an
|
||||||
expire time, say 5 days or something like that.
|
expire time, say 5 days or something like that.
|
||||||
|
|
||||||
The download urls generated by **Cenophane** consist of a unique
|
The download urls generated by **ephemerup** consist of a unique
|
||||||
onetime hash, so they are somewhat confident. However, if you're
|
onetime hash, so they are somewhat confident. However, if you're
|
||||||
uploading really sensitive data, you better encrypt it.
|
uploading really sensitive data, you better encrypt it.
|
||||||
|
|
||||||
**Cenophane** also supports something we call an API Context. There
|
**ephemerup** also supports something we call an API Context. There
|
||||||
can be many such API contexts. Each of these has an associated token,
|
can be many such API contexts. Each of these has an associated token,
|
||||||
which has to be used by legitimate clients to authenticate and
|
which has to be used by legitimate clients to authenticate and
|
||||||
authorize. A user can only manage uploads within that context. Think
|
authorize. A user can only manage uploads within that context. Think
|
||||||
@@ -60,17 +60,17 @@ releases available yet. You'll need a go build environment. Just run
|
|||||||
There's a `Dockerfile` available for the server so you can build and run it using docker:
|
There's a `Dockerfile` available for the server so you can build and run it using docker:
|
||||||
```
|
```
|
||||||
make buildimage
|
make buildimage
|
||||||
docker-compose run cenophane
|
docker-compose run ephemerup
|
||||||
```
|
```
|
||||||
Then use the client to test it.
|
Then use the client to test it.
|
||||||
|
|
||||||
## Server Usage
|
## Server Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
cenod -h
|
ephemerupd -h
|
||||||
--apikeys strings Api key[s] to allow access
|
--apikeys strings Api key[s] to allow access
|
||||||
-a, --apiprefix string API endpoint path (default "/api")
|
-a, --apiprefix string API endpoint path (default "/api")
|
||||||
-n, --appname string App name to say hi as (default "cenod v0.0.1")
|
-n, --appname string App name to say hi as (default "ephemerupd v0.0.1")
|
||||||
-b, --bodylimit int Max allowed upload size in bytes (default 10250000000)
|
-b, --bodylimit int Max allowed upload size in bytes (default 10250000000)
|
||||||
-c, --config string custom config file
|
-c, --config string custom config file
|
||||||
-D, --dbfile string Bold database file to use (default "/tmp/uploads.db")
|
-D, --dbfile string Bold database file to use (default "/tmp/uploads.db")
|
||||||
@@ -86,23 +86,23 @@ cenod -h
|
|||||||
-v, --version Print program version
|
-v, --version Print program version
|
||||||
```
|
```
|
||||||
|
|
||||||
All flags can be set using environment variables, prefix the flag with `CENOD_` and uppercase it, eg:
|
All flags can be set using environment variables, prefix the flag with `EPHEMERUPD_` and uppercase it, eg:
|
||||||
```
|
```
|
||||||
CENOD_LISTEN=:8080
|
EPHEMERUPD_LISTEN=:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
In addition it is possible to set api contexts using env vars (otherwise only possible using the config file):
|
In addition it is possible to set api contexts using env vars (otherwise only possible using the config file):
|
||||||
```
|
```
|
||||||
CENOD_CONTEXT_SUPPORT="support:tymag-fycyh-gymof-dysuf-doseb-puxyx"
|
EPHEMERUPD_CONTEXT_SUPPORT="support:tymag-fycyh-gymof-dysuf-doseb-puxyx"
|
||||||
CENOD_CONTEXT_FOOBAR="foobar:U3VuIE1hciAxOSAxMjoyNTo1NyBQTSBDRVQgMjAyMwo"
|
EPHEMERUPD_CONTEXT_FOOBAR="foobar:U3VuIE1hciAxOSAxMjoyNTo1NyBQTSBDRVQgMjAyMwo"
|
||||||
```
|
```
|
||||||
|
|
||||||
Configuration can also be done using a config file (searched in the following locations):
|
Configuration can also be done using a config file (searched in the following locations):
|
||||||
- `/etc/cenod.hcl`
|
- `/etc/ephemerupd.hcl`
|
||||||
- `/usr/local/etc/cenod.hcl`
|
- `/usr/local/etc/ephemerupd.hcl`
|
||||||
- `~/.config/cenod/cenod.hcl`
|
- `~/.config/ephemerupd/ephemerupd.hcl`
|
||||||
- `~/.cenod`
|
- `~/.ephemerupd`
|
||||||
- `$(pwd)/cenod.hcl`
|
- `$(pwd)/ephemerupd.hcl`
|
||||||
|
|
||||||
Or using the flag `-c`. Sample config file:
|
Or using the flag `-c`. Sample config file:
|
||||||
```
|
```
|
||||||
@@ -131,7 +131,7 @@ super = "root"
|
|||||||
The server serves the API under the following endpoint:
|
The server serves the API under the following endpoint:
|
||||||
`http://SERVERNAME[:PORT]/api/v1` where SERVERNAME[:PORT] is the
|
`http://SERVERNAME[:PORT]/api/v1` where SERVERNAME[:PORT] is the
|
||||||
argument to the `-l` commandline argument or the config option
|
argument to the `-l` commandline argument or the config option
|
||||||
`listen` or the environment variable `CENOD_LISTEN`.
|
`listen` or the environment variable `EPHEMERUPD_LISTEN`.
|
||||||
|
|
||||||
By default the server listens on any interface ip4 and ipv6 on TCP
|
By default the server listens on any interface ip4 and ipv6 on TCP
|
||||||
port 8080. You can specify a server name or an ipaddress and a
|
port 8080. You can specify a server name or an ipaddress and a
|
||||||
@@ -141,6 +141,83 @@ the `-4` respective the `-6` commandline flags.
|
|||||||
It does not support TLS at the moment. Use a nginx reverse proxy in
|
It does not support TLS at the moment. Use a nginx reverse proxy in
|
||||||
front of it.
|
front of it.
|
||||||
|
|
||||||
|
### Server REST API
|
||||||
|
|
||||||
|
Every endpoint returns a JSON object. Each returned object contains the data requested plus:
|
||||||
|
|
||||||
|
- success: true or false
|
||||||
|
- code: HTTP Response Code
|
||||||
|
- message: error message, if success==false
|
||||||
|
|
||||||
|
#### Endpoints
|
||||||
|
|
||||||
|
| HTTP Method | Endpoint | Parameters | Input | Returns | Description |
|
||||||
|
|-------------|-----------------------|---------------------|----------------------------|---------------------------------------|-----------------------------------------------|
|
||||||
|
| GET | /v1/uploads | apicontext,q,expire | | List of upload objects | list upload objects |
|
||||||
|
| POST | /v1/uploads | | multipart-formdata file[s] | List of 1 upload object if successful | upload a file and create a new upload object |
|
||||||
|
| GET | /v1/uploads/{id} | | | List of 1 upload object if successful | list one specific upload object matching {id} |
|
||||||
|
| DELETE | /v1/uploads/{id} | | | Noting | delete an upload object identified by {id} |
|
||||||
|
| PUT | /v1/uploads/{id} | | JSON upload object | List of 1 upload object if successful | modify an upload object identified by {id} |
|
||||||
|
| GET | /v1/uploads/{id}/file | | | File download | Download the file associated with the object |
|
||||||
|
| GET | /v1/forms | apicontext,q,expire | | List of form objects | list form objects |
|
||||||
|
| POST | /v1/forms | | JSON form object | List of 1 form object if successful | create a new form object |
|
||||||
|
| GET | /v1/forms/{id} | | | List of 1 form object if successful | list one specific form object matching {id} |
|
||||||
|
| DELETE | /v1/forms/{id} | | | Noting | delete an form object identified by {id} |
|
||||||
|
| PUT | /v1/forms/{id} | | JSON form object | List of 1 form object if successful | modify an form object identified by {id} |
|
||||||
|
|
||||||
|
#### Consumer URLs
|
||||||
|
|
||||||
|
The following endpoints are no API urls, but accessed directly by consumers using their browser or `wget` etc:
|
||||||
|
|
||||||
|
| URL | Description |
|
||||||
|
|-------------------------|---------------------------------------------------------|
|
||||||
|
| / | Display a short welcome message, can be customized |
|
||||||
|
| /download/{id}[/{file}] | Download link returned after an upload has been created |
|
||||||
|
| /form/{id} | Upload form for consumer |
|
||||||
|
|
||||||
|
#### API Objects
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
| Field | Data Type | Description |
|
||||||
|
|---------|-----------|---------------------------------------|
|
||||||
|
| success | bool | if true the request was successful |
|
||||||
|
| code | int | HTTP response code |
|
||||||
|
| message | string | error message, if any |
|
||||||
|
| uploads | array | list of upload objects (may be empty) |
|
||||||
|
| forms | array | list of form objects (may be empty) |
|
||||||
|
|
||||||
|
Upload:
|
||||||
|
|
||||||
|
| Field | Data Type | Description |
|
||||||
|
|----------|------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| id | string | unique identifier for the object |
|
||||||
|
| expire | string | when the upload has to expire, either "asap" or a Duration using numbers and the letters d,h,m,s (days,hours,minutes,seconds), e.g. 2d4h30m |
|
||||||
|
| file | string | filename after uploading, this is what a consumer gets when downloading it |
|
||||||
|
| members | array of strings | list of the original filenames |
|
||||||
|
| created | timestamp | time of object creation |
|
||||||
|
| context | string | the API context the upload has been created under |
|
||||||
|
| url | string | the download URL |
|
||||||
|
|
||||||
|
Form:
|
||||||
|
|
||||||
|
| Field | Data Type | Description |
|
||||||
|
|-------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| id | string | unique identifier for the object |
|
||||||
|
| expire | string | when the form has to expire, either "asap" or a Duration using numbers and the letters d,h,m,s (days,hours,minutes,seconds), e.g. 2d4h30m |
|
||||||
|
| description | string | arbitrary description, shown on the form page |
|
||||||
|
| context | string | the API context the form has been created under and the uploaded files will be created on |
|
||||||
|
| notify | string | email address of the form creator, who gets an email once the consumer has uploaded files using the form |
|
||||||
|
| created | timestamp | time of object creation |
|
||||||
|
| url | string | the form URL |
|
||||||
|
|
||||||
|
Note: if the expire field for a form is not set or set to "asap" only
|
||||||
|
1 upload object can be created from it. However, if a duration has
|
||||||
|
been specified, the form can be used multiple times and thus creates
|
||||||
|
multiple upload objects.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Client Usage
|
## Client Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -181,7 +258,7 @@ endpoint = "http://localhost:8080/api/v1"
|
|||||||
apikey = "970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a"
|
apikey = "970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a"
|
||||||
```
|
```
|
||||||
|
|
||||||
The `endpoint` is the **Cenophane** server running somewhere and the
|
The `endpoint` is the **ephemerup** server running somewhere and the
|
||||||
`apikey` is the token you got from the server operator..
|
`apikey` is the token you got from the server operator..
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
41
api/auth.go
41
api/auth.go
@@ -23,8 +23,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/keyauth/v2"
|
"github.com/gofiber/keyauth/v2"
|
||||||
"github.com/tlinden/cenophane/cfg"
|
"github.com/tlinden/ephemerup/cfg"
|
||||||
"regexp"
|
"github.com/tlinden/ephemerup/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
// these vars can be savely global, since they don't change ever
|
// these vars can be savely global, since they don't change ever
|
||||||
@@ -39,8 +39,7 @@ var (
|
|||||||
Message: "Invalid API key",
|
Message: "Invalid API key",
|
||||||
}
|
}
|
||||||
|
|
||||||
Authurls []*regexp.Regexp
|
Apikeys []cfg.Apicontext
|
||||||
Apikeys []cfg.Apicontext
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// fill from server: accepted keys
|
// fill from server: accepted keys
|
||||||
@@ -48,13 +47,6 @@ func AuthSetApikeys(keys []cfg.Apicontext) {
|
|||||||
Apikeys = keys
|
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure we always return JSON encoded errors
|
// make sure we always return JSON encoded errors
|
||||||
func AuthErrHandler(ctx *fiber.Ctx, err error) error {
|
func AuthErrHandler(ctx *fiber.Ctx, err error) error {
|
||||||
ctx.Status(fiber.StatusForbidden)
|
ctx.Status(fiber.StatusForbidden)
|
||||||
@@ -66,6 +58,33 @@ func AuthErrHandler(ctx *fiber.Ctx, err error) error {
|
|||||||
return ctx.JSON(errInvalid)
|
return ctx.JSON(errInvalid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validator hook, validates incoming api key against form id, which
|
||||||
|
// also acts as onetime api key
|
||||||
|
func AuthValidateOnetimeKey(c *fiber.Ctx, key string, db *Db) (bool, error) {
|
||||||
|
resp, err := db.Get("", key, common.TypeForm)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.New("Onetime key doesn't match any form id!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Forms) != 1 {
|
||||||
|
return false, errors.New("db.Get(form) returned no results and no errors!")
|
||||||
|
}
|
||||||
|
|
||||||
|
sess, err := Sessionstore.Get(c)
|
||||||
|
|
||||||
|
// store the result into the session, the 'formid' key tells the
|
||||||
|
// upload handler that the apicontext it sees is in fact a form id
|
||||||
|
// and has to be deleted if set to asap.
|
||||||
|
sess.Set("apicontext", resp.Forms[0].Context)
|
||||||
|
sess.Set("formid", key)
|
||||||
|
|
||||||
|
if err := sess.Save(); err != nil {
|
||||||
|
return false, errors.New("Unable to save session store!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// validator hook, called by fiber via server keyauth.New()
|
// validator hook, called by fiber via server keyauth.New()
|
||||||
func AuthValidateAPIKey(c *fiber.Ctx, key string) (bool, error) {
|
func AuthValidateAPIKey(c *fiber.Ctx, key string) (bool, error) {
|
||||||
// create a new session, it will be thrown away if something fails
|
// create a new session, it will be thrown away if something fails
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
//"github.com/alecthomas/repr"
|
//"github.com/alecthomas/repr"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/tlinden/cenophane/cfg"
|
"github.com/tlinden/ephemerup/cfg"
|
||||||
"github.com/tlinden/cenophane/common"
|
"github.com/tlinden/ephemerup/common"
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -42,7 +42,7 @@ func DeleteExpiredUploads(conf *cfg.Config, db *Db) error {
|
|||||||
return fmt.Errorf("unable to unmarshal json: %s", err)
|
return fmt.Errorf("unable to unmarshal json: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if IsExpired(conf, upload.Uploaded.Time, upload.Expire) {
|
if IsExpired(conf, upload.Created.Time, upload.Expire) {
|
||||||
if err := bucket.Delete([]byte(id)); err != nil {
|
if err := bucket.Delete([]byte(id)); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
83
api/db.go
83
api/db.go
@@ -18,15 +18,14 @@ 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/ephemerup/cfg"
|
||||||
"github.com/tlinden/cenophane/common"
|
"github.com/tlinden/ephemerup/common"
|
||||||
//"github.com/alecthomas/repr"
|
//"github.com/alecthomas/repr"
|
||||||
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) List(apicontext string, filter string) (*common.Uploads, error) {
|
func (db *Db) List(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,31 @@ func (db *Db) List(apicontext string, filter string) (*common.Uploads, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("apicontext: %s, filter: %s\n", apicontext, filter)
|
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)
|
||||||
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)
|
response.Append(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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)
|
response.Append(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,12 +146,13 @@ func (db *Db) List(apicontext string, filter string) (*common.Uploads, error) {
|
|||||||
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) {
|
// FIXME: turn the id into a filter and call (Uploads|Forms)List(), same code!
|
||||||
uploads := &common.Uploads{}
|
func (db *Db) Get(apicontext string, id string, t int) (*common.Response, error) {
|
||||||
|
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 +165,42 @@ 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)
|
response.Append(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
231
api/db_test.go
Normal file
231
api/db_test.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
/*
|
||||||
|
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/maxatome/go-testdeep/td"
|
||||||
|
"github.com/tlinden/ephemerup/cfg"
|
||||||
|
"github.com/tlinden/ephemerup/common"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func finalize(db *Db) {
|
||||||
|
if db.bolt != nil {
|
||||||
|
db.Close()
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(db.cfg.DbFile); err == nil {
|
||||||
|
os.Remove(db.cfg.DbFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
dbfile string
|
||||||
|
wantfail bool
|
||||||
|
}{
|
||||||
|
{"opennew", "test.db", false},
|
||||||
|
{"openfail", "/hopefully/not/existing/directory/test.db", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
c := &cfg.Config{DbFile: tt.dbfile}
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
db, err := NewDb(c)
|
||||||
|
defer finalize(db)
|
||||||
|
if err != nil && !tt.wantfail {
|
||||||
|
t.Errorf("expected: &Db{}, got err: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && tt.wantfail {
|
||||||
|
t.Errorf("expected: fail, got &Db{}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeformat string = "2006-01-02T15:04:05.000Z"
|
||||||
|
|
||||||
|
var dbtests = []struct {
|
||||||
|
name string
|
||||||
|
dbfile string
|
||||||
|
wantfail bool
|
||||||
|
id string
|
||||||
|
context string
|
||||||
|
ts string
|
||||||
|
filter string
|
||||||
|
upload common.Upload
|
||||||
|
form common.Form
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"upload", "test.db", false, "1", "foo",
|
||||||
|
"2023-03-10T11:45:00.000Z", "",
|
||||||
|
common.Upload{
|
||||||
|
Id: "1", Expire: "asap", File: "none", Context: "foo",
|
||||||
|
Created: common.Timestamp{}},
|
||||||
|
common.Form{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"form", "test.db", false, "2", "foo",
|
||||||
|
"2023-03-10T11:45:00.000Z", "",
|
||||||
|
common.Upload{},
|
||||||
|
common.Form{
|
||||||
|
Id: "1", Expire: "asap", Description: "none", Context: "foo",
|
||||||
|
Created: common.Timestamp{}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
We need to test the whole Db operation in one run, because it
|
||||||
|
doesn't work well if using a global Db.
|
||||||
|
*/
|
||||||
|
func TestDboperation(t *testing.T) {
|
||||||
|
for _, tt := range dbtests {
|
||||||
|
c := &cfg.Config{DbFile: tt.dbfile}
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// create new bbolt db
|
||||||
|
db, err := NewDb(c)
|
||||||
|
defer finalize(db)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not open new DB: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.upload.Id != "" {
|
||||||
|
// set ts
|
||||||
|
ts, err := time.Parse(timeformat, tt.ts)
|
||||||
|
tt.upload.Created = common.Timestamp{Time: ts}
|
||||||
|
|
||||||
|
// create new upload db object
|
||||||
|
err = db.Insert(tt.id, tt.upload)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not insert new upload object: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch it
|
||||||
|
response, err := db.Get(tt.context, tt.id, common.TypeUpload)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not fetch upload object: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// is it there?
|
||||||
|
if len(response.Uploads) != 1 {
|
||||||
|
t.Errorf("db.Get() did not return an upload obj")
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare times
|
||||||
|
if !tt.upload.Created.Time.Equal(response.Uploads[0].Created.Time) {
|
||||||
|
t.Errorf("Timestamps don't match!\ngot: %s\nexp: %s\n",
|
||||||
|
response.Uploads[0].Created, tt.upload.Created)
|
||||||
|
}
|
||||||
|
|
||||||
|
// equal them artificially, because otherwise td will
|
||||||
|
// fail because of time.Time.wall+ext, or TZ is missing
|
||||||
|
response.Uploads[0].Created = tt.upload.Created
|
||||||
|
|
||||||
|
// compare
|
||||||
|
td.Cmp(t, response.Uploads[0], &tt.upload, tt.name)
|
||||||
|
|
||||||
|
// fetch list
|
||||||
|
response, err = db.List(tt.context, tt.filter, common.TypeUpload)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not fetch uploads list: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// is it there?
|
||||||
|
if len(response.Uploads) != 1 {
|
||||||
|
t.Errorf("db.List() did not return upload obj[s]")
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete
|
||||||
|
err = db.Delete(tt.context, tt.id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not delete upload obj: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch again, shall return empty
|
||||||
|
response, err = db.Get(tt.context, tt.id, common.TypeUpload)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Could fetch upload object again although we deleted it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.form.Id != "" {
|
||||||
|
// set ts
|
||||||
|
ts, err := time.Parse(timeformat, tt.ts)
|
||||||
|
tt.form.Created = common.Timestamp{Time: ts}
|
||||||
|
|
||||||
|
// create new form db object
|
||||||
|
err = db.Insert(tt.id, tt.form)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not insert new form object: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch it
|
||||||
|
response, err := db.Get(tt.context, tt.id, common.TypeForm)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not fetch form object: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// is it there?
|
||||||
|
if len(response.Forms) != 1 {
|
||||||
|
t.Errorf("db.Get() did not return an form obj")
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare times
|
||||||
|
if !tt.form.Created.Time.Equal(response.Forms[0].Created.Time) {
|
||||||
|
t.Errorf("Timestamps don't match!\ngot: %s\nexp: %s\n",
|
||||||
|
response.Forms[0].Created, tt.form.Created)
|
||||||
|
}
|
||||||
|
|
||||||
|
// equal them artificially, because otherwise td will
|
||||||
|
// fail because of time.Time.wall+ext, or TZ is missing
|
||||||
|
response.Forms[0].Created = tt.form.Created
|
||||||
|
|
||||||
|
// compare
|
||||||
|
td.Cmp(t, response.Forms[0], &tt.form, tt.name)
|
||||||
|
|
||||||
|
// fetch list
|
||||||
|
response, err = db.List(tt.context, tt.filter, common.TypeForm)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not fetch forms list: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// is it there?
|
||||||
|
if len(response.Forms) != 1 {
|
||||||
|
t.Errorf("db.FormsList() did not return form obj[s]")
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete
|
||||||
|
err = db.Delete(tt.context, tt.id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not delete form obj: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch again, shall return empty
|
||||||
|
response, err = db.Get(tt.context, tt.id, common.TypeForm)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Could fetch form object again although we deleted it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,8 +21,8 @@ import (
|
|||||||
"archive/zip"
|
"archive/zip"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/tlinden/cenophane/cfg"
|
"github.com/tlinden/ephemerup/cfg"
|
||||||
"github.com/tlinden/cenophane/common"
|
"github.com/tlinden/ephemerup/common"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
@@ -83,7 +83,7 @@ func ProcessFormFiles(cfg *cfg.Config, members []string, id string) (string, str
|
|||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
returnUrl = strings.Join([]string{cfg.Url + cfg.ApiPrefix + ApiVersion, "file", id, zipfile}, "/")
|
returnUrl = strings.Join([]string{cfg.Url, "download", id, zipfile}, "/")
|
||||||
Filename = zipfile
|
Filename = zipfile
|
||||||
|
|
||||||
// clean up after us
|
// clean up after us
|
||||||
|
|||||||
230
api/form_handlers.go
Normal file
230
api/form_handlers.go
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
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/ephemerup/cfg"
|
||||||
|
"github.com/tlinden/ephemerup/common"
|
||||||
|
|
||||||
|
"bytes"
|
||||||
|
"html/template"
|
||||||
|
"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 := SessionGetApicontext(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 expire data: "+err.Error())
|
||||||
|
}
|
||||||
|
entry.Expire = ex
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(formdata.Notify) != 0 {
|
||||||
|
nt, err := common.Untaint(formdata.Notify, cfg.RegEmail)
|
||||||
|
if err != nil {
|
||||||
|
return JsonStatus(c, fiber.StatusForbidden,
|
||||||
|
"Invalid email address: "+err.Error())
|
||||||
|
}
|
||||||
|
entry.Notify = nt
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete form
|
||||||
|
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 := SessionGetApicontext(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 form with that id could be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := SessionGetApicontext(c)
|
||||||
|
if err != nil {
|
||||||
|
return JsonStatus(c, fiber.StatusInternalServerError,
|
||||||
|
"Unable to initialize session store from context: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// get list
|
||||||
|
response, err := db.List(apicontext, filter, common.TypeForm)
|
||||||
|
if err != nil {
|
||||||
|
return JsonStatus(c, fiber.StatusForbidden,
|
||||||
|
"Unable to list forms: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we reached this point we can signal success
|
||||||
|
response.Success = true
|
||||||
|
response.Code = fiber.StatusOK
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns just one form obj + error code
|
||||||
|
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 := SessionGetApicontext(c)
|
||||||
|
if err != nil {
|
||||||
|
return JsonStatus(c, fiber.StatusInternalServerError,
|
||||||
|
"Unable to initialize session store from context: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := db.Get(apicontext, id, common.TypeForm)
|
||||||
|
if err != nil {
|
||||||
|
return JsonStatus(c, fiber.StatusForbidden,
|
||||||
|
"No form with that id could be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, form := range response.Forms {
|
||||||
|
form.Url = strings.Join([]string{cfg.Url, "form", id}, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we reached this point we can signal success
|
||||||
|
response.Success = true
|
||||||
|
response.Code = fiber.StatusOK
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Render the upload html form. Template given by --formpage, stored
|
||||||
|
as text in cfg.Formpage. It will be rendered using golang's
|
||||||
|
template engine, data to be filled in is the form matching the
|
||||||
|
given id.
|
||||||
|
*/
|
||||||
|
func FormPage(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallexpire bool) error {
|
||||||
|
id, err := common.Untaint(c.Params("id"), cfg.RegKey)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusForbidden).SendString("Invalid id provided!")
|
||||||
|
}
|
||||||
|
|
||||||
|
apicontext, err := SessionGetApicontext(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).SendString("Unable to initialize session store from context:" + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := db.Get(apicontext, id, common.TypeForm)
|
||||||
|
if err != nil || len(response.Forms) == 0 {
|
||||||
|
return c.Status(fiber.StatusForbidden).SendString("No form with that id could be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
t := template.New("form")
|
||||||
|
if t, err = t.Parse(cfg.Formpage); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).SendString("Unable to load form template: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare upload url
|
||||||
|
uploadurl := strings.Join([]string{cfg.ApiPrefix + ApiVersion, "uploads"}, "/")
|
||||||
|
response.Forms[0].Url = uploadurl
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := t.Execute(&out, response.Forms[0]); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).SendString("Unable to render form template: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("Content-type", "text/html; charset=utf-8")
|
||||||
|
return c.Status(fiber.StatusOK).SendString(out.String())
|
||||||
|
}
|
||||||
54
api/mail.go
Normal file
54
api/mail.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"fmt"
|
||||||
|
"github.com/tlinden/ephemerup/cfg"
|
||||||
|
"net/smtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mailtpl string = `To: %s\r
|
||||||
|
From: %s\r
|
||||||
|
Subject: %s\r
|
||||||
|
\r
|
||||||
|
%s\r
|
||||||
|
`
|
||||||
|
|
||||||
|
/*
|
||||||
|
Send an email via an external mail gateway. SMTP Auth is
|
||||||
|
required. Errors may occur with a time delay, like server timeouts
|
||||||
|
etc. So only call it detached via go routine.
|
||||||
|
*/
|
||||||
|
func Sendmail(c *cfg.Config, recipient string, body string, subject string) error {
|
||||||
|
// Message.
|
||||||
|
message := []byte(fmt.Sprintf(mailtpl, recipient, c.Mail.From, subject, body))
|
||||||
|
|
||||||
|
// Authentication.
|
||||||
|
auth := smtp.PlainAuth("", c.Mail.From, c.Mail.Password, c.Mail.Server)
|
||||||
|
|
||||||
|
// Sending email.
|
||||||
|
Log("Trying to send mail to %s via %s:%s with subject %s",
|
||||||
|
recipient, c.Mail.Server, c.Mail.Port, subject)
|
||||||
|
err := smtp.SendMail(c.Mail.Server+":"+c.Mail.Port, auth, c.Mail.From, []string{recipient}, []byte(message))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -26,8 +26,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||||
"github.com/gofiber/fiber/v2/middleware/session"
|
"github.com/gofiber/fiber/v2/middleware/session"
|
||||||
"github.com/gofiber/keyauth/v2"
|
"github.com/gofiber/keyauth/v2"
|
||||||
"github.com/tlinden/cenophane/cfg"
|
"github.com/tlinden/ephemerup/cfg"
|
||||||
"github.com/tlinden/cenophane/common"
|
"github.com/tlinden/ephemerup/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sessions are context specific and can be global savely
|
// sessions are context specific and can be global savely
|
||||||
@@ -47,7 +47,7 @@ func Runserver(conf *cfg.Config, args []string) error {
|
|||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// setup authenticated endpoints
|
// setup authenticated endpoints
|
||||||
auth := SetupAuthStore(conf)
|
auth := SetupAuthStore(conf, db)
|
||||||
|
|
||||||
// setup api server
|
// setup api server
|
||||||
router := SetupServer(conf)
|
router := SetupServer(conf)
|
||||||
@@ -56,32 +56,50 @@ func Runserver(conf *cfg.Config, args []string) error {
|
|||||||
api := router.Group(conf.ApiPrefix + ApiVersion)
|
api := router.Group(conf.ApiPrefix + ApiVersion)
|
||||||
{
|
{
|
||||||
// upload
|
// upload
|
||||||
api.Post("/file/", auth, func(c *fiber.Ctx) error {
|
api.Post("/uploads", auth, func(c *fiber.Ctx) error {
|
||||||
return FilePut(c, conf, db)
|
return UploadPost(c, conf, db)
|
||||||
})
|
|
||||||
|
|
||||||
// download w/o expire
|
|
||||||
api.Get("/file/:id/:file", auth, func(c *fiber.Ctx) error {
|
|
||||||
return FileGet(c, conf, db)
|
|
||||||
})
|
|
||||||
api.Get("/file/:id/", auth, func(c *fiber.Ctx) error {
|
|
||||||
return FileGet(c, conf, db)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// remove
|
// remove
|
||||||
api.Delete("/file/:id/", auth, func(c *fiber.Ctx) error {
|
api.Delete("/uploads/:id", auth, func(c *fiber.Ctx) error {
|
||||||
err := DeleteUpload(c, conf, db)
|
err := UploadDelete(c, conf, db)
|
||||||
return SendResponse(c, "", err)
|
return SendResponse(c, "", err)
|
||||||
})
|
})
|
||||||
|
|
||||||
// listing
|
// listing
|
||||||
api.Get("/list/", auth, func(c *fiber.Ctx) error {
|
api.Get("/uploads", auth, func(c *fiber.Ctx) error {
|
||||||
return List(c, conf, db)
|
return UploadsList(c, conf, db)
|
||||||
})
|
})
|
||||||
|
|
||||||
// info
|
// info/describe
|
||||||
api.Get("/upload/:id/", auth, func(c *fiber.Ctx) error {
|
api.Get("/uploads/:id", auth, func(c *fiber.Ctx) error {
|
||||||
return Describe(c, conf, db)
|
return UploadDescribe(c, conf, db)
|
||||||
|
})
|
||||||
|
|
||||||
|
// download w/o expire
|
||||||
|
api.Get("/uploads/:id/file", auth, func(c *fiber.Ctx) error {
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,12 +110,17 @@ func Runserver(conf *cfg.Config, args []string) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.Get("/download/:id/:file", func(c *fiber.Ctx) error {
|
router.Get("/download/:id/:file", func(c *fiber.Ctx) error {
|
||||||
return FileGet(c, conf, db, shallExpire)
|
return UploadFetch(c, conf, db, shallExpire)
|
||||||
})
|
})
|
||||||
|
|
||||||
router.Get("/download/:id/", func(c *fiber.Ctx) error {
|
router.Get("/download/:id", func(c *fiber.Ctx) error {
|
||||||
return FileGet(c, conf, db, shallExpire)
|
return UploadFetch(c, conf, db, shallExpire)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.Get("/form/:id", func(c *fiber.Ctx) error {
|
||||||
|
return FormPage(c, conf, db, shallExpire)
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup cleaner
|
// setup cleaner
|
||||||
@@ -112,12 +135,23 @@ func Runserver(conf *cfg.Config, args []string) error {
|
|||||||
return router.Listen(conf.Listen)
|
return router.Listen(conf.Listen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupAuthStore(conf *cfg.Config) func(*fiber.Ctx) error {
|
func SetupAuthStore(conf *cfg.Config, db *Db) func(*fiber.Ctx) error {
|
||||||
AuthSetEndpoints(conf.ApiPrefix, ApiVersion, []string{"/file"})
|
|
||||||
AuthSetApikeys(conf.Apicontexts)
|
AuthSetApikeys(conf.Apicontexts)
|
||||||
|
|
||||||
return keyauth.New(keyauth.Config{
|
return keyauth.New(keyauth.Config{
|
||||||
Validator: AuthValidateAPIKey,
|
Validator: func(c *fiber.Ctx, key string) (bool, error) {
|
||||||
|
// we use a wrapper closure to be able to forward the db object
|
||||||
|
formuser, err := AuthValidateOnetimeKey(c, key, db)
|
||||||
|
|
||||||
|
// incoming apicontext matches a form id, accept it
|
||||||
|
if err == nil {
|
||||||
|
Log("Incoming API Context equals formuser: %t, id: %s", formuser, key)
|
||||||
|
return formuser, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// nope, we need to check against regular configured apicontexts
|
||||||
|
return AuthValidateAPIKey(c, key)
|
||||||
|
},
|
||||||
ErrorHandler: AuthErrHandler,
|
ErrorHandler: AuthErrHandler,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -128,7 +162,7 @@ func SetupServer(conf *cfg.Config) *fiber.App {
|
|||||||
StrictRouting: true,
|
StrictRouting: true,
|
||||||
Immutable: true,
|
Immutable: true,
|
||||||
Prefork: conf.Prefork,
|
Prefork: conf.Prefork,
|
||||||
ServerHeader: "Cenophane Server",
|
ServerHeader: "ephemerup Server",
|
||||||
AppName: conf.AppName,
|
AppName: conf.AppName,
|
||||||
BodyLimit: conf.BodyLimit,
|
BodyLimit: conf.BodyLimit,
|
||||||
Network: conf.Network,
|
Network: conf.Network,
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import (
|
|||||||
//"github.com/alecthomas/repr"
|
//"github.com/alecthomas/repr"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/tlinden/cenophane/cfg"
|
"github.com/tlinden/ephemerup/cfg"
|
||||||
"github.com/tlinden/cenophane/common"
|
"github.com/tlinden/ephemerup/common"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -34,7 +35,7 @@ type SetContext struct {
|
|||||||
Apicontext string `json:"apicontext" form:"apicontext"`
|
Apicontext string `json:"apicontext" form:"apicontext"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func FilePut(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
||||||
// supports upload of multiple files with:
|
// supports upload of multiple files with:
|
||||||
//
|
//
|
||||||
// curl -X POST localhost:8080/putfile \
|
// curl -X POST localhost:8080/putfile \
|
||||||
@@ -62,10 +63,10 @@ func FilePut(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// init upload obj
|
// init upload obj
|
||||||
entry := &common.Upload{Id: id, Uploaded: common.Timestamp{Time: time.Now()}}
|
entry := &common.Upload{Id: id, Created: common.Timestamp{Time: time.Now()}}
|
||||||
|
|
||||||
// retrieve the API Context name from the session
|
// retrieve the API Context name from the session
|
||||||
apicontext, err := GetApicontext(c)
|
apicontext, err := SessionGetApicontext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return JsonStatus(c, fiber.StatusInternalServerError,
|
return JsonStatus(c, fiber.StatusInternalServerError,
|
||||||
"Unable to initialize session store from context: "+err.Error())
|
"Unable to initialize session store from context: "+err.Error())
|
||||||
@@ -106,6 +107,7 @@ func FilePut(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
|||||||
"Could not process uploaded file[s]: "+err.Error())
|
"Could not process uploaded file[s]: "+err.Error())
|
||||||
}
|
}
|
||||||
entry.File = Newfilename
|
entry.File = Newfilename
|
||||||
|
entry.Url = returnUrl
|
||||||
|
|
||||||
Log("Now serving %s from %s/%s", returnUrl, cfg.StorageDir, id)
|
Log("Now serving %s from %s/%s", returnUrl, cfg.StorageDir, id)
|
||||||
Log("Expire set to: %s", entry.Expire)
|
Log("Expire set to: %s", entry.Expire)
|
||||||
@@ -115,14 +117,41 @@ func FilePut(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.Message = "Download url: " + returnUrl
|
|
||||||
res.Code = fiber.StatusOK
|
res.Code = fiber.StatusOK
|
||||||
|
|
||||||
|
// ok, check if we need to remove a form, if so we do it in the
|
||||||
|
// background. delete error doesn't lead to upload failure, we
|
||||||
|
// only log it. same applies to mail notification.
|
||||||
|
formid, _ := SessionGetFormId(c)
|
||||||
|
if formid != "" {
|
||||||
|
go func() {
|
||||||
|
r, err := db.Get(apicontext, formid, common.TypeForm)
|
||||||
|
if err == nil {
|
||||||
|
if len(r.Forms) == 1 {
|
||||||
|
if r.Forms[0].Expire == "asap" {
|
||||||
|
db.Delete(apicontext, formid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// email notification to form creator
|
||||||
|
if r.Forms[0].Notify != "" {
|
||||||
|
body := fmt.Sprintf("Upload is available under: %s", returnUrl)
|
||||||
|
subject := fmt.Sprintf("Upload form %s has been used", formid)
|
||||||
|
err := Sendmail(cfg, r.Forms[0].Notify, body, subject)
|
||||||
|
if err != nil {
|
||||||
|
Log("Failed to send mail: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(res)
|
return c.Status(fiber.StatusOK).JSON(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FileGet(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) error {
|
func UploadFetch(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) error {
|
||||||
// deliver a file and delete it if expire is set to asap
|
// 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
|
// we ignore c.Params("file"), cause it may be malign. Also we've
|
||||||
@@ -133,18 +162,23 @@ func FileGet(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// retrieve the API Context name from the session
|
// retrieve the API Context name from the session
|
||||||
apicontext, err := GetApicontext(c)
|
apicontext, err := SessionGetApicontext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return JsonStatus(c, fiber.StatusInternalServerError,
|
return JsonStatus(c, fiber.StatusInternalServerError,
|
||||||
"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)
|
||||||
|
|
||||||
@@ -173,7 +207,7 @@ func FileGet(c *fiber.Ctx, cfg *cfg.Config, db *Db, shallExpire ...bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// delete file, id dir and db entry
|
// delete file, id dir and db entry
|
||||||
func DeleteUpload(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
func UploadDelete(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
||||||
|
|
||||||
id, err := common.Untaint(c.Params("id"), cfg.RegKey)
|
id, err := common.Untaint(c.Params("id"), cfg.RegKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -187,7 +221,7 @@ func DeleteUpload(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// retrieve the API Context name from the session
|
// retrieve the API Context name from the session
|
||||||
apicontext, err := GetApicontext(c)
|
apicontext, err := SessionGetApicontext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return JsonStatus(c, fiber.StatusInternalServerError,
|
return JsonStatus(c, fiber.StatusInternalServerError,
|
||||||
"Unable to initialize session store from context: "+err.Error())
|
"Unable to initialize session store from context: "+err.Error())
|
||||||
@@ -206,7 +240,7 @@ func DeleteUpload(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 List(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 filter from body(json expected)
|
||||||
setcontext := new(SetContext)
|
setcontext := new(SetContext)
|
||||||
if err := c.BodyParser(setcontext); err != nil {
|
if err := c.BodyParser(setcontext); err != nil {
|
||||||
@@ -221,14 +255,14 @@ func List(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// retrieve the API Context name from the session
|
// retrieve the API Context name from the session
|
||||||
apicontext, err := GetApicontext(c)
|
apicontext, err := SessionGetApicontext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return JsonStatus(c, fiber.StatusInternalServerError,
|
return JsonStatus(c, fiber.StatusInternalServerError,
|
||||||
"Unable to initialize session store from context: "+err.Error())
|
"Unable to initialize session store from context: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// get list
|
// get list
|
||||||
uploads, err := db.List(apicontext, filter)
|
uploads, err := db.List(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())
|
||||||
@@ -242,7 +276,7 @@ func List(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns just one upload obj + error code, no post processing by server
|
// returns just one upload obj + error code, no post processing by server
|
||||||
func Describe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
func UploadDescribe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
||||||
id, err := common.Untaint(c.Params("id"), cfg.RegKey)
|
id, err := common.Untaint(c.Params("id"), cfg.RegKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return JsonStatus(c, fiber.StatusForbidden,
|
return JsonStatus(c, fiber.StatusForbidden,
|
||||||
@@ -250,25 +284,25 @@ func Describe(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// retrieve the API Context name from the session
|
// retrieve the API Context name from the session
|
||||||
apicontext, err := GetApicontext(c)
|
apicontext, err := SessionGetApicontext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return JsonStatus(c, fiber.StatusInternalServerError,
|
return JsonStatus(c, fiber.StatusInternalServerError,
|
||||||
"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)
|
||||||
}
|
}
|
||||||
25
api/utils.go
25
api/utils.go
@@ -20,8 +20,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/tlinden/cenophane/cfg"
|
"github.com/tlinden/ephemerup/cfg"
|
||||||
"github.com/tlinden/cenophane/common"
|
"github.com/tlinden/ephemerup/common"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ func Ts() string {
|
|||||||
|
|
||||||
If there's no apicontext in the session, assume unauth user, return ""
|
If there's no apicontext in the session, assume unauth user, return ""
|
||||||
*/
|
*/
|
||||||
func GetApicontext(c *fiber.Ctx) (string, error) {
|
func SessionGetApicontext(c *fiber.Ctx) (string, error) {
|
||||||
sess, err := Sessionstore.Get(c)
|
sess, err := Sessionstore.Get(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Unable to initialize session store from context: " + err.Error())
|
return "", fmt.Errorf("Unable to initialize session store from context: " + err.Error())
|
||||||
@@ -69,6 +69,25 @@ func GetApicontext(c *fiber.Ctx) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Retrieve the formid (aka onetime api key) from the session. It is
|
||||||
|
configured if an upload request has been successfully authenticated
|
||||||
|
using a onetime key.
|
||||||
|
*/
|
||||||
|
func SessionGetFormId(c *fiber.Ctx) (string, error) {
|
||||||
|
sess, err := Sessionstore.Get(c)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Unable to initialize session store from context: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
formid := sess.Get("formid")
|
||||||
|
if formid != nil {
|
||||||
|
return formid.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Calculate if time is up based on start time.Time and
|
Calculate if time is up based on start time.Time and
|
||||||
duration. Returns true if time is expired. Start time comes from
|
duration. Returns true if time is expired. Start time comes from
|
||||||
|
|||||||
@@ -32,8 +32,16 @@ type Apicontext struct {
|
|||||||
Key string `koanf:"key"`
|
Key string `koanf:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Mailsettings struct {
|
||||||
|
Server string `koanf:"server"`
|
||||||
|
Port string `koanf:"port"`
|
||||||
|
From string `koanf:"from"`
|
||||||
|
Password string `koanf:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
// holds the whole configs, filled by commandline flags, env and config file
|
// holds the whole configs, filled by commandline flags, env and config file
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// Flags+config file settings
|
||||||
ApiPrefix string `koanf:"apiprefix"` // path prefix
|
ApiPrefix string `koanf:"apiprefix"` // path prefix
|
||||||
Debug bool `koanf:"debug"`
|
Debug bool `koanf:"debug"`
|
||||||
Listen string `koanf:"listen"` // [host]:port
|
Listen string `koanf:"listen"` // [host]:port
|
||||||
@@ -42,6 +50,7 @@ type Config struct {
|
|||||||
DbFile string `koanf:"dbfile"`
|
DbFile string `koanf:"dbfile"`
|
||||||
Super string `koanf:"super"` // the apicontext which has all permissions
|
Super string `koanf:"super"` // the apicontext which has all permissions
|
||||||
Frontpage string `koanf:"frontpage"` // a html file
|
Frontpage string `koanf:"frontpage"` // a html file
|
||||||
|
Formpage string `koanf:"formpage"` // a html file
|
||||||
|
|
||||||
// fiber settings, see:
|
// fiber settings, see:
|
||||||
// https://docs.gofiber.io/api/fiber/#config
|
// https://docs.gofiber.io/api/fiber/#config
|
||||||
@@ -55,10 +64,14 @@ type Config struct {
|
|||||||
// only settable via config
|
// only settable via config
|
||||||
Apicontexts []Apicontext `koanf:"apicontext"`
|
Apicontexts []Apicontext `koanf:"apicontext"`
|
||||||
|
|
||||||
|
// smtp settings
|
||||||
|
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
|
||||||
CleanInterval time.Duration
|
CleanInterval time.Duration
|
||||||
DefaultExpire int
|
DefaultExpire int
|
||||||
}
|
}
|
||||||
@@ -70,7 +83,7 @@ func Getversion() string {
|
|||||||
// main branch, and cfg.Version-$branch-$lastcommit-$date on
|
// main branch, and cfg.Version-$branch-$lastcommit-$date on
|
||||||
// development branch
|
// development branch
|
||||||
|
|
||||||
return fmt.Sprintf("This is cenophane server version %s", VERSION)
|
return fmt.Sprintf("This is ephemerup server version %s", VERSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetVersion() string {
|
func (c *Config) GetVersion() string {
|
||||||
@@ -105,6 +118,8 @@ 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-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
|
||||||
|
|||||||
102
cmd/formtemplate.go
Normal file
102
cmd/formtemplate.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
const formtemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- -*-web-*- -->
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="description" content="upload form" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>File upload form</title>
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet" integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h4>Upload form {{ .Id }}</h4>
|
||||||
|
<!-- Response -->
|
||||||
|
<div class="statusMsg"></div>
|
||||||
|
|
||||||
|
<!-- File upload form -->
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<form id="UploadForm" enctype="multipart/form-data" action="/v1/uploads" method="POST">
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<p>
|
||||||
|
Use this form to upload one or more files. The creator of the form will automatically get notified.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label for="file" class="col-sm-2 col-form-label">Select</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="file" class="form-control" id="file" name="uploads[]" multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label for="display" class="col-sm-2 col-form-label">Selected Files</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<!-- <input type="textara" class="form-control" id="upload-file-info" readonly>-->
|
||||||
|
<div id="upload-file-info"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" name="submit" class="btn btn-success submitBtn" value="Upload"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/js/bootstrap.bundle.min.js"
|
||||||
|
integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N" crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function(){
|
||||||
|
// Submit form data via Ajax
|
||||||
|
$("#UploadForm").on('submit', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/v1/uploads',
|
||||||
|
data: new FormData(this),
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: false,
|
||||||
|
cache: false,
|
||||||
|
processData:false,
|
||||||
|
beforeSend: function(xhr){
|
||||||
|
$('.submitBtn').attr("disabled","disabled");
|
||||||
|
$('#UploadForm').css("opacity",".5");
|
||||||
|
xhr.setRequestHeader('Authorization', 'Bearer {{.Id}}');
|
||||||
|
},
|
||||||
|
success: function(response){
|
||||||
|
$('.statusMsg').html('');
|
||||||
|
if(response.success){
|
||||||
|
$('#UploadForm')[0].reset();
|
||||||
|
$('.statusMsg').html('<p class="alert alert-success">Your upload is available at <code>'
|
||||||
|
+response.uploads[0].url+'</code> for download</p>');
|
||||||
|
$('#UploadForm').hide();
|
||||||
|
}else{
|
||||||
|
$('.statusMsg').html('<p class="alert alert-danger">'+response.message+'</p>');
|
||||||
|
}
|
||||||
|
$('#UploadForm').css("opacity","");
|
||||||
|
$(".submitBtn").removeAttr("disabled");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#file").on('change', function() {
|
||||||
|
$("#upload-file-info").empty();
|
||||||
|
for (var i = 0; i < $(this).get(0).files.length; ++i) {
|
||||||
|
$("#upload-file-info").append('<i class="bi-check-lg"></i> ' + $(this).get(0).files[i].name + '<br>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
`
|
||||||
44
cmd/root.go
44
cmd/root.go
@@ -29,8 +29,8 @@ import (
|
|||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/alecthomas/repr"
|
"github.com/alecthomas/repr"
|
||||||
"github.com/tlinden/cenophane/api"
|
"github.com/tlinden/ephemerup/api"
|
||||||
"github.com/tlinden/cenophane/cfg"
|
"github.com/tlinden/ephemerup/cfg"
|
||||||
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
@@ -59,19 +59,20 @@ func Execute() error {
|
|||||||
f.BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging")
|
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.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.StorageDir, "storagedir", "s", "/tmp", "storage directory for uploaded files")
|
||||||
f.StringVarP(&conf.ApiPrefix, "apiprefix", "a", "/api", "API endpoint path")
|
f.StringVarP(&conf.ApiPrefix, "apiprefix", "a", "", "API endpoint path")
|
||||||
f.StringVarP(&conf.Url, "url", "u", "", "HTTP endpoint w/o 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")
|
f.StringVarP(&conf.DbFile, "dbfile", "D", "/tmp/uploads.db", "Bold database file to use")
|
||||||
f.StringVarP(&conf.Super, "super", "", "", "The API Context which has permissions on all contexts")
|
f.StringVarP(&conf.Super, "super", "", "", "The API Context which has permissions on all contexts")
|
||||||
f.StringVarP(&conf.Frontpage, "frontpage", "", "welcome to upload api, use /api enpoint!",
|
f.StringVarP(&conf.Frontpage, "frontpage", "", "welcome to upload api, use /api enpoint!",
|
||||||
"Content or filename to be displayed on / in case someone visits")
|
"Content or filename to be displayed on / in case someone visits")
|
||||||
|
f.StringVarP(&conf.Formpage, "formpage", "", "", "Content or filename to be displayed for forms (must be a go template)")
|
||||||
|
|
||||||
// server settings
|
// server settings
|
||||||
f.BoolVarP(&conf.V4only, "ipv4", "4", false, "Only listen on ipv4")
|
f.BoolVarP(&conf.V4only, "ipv4", "4", false, "Only listen on ipv4")
|
||||||
f.BoolVarP(&conf.V6only, "ipv6", "6", false, "Only listen on ipv6")
|
f.BoolVarP(&conf.V6only, "ipv6", "6", false, "Only listen on ipv6")
|
||||||
|
|
||||||
f.BoolVarP(&conf.Prefork, "prefork", "p", false, "Prefork server threads")
|
f.BoolVarP(&conf.Prefork, "prefork", "p", false, "Prefork server threads")
|
||||||
f.StringVarP(&conf.AppName, "appname", "n", "cenod "+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:])
|
f.Parse(os.Args[1:])
|
||||||
@@ -91,10 +92,10 @@ func Execute() error {
|
|||||||
configfiles = []string{configfile}
|
configfiles = []string{configfile}
|
||||||
} else {
|
} else {
|
||||||
configfiles = []string{
|
configfiles = []string{
|
||||||
"/etc/cenod.hcl", "/usr/local/etc/cenod.hcl", // unix variants
|
"/etc/ephemerupd.hcl", "/usr/local/etc/ephemerupd.hcl", // unix variants
|
||||||
filepath.Join(os.Getenv("HOME"), ".config", "cenod", "cenod.hcl"),
|
filepath.Join(os.Getenv("HOME"), ".config", "ephemerupd", "ephemerupd.hcl"),
|
||||||
filepath.Join(os.Getenv("HOME"), ".cenod"),
|
filepath.Join(os.Getenv("HOME"), ".ephemerupd"),
|
||||||
"cenod.hcl",
|
"ephemerupd.hcl",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,9 +109,9 @@ func Execute() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// env overrides config file
|
// env overrides config file
|
||||||
k.Load(env.Provider("CENOD_", ".", func(s string) string {
|
k.Load(env.Provider("EPHEMERUPD_", ".", func(s string) string {
|
||||||
return strings.Replace(strings.ToLower(
|
return strings.Replace(strings.ToLower(
|
||||||
strings.TrimPrefix(s, "CENOD_")), "_", ".", -1)
|
strings.TrimPrefix(s, "EPHEMERUPD_")), "_", ".", -1)
|
||||||
}), nil)
|
}), nil)
|
||||||
|
|
||||||
// command line overrides env
|
// command line overrides env
|
||||||
@@ -142,6 +143,23 @@ func Execute() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Formpage?
|
||||||
|
if conf.Formpage != "" {
|
||||||
|
if _, err := os.Stat(conf.Formpage); err == nil {
|
||||||
|
// it's a filename, try to use it
|
||||||
|
content, err := ioutil.ReadFile(conf.Formpage)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("error loading config: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace the filename
|
||||||
|
conf.Formpage = string(content)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// use builtin default
|
||||||
|
conf.Formpage = formtemplate
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case ShowVersion:
|
case ShowVersion:
|
||||||
fmt.Println(cfg.Getversion())
|
fmt.Println(cfg.Getversion())
|
||||||
@@ -157,11 +175,11 @@ func Execute() error {
|
|||||||
|
|
||||||
Multiple env vars are supported in this format:
|
Multiple env vars are supported in this format:
|
||||||
|
|
||||||
CENOD_CONTEXT_$(NAME)="<context>:<key>"
|
EPHEMERUPD_CONTEXT_$(NAME)="<context>:<key>"
|
||||||
|
|
||||||
eg:
|
eg:
|
||||||
|
|
||||||
CENOD_CONTEXT_SUPPORT="support:tymag-fycyh-gymof-dysuf-doseb-puxyx"
|
EPHEMERUPD_CONTEXT_SUPPORT="support:tymag-fycyh-gymof-dysuf-doseb-puxyx"
|
||||||
^^^^^^^- doesn't matter.
|
^^^^^^^- doesn't matter.
|
||||||
|
|
||||||
Modifies cfg.Config directly
|
Modifies cfg.Config directly
|
||||||
@@ -171,7 +189,7 @@ func GetApicontextsFromEnv(conf *cfg.Config) {
|
|||||||
|
|
||||||
for _, envvar := range os.Environ() {
|
for _, envvar := range os.Environ() {
|
||||||
pair := strings.SplitN(envvar, "=", 2)
|
pair := strings.SplitN(envvar, "=", 2)
|
||||||
if strings.HasPrefix(pair[0], "CENOD_CONTEXT_") {
|
if strings.HasPrefix(pair[0], "EPHEMERUPD_CONTEXT_") {
|
||||||
c := strings.SplitN(pair[1], ":", 2)
|
c := strings.SplitN(pair[1], ":", 2)
|
||||||
if len(c) == 2 {
|
if len(c) == 2 {
|
||||||
contexts = append(contexts, cfg.Apicontext{Context: c[0], Key: c[1]})
|
contexts = append(contexts, cfg.Apicontext{Context: c[0], Key: c[1]})
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module github.com/tlinden/cenophane/common
|
module github.com/tlinden/ephemerup/common
|
||||||
|
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|||||||
140
common/types.go
140
common/types.go
@@ -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,20 +29,137 @@ 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"`
|
||||||
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
|
||||||
Uploaded Timestamp `json:"uploaded"`
|
Created Timestamp `json:"uploaded"`
|
||||||
Context string `json:"context"`
|
Context string `json:"context"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
// Note the dual use of the Id: it will be used as onetime api key
|
||||||
|
// from generated upload forms and stored in the session store so
|
||||||
|
// 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
|
||||||
|
// asap)
|
||||||
|
Id string `json:"id"`
|
||||||
|
Expire string `json:"expire"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Created Timestamp `json:"uploaded"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Notify string `json:"notify"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Response methods
|
||||||
|
*/
|
||||||
|
func (r *Response) Append(entry Dbentry) {
|
||||||
|
switch entry.(type) {
|
||||||
|
case *Upload:
|
||||||
|
r.Uploads = append(r.Uploads, entry.(*Upload))
|
||||||
|
case Upload:
|
||||||
|
r.Uploads = append(r.Uploads, entry.(*Upload))
|
||||||
|
case Form:
|
||||||
|
r.Forms = append(r.Forms, entry.(*Form))
|
||||||
|
case *Form:
|
||||||
|
r.Forms = append(r.Forms, entry.(*Form))
|
||||||
|
default:
|
||||||
|
panic("unknown type!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
version: "3.9"
|
version: "3.9"
|
||||||
services:
|
services:
|
||||||
cenophane:
|
ephemerup:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|||||||
@@ -17,3 +17,10 @@ apicontext = [
|
|||||||
|
|
||||||
# this is the root context with all permissions
|
# this is the root context with all permissions
|
||||||
super = "root"
|
super = "root"
|
||||||
|
|
||||||
|
mail = {
|
||||||
|
server = "localhost"
|
||||||
|
port = "25"
|
||||||
|
from = "root@localhost"
|
||||||
|
password = ""
|
||||||
|
}
|
||||||
8
go.mod
8
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module github.com/tlinden/cenophane
|
module github.com/tlinden/ephemerup
|
||||||
|
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
@@ -13,12 +13,13 @@ require (
|
|||||||
github.com/knadh/koanf/providers/posflag v0.1.0
|
github.com/knadh/koanf/providers/posflag v0.1.0
|
||||||
github.com/knadh/koanf/v2 v2.0.0
|
github.com/knadh/koanf/v2 v2.0.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/tlinden/cenophane/common v0.0.0-00010101000000-000000000000
|
github.com/tlinden/ephemerup/common v0.0.0-00010101000000-000000000000
|
||||||
go.etcd.io/bbolt v1.3.7
|
go.etcd.io/bbolt v1.3.7
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.15.9 // indirect
|
github.com/klauspost/compress v1.15.9 // indirect
|
||||||
@@ -26,6 +27,7 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
|
github.com/maxatome/go-testdeep v1.13.0 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
@@ -40,4 +42,4 @@ require (
|
|||||||
golang.org/x/sys v0.4.0 // indirect
|
golang.org/x/sys v0.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/tlinden/cenophane/common => ./common
|
replace github.com/tlinden/ephemerup/common => ./common
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -35,6 +35,8 @@ 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-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 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/maxatome/go-testdeep v1.13.0 h1:EBmRelH7MhMfPvA+0kXAeOeJUXn3mzul5NmvjLDcQZI=
|
||||||
|
github.com/maxatome/go-testdeep v1.13.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
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/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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/tlinden/cenophane/cmd"
|
"github.com/tlinden/ephemerup/cmd"
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
98
templates/formtemplate.html
Normal file
98
templates/formtemplate.html
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- -*-web-*- -->
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="description" content="upload form" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>File upload form</title>
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet" integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h4>Upload form {{ .Id }}</h4>
|
||||||
|
<!-- Response -->
|
||||||
|
<div class="statusMsg"></div>
|
||||||
|
|
||||||
|
<!-- File upload form -->
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<form id="UploadForm" enctype="multipart/form-data" action="/v1/uploads" method="POST">
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<p>
|
||||||
|
Use this form to upload one or more files. The creator of the form will automatically get notified.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label for="file" class="col-sm-2 col-form-label">Select</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="file" class="form-control" id="file" name="uploads[]" multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label for="display" class="col-sm-2 col-form-label">Selected Files</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<!-- <input type="textara" class="form-control" id="upload-file-info" readonly>-->
|
||||||
|
<div id="upload-file-info"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" name="submit" class="btn btn-success submitBtn" value="Upload"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/js/bootstrap.bundle.min.js"
|
||||||
|
integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N" crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function(){
|
||||||
|
// Submit form data via Ajax
|
||||||
|
$("#UploadForm").on('submit', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/v1/uploads',
|
||||||
|
data: new FormData(this),
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: false,
|
||||||
|
cache: false,
|
||||||
|
processData:false,
|
||||||
|
beforeSend: function(xhr){
|
||||||
|
$('.submitBtn').attr("disabled","disabled");
|
||||||
|
$('#UploadForm').css("opacity",".5");
|
||||||
|
xhr.setRequestHeader('Authorization', 'Bearer {{.Id}}');
|
||||||
|
},
|
||||||
|
success: function(response){
|
||||||
|
$('.statusMsg').html('');
|
||||||
|
if(response.success){
|
||||||
|
$('#UploadForm')[0].reset();
|
||||||
|
$('.statusMsg').html('<p class="alert alert-success">Your upload is available at <code>'
|
||||||
|
+response.uploads[0].url+'</code> for download</p>');
|
||||||
|
$('#UploadForm').hide();
|
||||||
|
}else{
|
||||||
|
$('.statusMsg').html('<p class="alert alert-danger">'+response.message+'</p>');
|
||||||
|
}
|
||||||
|
$('#UploadForm').css("opacity","");
|
||||||
|
$(".submitBtn").removeAttr("disabled");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#file").on('change', function() {
|
||||||
|
$("#upload-file-info").empty();
|
||||||
|
for (var i = 0; i < $(this).get(0).files.length; ++i) {
|
||||||
|
$("#upload-file-info").append('<i class="bi-check-lg"></i> ' + $(this).get(0).files[i].name + '<br>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ all: buildlocal
|
|||||||
|
|
||||||
|
|
||||||
buildlocal:
|
buildlocal:
|
||||||
go build -ldflags "-X 'github.com/tlinden/cenophane/upctl/cfg.VERSION=$(VERSION)'"
|
go build -ldflags "-X 'github.com/tlinden/ephemerup/upctl/cfg.VERSION=$(VERSION)'"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
./mkrel.sh $(tool) $(version)
|
./mkrel.sh $(tool) $(version)
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ type Config struct {
|
|||||||
|
|
||||||
// required to intercept requests using httpmock in tests
|
// required to intercept requests using httpmock in tests
|
||||||
Mock bool
|
Mock bool
|
||||||
|
|
||||||
|
// required for forms
|
||||||
|
Description string
|
||||||
|
Notify string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Getversion() string {
|
func Getversion() string {
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2023 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/tlinden/cenophane/upctl/cfg"
|
|
||||||
"github.com/tlinden/cenophane/upctl/lib"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DeleteCommand(conf *cfg.Config) *cobra.Command {
|
|
||||||
var deleteCmd = &cobra.Command{
|
|
||||||
Use: "delete [options] <id>",
|
|
||||||
Short: "Delete an upload",
|
|
||||||
Long: `Delete an upload identified by its id`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return errors.New("No id specified to delete!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// errors at this stage do not cause the usage to be shown
|
|
||||||
cmd.SilenceUsage = true
|
|
||||||
|
|
||||||
return lib.Delete(conf, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteCmd.Aliases = append(deleteCmd.Aliases, "rm")
|
|
||||||
deleteCmd.Aliases = append(deleteCmd.Aliases, "d")
|
|
||||||
|
|
||||||
return deleteCmd
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2023 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/tlinden/cenophane/upctl/cfg"
|
|
||||||
"github.com/tlinden/cenophane/upctl/lib"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DescribeCommand(conf *cfg.Config) *cobra.Command {
|
|
||||||
var listCmd = &cobra.Command{
|
|
||||||
Use: "describe [options] upload-id",
|
|
||||||
Long: "Show detailed informations about an upload object.",
|
|
||||||
Short: `Describe an upload.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return errors.New("No id specified to delete!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// errors at this stage do not cause the usage to be shown
|
|
||||||
cmd.SilenceUsage = true
|
|
||||||
|
|
||||||
return lib.Describe(conf, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
listCmd.Aliases = append(listCmd.Aliases, "des")
|
|
||||||
listCmd.Aliases = append(listCmd.Aliases, "info")
|
|
||||||
listCmd.Aliases = append(listCmd.Aliases, "i")
|
|
||||||
|
|
||||||
return listCmd
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2023 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/tlinden/cenophane/upctl/cfg"
|
|
||||||
"github.com/tlinden/cenophane/upctl/lib"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DownloadCommand(conf *cfg.Config) *cobra.Command {
|
|
||||||
var listCmd = &cobra.Command{
|
|
||||||
Use: "download [options] upload-id",
|
|
||||||
Long: "Download the file associated with an upload object.",
|
|
||||||
Short: `Download a file.`,
|
|
||||||
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.Download(conf, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
listCmd.Aliases = append(listCmd.Aliases, "down")
|
|
||||||
listCmd.Aliases = append(listCmd.Aliases, "get")
|
|
||||||
listCmd.Aliases = append(listCmd.Aliases, "g")
|
|
||||||
listCmd.Aliases = append(listCmd.Aliases, "fetch")
|
|
||||||
|
|
||||||
return listCmd
|
|
||||||
}
|
|
||||||
73
upctl/cmd/formcommands.go
Normal file
73
upctl/cmd/formcommands.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2023 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
//"errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/tlinden/ephemerup/upctl/cfg"
|
||||||
|
"github.com/tlinden/ephemerup/upctl/lib"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FormCommand(conf *cfg.Config) *cobra.Command {
|
||||||
|
var formCmd = &cobra.Command{
|
||||||
|
Use: "form {create|delete|modify|list}",
|
||||||
|
Short: "Form commands",
|
||||||
|
Long: `Manage upload forms.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// errors at this stage do not cause the usage to be shown
|
||||||
|
//cmd.SilenceUsage = true
|
||||||
|
if len(args) == 0 {
|
||||||
|
cmd.Help()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
formCmd.Aliases = append(formCmd.Aliases, "frm")
|
||||||
|
formCmd.Aliases = append(formCmd.Aliases, "f")
|
||||||
|
|
||||||
|
formCmd.AddCommand(FormCreateCommand(conf))
|
||||||
|
|
||||||
|
return formCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormCreateCommand(conf *cfg.Config) *cobra.Command {
|
||||||
|
var formCreateCmd = &cobra.Command{
|
||||||
|
Use: "create [options]",
|
||||||
|
Short: "Create a new form",
|
||||||
|
Long: `Create a new form for consumers so they can upload something.`,
|
||||||
|
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.CreateForm(os.Stdout, conf)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// options
|
||||||
|
formCreateCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)")
|
||||||
|
formCreateCmd.PersistentFlags().StringVarP(&conf.Description, "description", "D", "", "Description of the form")
|
||||||
|
formCreateCmd.PersistentFlags().StringVarP(&conf.Notify, "notify", "n", "", "Email address to get notified when consumer has uploaded files")
|
||||||
|
|
||||||
|
formCreateCmd.Aliases = append(formCreateCmd.Aliases, "add")
|
||||||
|
formCreateCmd.Aliases = append(formCreateCmd.Aliases, "+")
|
||||||
|
|
||||||
|
return formCreateCmd
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
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/cenophane/upctl/cfg"
|
|
||||||
"github.com/tlinden/cenophane/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")
|
|
||||||
|
|
||||||
listCmd.Aliases = append(listCmd.Aliases, "ls")
|
|
||||||
listCmd.Aliases = append(listCmd.Aliases, "l")
|
|
||||||
|
|
||||||
return listCmd
|
|
||||||
}
|
|
||||||
145
upctl/cmd/maincommands.go
Normal file
145
upctl/cmd/maincommands.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2023 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/tlinden/ephemerup/upctl/cfg"
|
||||||
|
"github.com/tlinden/ephemerup/upctl/lib"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UploadCommand(conf *cfg.Config) *cobra.Command {
|
||||||
|
var uploadCmd = &cobra.Command{
|
||||||
|
Use: "upload [options] [file ..]",
|
||||||
|
Short: "Upload files",
|
||||||
|
Long: `Upload files to an upload api.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("No files specified to upload!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// errors at this stage do not cause the usage to be shown
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
return lib.UploadFiles(os.Stdout, conf, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// options
|
||||||
|
uploadCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)")
|
||||||
|
|
||||||
|
uploadCmd.Aliases = append(uploadCmd.Aliases, "up")
|
||||||
|
uploadCmd.Aliases = append(uploadCmd.Aliases, "u")
|
||||||
|
|
||||||
|
return uploadCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
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(os.Stdout, conf, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// options
|
||||||
|
listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context")
|
||||||
|
|
||||||
|
listCmd.Aliases = append(listCmd.Aliases, "ls")
|
||||||
|
listCmd.Aliases = append(listCmd.Aliases, "l")
|
||||||
|
|
||||||
|
return listCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteCommand(conf *cfg.Config) *cobra.Command {
|
||||||
|
var deleteCmd = &cobra.Command{
|
||||||
|
Use: "delete [options] <id>",
|
||||||
|
Short: "Delete an upload",
|
||||||
|
Long: `Delete an upload identified by its id`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("No id specified to delete!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// errors at this stage do not cause the usage to be shown
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
return lib.Delete(os.Stdout, conf, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCmd.Aliases = append(deleteCmd.Aliases, "rm")
|
||||||
|
deleteCmd.Aliases = append(deleteCmd.Aliases, "d")
|
||||||
|
|
||||||
|
return deleteCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func DescribeCommand(conf *cfg.Config) *cobra.Command {
|
||||||
|
var listCmd = &cobra.Command{
|
||||||
|
Use: "describe [options] upload-id",
|
||||||
|
Long: "Show detailed informations about an upload object.",
|
||||||
|
Short: `Describe an upload.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("No id specified to delete!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// errors at this stage do not cause the usage to be shown
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
return lib.Describe(os.Stdout, conf, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
listCmd.Aliases = append(listCmd.Aliases, "des")
|
||||||
|
listCmd.Aliases = append(listCmd.Aliases, "info")
|
||||||
|
listCmd.Aliases = append(listCmd.Aliases, "i")
|
||||||
|
|
||||||
|
return listCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadCommand(conf *cfg.Config) *cobra.Command {
|
||||||
|
var listCmd = &cobra.Command{
|
||||||
|
Use: "download [options] upload-id",
|
||||||
|
Long: "Download the file associated with an upload object.",
|
||||||
|
Short: `Download a file.`,
|
||||||
|
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.Download(os.Stdout, conf, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
listCmd.Aliases = append(listCmd.Aliases, "down")
|
||||||
|
listCmd.Aliases = append(listCmd.Aliases, "get")
|
||||||
|
listCmd.Aliases = append(listCmd.Aliases, "g")
|
||||||
|
listCmd.Aliases = append(listCmd.Aliases, "fetch")
|
||||||
|
|
||||||
|
return listCmd
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/tlinden/cenophane/upctl/cfg"
|
"github.com/tlinden/ephemerup/upctl/cfg"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -92,6 +92,7 @@ func Execute() {
|
|||||||
rootCmd.AddCommand(DeleteCommand(&conf))
|
rootCmd.AddCommand(DeleteCommand(&conf))
|
||||||
rootCmd.AddCommand(DescribeCommand(&conf))
|
rootCmd.AddCommand(DescribeCommand(&conf))
|
||||||
rootCmd.AddCommand(DownloadCommand(&conf))
|
rootCmd.AddCommand(DownloadCommand(&conf))
|
||||||
|
rootCmd.AddCommand(FormCommand(&conf))
|
||||||
|
|
||||||
err := rootCmd.Execute()
|
err := rootCmd.Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2023 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/tlinden/cenophane/upctl/cfg"
|
|
||||||
"github.com/tlinden/cenophane/upctl/lib"
|
|
||||||
)
|
|
||||||
|
|
||||||
func UploadCommand(conf *cfg.Config) *cobra.Command {
|
|
||||||
var uploadCmd = &cobra.Command{
|
|
||||||
Use: "upload [options] [file ..]",
|
|
||||||
Short: "Upload files",
|
|
||||||
Long: `Upload files to an upload api.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return errors.New("No files specified to upload!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// errors at this stage do not cause the usage to be shown
|
|
||||||
cmd.SilenceUsage = true
|
|
||||||
|
|
||||||
return lib.UploadFiles(conf, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// options
|
|
||||||
uploadCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)")
|
|
||||||
|
|
||||||
uploadCmd.Aliases = append(uploadCmd.Aliases, "up")
|
|
||||||
uploadCmd.Aliases = append(uploadCmd.Aliases, "u")
|
|
||||||
|
|
||||||
return uploadCmd
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module github.com/tlinden/cenophane/upctl
|
module github.com/tlinden/ephemerup/upctl
|
||||||
|
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
@@ -10,10 +10,11 @@ require (
|
|||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.6.1
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.15.0
|
github.com/spf13/viper v1.15.0
|
||||||
github.com/tlinden/cenophane/common v0.0.0-00010101000000-000000000000
|
github.com/tlinden/ephemerup/common v0.0.0-00010101000000-000000000000
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/repr v0.2.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
@@ -50,4 +51,4 @@ require (
|
|||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/tlinden/cenophane/common => ../common
|
replace github.com/tlinden/ephemerup/common => ../common
|
||||||
|
|||||||
@@ -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=
|
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/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/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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ import (
|
|||||||
"github.com/imroc/req/v3"
|
"github.com/imroc/req/v3"
|
||||||
"github.com/jarcoal/httpmock"
|
"github.com/jarcoal/httpmock"
|
||||||
"github.com/schollz/progressbar/v3"
|
"github.com/schollz/progressbar/v3"
|
||||||
"github.com/tlinden/cenophane/common"
|
"github.com/tlinden/ephemerup/common"
|
||||||
"github.com/tlinden/cenophane/upctl/cfg"
|
"github.com/tlinden/ephemerup/upctl/cfg"
|
||||||
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -49,8 +50,11 @@ type ListParams struct {
|
|||||||
Apicontext string `json:"apicontext"`
|
Apicontext string `json:"apicontext"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const Maxwidth = 10
|
const Maxwidth = 12
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a new request object for outgoing queries
|
||||||
|
*/
|
||||||
func Setup(c *cfg.Config, path string) *Request {
|
func Setup(c *cfg.Config, path string) *Request {
|
||||||
client := req.C()
|
client := req.C()
|
||||||
if c.Debug {
|
if c.Debug {
|
||||||
@@ -86,9 +90,12 @@ func Setup(c *cfg.Config, path string) *Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Request{Url: c.Endpoint + path, R: R}
|
return &Request{Url: c.Endpoint + path, R: R}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Iterate over args, considering the elements are filenames, and add
|
||||||
|
them to the request.
|
||||||
|
*/
|
||||||
func GatherFiles(rq *Request, args []string) error {
|
func GatherFiles(rq *Request, args []string) error {
|
||||||
for _, file := range args {
|
for _, file := range args {
|
||||||
info, err := os.Stat(file)
|
info, err := os.Stat(file)
|
||||||
@@ -120,9 +127,48 @@ func GatherFiles(rq *Request, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadFiles(c *cfg.Config, args []string) error {
|
/*
|
||||||
|
Check HTTP Response Code and validate JSON status output, if
|
||||||
|
any. Turns'em into a regular error
|
||||||
|
*/
|
||||||
|
func HandleResponse(c *cfg.Config, resp *req.Response) error {
|
||||||
|
// we expect a json response, extract the error, if any
|
||||||
|
r := Response{}
|
||||||
|
|
||||||
|
if c.Debug {
|
||||||
|
trace := resp.Request.TraceInfo()
|
||||||
|
fmt.Println(trace.Blame())
|
||||||
|
fmt.Println("----------")
|
||||||
|
fmt.Println(trace)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(resp.String()), &r); err != nil {
|
||||||
|
// text output!
|
||||||
|
r.Message = resp.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.IsSuccessState() {
|
||||||
|
return fmt.Errorf("bad response: %s (%s)", resp.Status, r.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.Success {
|
||||||
|
if len(r.Message) == 0 {
|
||||||
|
if resp.Err != nil {
|
||||||
|
return resp.Err
|
||||||
|
} else {
|
||||||
|
return errors.New("Unknown error")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New(r.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "/file/")
|
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 {
|
||||||
@@ -150,47 +196,15 @@ func UploadFiles(c *cfg.Config, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return RespondExtended(resp)
|
if err := HandleResponse(c, resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return RespondExtended(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleResponse(c *cfg.Config, resp *req.Response) error {
|
func List(w io.Writer, c *cfg.Config, args []string) error {
|
||||||
// we expect a json response, extract the error, if any
|
rq := Setup(c, "/uploads")
|
||||||
r := Response{}
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(resp.String()), &r); err != nil {
|
|
||||||
// text output!
|
|
||||||
r.Message = resp.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Debug {
|
|
||||||
trace := resp.Request.TraceInfo()
|
|
||||||
fmt.Println(trace.Blame())
|
|
||||||
fmt.Println("----------")
|
|
||||||
fmt.Println(trace)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !r.Success {
|
|
||||||
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
|
|
||||||
if r.Message != "" {
|
|
||||||
fmt.Println(r.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func List(c *cfg.Config, args []string) error {
|
|
||||||
rq := Setup(c, "/list/")
|
|
||||||
|
|
||||||
params := &ListParams{Apicontext: c.Apicontext}
|
params := &ListParams{Apicontext: c.Apicontext}
|
||||||
resp, err := rq.R.
|
resp, err := rq.R.
|
||||||
@@ -201,12 +215,16 @@ func List(c *cfg.Config, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return RespondTable(resp)
|
if err := HandleResponse(c, resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return UploadsRespondTable(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Delete(c *cfg.Config, args []string) error {
|
func Delete(w io.Writer, c *cfg.Config, args []string) error {
|
||||||
for _, id := range args {
|
for _, id := range args {
|
||||||
rq := Setup(c, "/file/"+id+"/")
|
rq := Setup(c, "/uploads/"+id+"/")
|
||||||
|
|
||||||
resp, err := rq.R.Delete(rq.Url)
|
resp, err := rq.R.Delete(rq.Url)
|
||||||
|
|
||||||
@@ -218,47 +236,67 @@ func Delete(c *cfg.Config, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Upload %s successfully deleted.\n", id)
|
fmt.Fprintf(w, "Upload %s successfully deleted.\n", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Describe(c *cfg.Config, args []string) error {
|
func Describe(w io.Writer, c *cfg.Config, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("No id provided!")
|
||||||
|
}
|
||||||
|
|
||||||
id := args[0] // we describe only 1 object
|
id := args[0] // we describe only 1 object
|
||||||
|
|
||||||
rq := Setup(c, "/upload/"+id+"/")
|
rq := Setup(c, "/uploads/"+id)
|
||||||
resp, err := rq.R.Get(rq.Url)
|
resp, err := rq.R.Get(rq.Url)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return RespondExtended(resp)
|
if err := HandleResponse(c, resp); err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
func Download(c *cfg.Config, args []string) error {
|
|
||||||
id := args[0]
|
return RespondExtended(w, resp)
|
||||||
|
}
|
||||||
// progres bar
|
|
||||||
bar := progressbar.Default(100)
|
func Download(w io.Writer, c *cfg.Config, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
callback := func(info req.DownloadInfo) {
|
return errors.New("No id provided!")
|
||||||
if info.Response.Response != nil {
|
}
|
||||||
bar.Add(1)
|
|
||||||
}
|
id := args[0]
|
||||||
|
|
||||||
|
rq := Setup(c, "/uploads/"+id+"/file")
|
||||||
|
|
||||||
|
if !c.Silent {
|
||||||
|
// progres bar
|
||||||
|
bar := progressbar.Default(100)
|
||||||
|
|
||||||
|
callback := func(info req.DownloadInfo) {
|
||||||
|
if info.Response.Response != nil {
|
||||||
|
bar.Add(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rq.R.SetDownloadCallback(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
rq := Setup(c, "/file/"+id+"/")
|
|
||||||
resp, err := rq.R.
|
resp, err := rq.R.
|
||||||
SetOutputFile(id).
|
SetOutputFile(id).
|
||||||
SetDownloadCallback(callback).
|
|
||||||
Get(rq.Url)
|
Get(rq.Url)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !resp.IsSuccessState() {
|
||||||
|
return fmt.Errorf("bad response: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
_, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
|
_, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.Remove(id)
|
os.Remove(id)
|
||||||
@@ -278,7 +316,34 @@ func Download(c *cfg.Config, args []string) error {
|
|||||||
return fmt.Errorf("\nUnable to rename file: " + err.Error())
|
return fmt.Errorf("\nUnable to rename file: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s successfully downloaded to file %s.", id, cleanfilename)
|
fmt.Fprintf(w, "%s successfully downloaded to file %s.", id, cleanfilename)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/**** Forms stuff ****/
|
||||||
|
func CreateForm(w io.Writer, c *cfg.Config) error {
|
||||||
|
// setup url, req.Request, timeout handling etc
|
||||||
|
rq := Setup(c, "/forms")
|
||||||
|
|
||||||
|
// actual post w/ settings
|
||||||
|
resp, err := rq.R.
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"expire": c.Expire,
|
||||||
|
"description": c.Description,
|
||||||
|
"notify": c.Notify,
|
||||||
|
}).
|
||||||
|
Post(rq.Url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := HandleResponse(c, resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return RespondExtended(w, resp)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,37 +19,85 @@ package lib
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
//"github.com/alecthomas/repr"
|
//"github.com/alecthomas/repr"
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jarcoal/httpmock"
|
"github.com/jarcoal/httpmock"
|
||||||
"github.com/tlinden/cenophane/upctl/cfg"
|
"github.com/tlinden/ephemerup/upctl/cfg"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
const endpoint string = "http://localhost:8080/api/v1"
|
const endpoint string = "http://localhost:8080/v1"
|
||||||
|
|
||||||
type Unit struct {
|
type Unit struct {
|
||||||
name string
|
name string
|
||||||
apikey string // set to something else than "token" to fail auth
|
apikey string // set to something else than "token" to fail auth
|
||||||
wantfail bool // true: expect to fail
|
wantfail bool // true: expect to fail
|
||||||
files []string // path relative to ./t/
|
files []string // path relative to ./t/
|
||||||
sendcode int // for httpmock
|
expect string // regex used to parse the output
|
||||||
sendjson string // struct to respond with
|
|
||||||
route string // dito
|
sendcode int // for httpmock
|
||||||
method string // method to use
|
sendjson string // struct to respond with
|
||||||
|
sendfile string // bare file content to be sent
|
||||||
|
route string // dito
|
||||||
|
method string // method to use
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulate our cenophane server
|
// simulate our ephemerup server
|
||||||
func Intercept(tt Unit) {
|
func Intercept(tt Unit) {
|
||||||
httpmock.RegisterResponder(tt.method, endpoint+tt.route,
|
httpmock.RegisterResponder(tt.method, endpoint+tt.route,
|
||||||
func(request *http.Request) (*http.Response, error) {
|
func(request *http.Request) (*http.Response, error) {
|
||||||
respbody := fmt.Sprintf(tt.sendjson)
|
var resp *http.Response
|
||||||
resp := httpmock.NewStringResponse(tt.sendcode, respbody)
|
|
||||||
resp.Header.Set("Content-Type", "application/json; charset=utf-8")
|
if tt.sendfile != "" {
|
||||||
|
// simulate a file download
|
||||||
|
content, err := ioutil.ReadFile(tt.sendfile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // should not happen
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, err := os.Stat(tt.sendfile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // should not happen as well
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = httpmock.NewStringResponse(tt.sendcode, string(content))
|
||||||
|
resp.Header.Set("Content-Type", "text/markdown; charset=utf-8")
|
||||||
|
resp.Header.Set("Content-Length", strconv.Itoa(int(stat.Size())))
|
||||||
|
resp.Header.Set("Content-Disposition", "attachment; filename='t1'")
|
||||||
|
} else {
|
||||||
|
// simulate JSON response
|
||||||
|
resp = httpmock.NewStringResponse(tt.sendcode, tt.sendjson)
|
||||||
|
resp.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// execute the actual test
|
||||||
|
func Check(t *testing.T, tt Unit, w *bytes.Buffer, err error) {
|
||||||
|
testname := fmt.Sprintf("%s-%t", tt.name, tt.wantfail)
|
||||||
|
|
||||||
|
if err != nil && !tt.wantfail {
|
||||||
|
t.Errorf("%s failed! wantfail: %t, error: %s", testname, tt.wantfail, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.expect != "" {
|
||||||
|
got := strings.TrimSpace(w.String())
|
||||||
|
r := regexp.MustCompile(tt.expect)
|
||||||
|
if !r.MatchString(got) {
|
||||||
|
t.Errorf("%s failed! error: output does not match!\nexpect: %s\ngot:\n%s", testname, tt.expect, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUploadFiles(t *testing.T) {
|
func TestUploadFiles(t *testing.T) {
|
||||||
conf := &cfg.Config{
|
conf := &cfg.Config{
|
||||||
Mock: true,
|
Mock: true,
|
||||||
@@ -63,42 +111,67 @@ func TestUploadFiles(t *testing.T) {
|
|||||||
name: "upload-file",
|
name: "upload-file",
|
||||||
apikey: "token",
|
apikey: "token",
|
||||||
wantfail: false,
|
wantfail: false,
|
||||||
route: "/file/",
|
route: "/uploads",
|
||||||
sendcode: 200,
|
sendcode: 200,
|
||||||
sendjson: `{"success": true}`,
|
sendjson: `{"success": true}`,
|
||||||
files: []string{"../t/t1"}, // pwd is lib/ !
|
files: []string{"../t/t1"}, // pwd is lib/ !
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "upload-nonexistent-file",
|
name: "upload-dir",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: false,
|
||||||
|
route: "/uploads",
|
||||||
|
sendcode: 200,
|
||||||
|
sendjson: `{"success": true}`,
|
||||||
|
files: []string{"../t"}, // pwd is lib/ !
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upload-catch-nonexistent-file",
|
||||||
apikey: "token",
|
apikey: "token",
|
||||||
wantfail: true,
|
wantfail: true,
|
||||||
route: "/file/",
|
route: "/uploads",
|
||||||
sendcode: 200,
|
sendcode: 200,
|
||||||
sendjson: `{"success": false}`,
|
sendjson: `{"success": false}`,
|
||||||
files: []string{"../t/none"},
|
files: []string{"../t/none"},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "upload-unauth",
|
name: "upload-catch-no-access",
|
||||||
apikey: "token",
|
apikey: "token",
|
||||||
wantfail: true,
|
wantfail: true,
|
||||||
route: "/file/",
|
route: "/uploads",
|
||||||
sendcode: 403,
|
sendcode: 403,
|
||||||
sendjson: `{"success": false}`,
|
sendjson: `{"success": false}`,
|
||||||
files: []string{"../t/t1"},
|
files: []string{"../t/t1"},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "upload-check-output",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: false,
|
||||||
|
route: "/uploads",
|
||||||
|
sendcode: 200,
|
||||||
|
sendjson: `{"uploads":[
|
||||||
|
{
|
||||||
|
"id":"cc2c965a","expire":"asap","file":"t1","members":["t1"],
|
||||||
|
"uploaded":1679396814.890502,"context":"foo","url":""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"success":true,
|
||||||
|
"message":"Download url: http://localhost:8080/download/cc2c965a/t1",
|
||||||
|
"code":200}`,
|
||||||
|
files: []string{"../t/t1"}, // pwd is lib/ !
|
||||||
|
method: "POST",
|
||||||
|
expect: "Expire: On first access",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, unit := range tests {
|
||||||
testname := fmt.Sprintf("UploadFiles-%s-%t", tt.name, tt.wantfail)
|
var w bytes.Buffer
|
||||||
Intercept(tt)
|
Intercept(unit)
|
||||||
err := UploadFiles(conf, tt.files)
|
Check(t, unit, &w, UploadFiles(&w, conf, unit.files))
|
||||||
|
|
||||||
if err != nil && !tt.wantfail {
|
|
||||||
t.Errorf("%s failed! wantfail: %t, error: %s", testname, tt.wantfail, err.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,28 +183,227 @@ func TestList(t *testing.T) {
|
|||||||
Silent: true,
|
Silent: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
listing := `{"uploads":[{"id":"c8dh","expire":"asap","file":"t1","members":["t1"],"uploaded":1679318969.6434112,"context":"foo","url":""}],"success":true,"message":"","code":200}`
|
listing := `{"uploads":[
|
||||||
|
{
|
||||||
|
"id":"cc2c965a","expire":"asap","file":"t1","members":["t1"],
|
||||||
|
"uploaded":1679396814.890502,"context":"foo","url":""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"success":true,
|
||||||
|
"message":"",
|
||||||
|
"code":200}`
|
||||||
|
|
||||||
|
listingnoaccess := `{"success":false,"message":"invalid context","code":503}`
|
||||||
|
|
||||||
tests := []Unit{
|
tests := []Unit{
|
||||||
{
|
{
|
||||||
name: "list",
|
name: "list",
|
||||||
apikey: "token",
|
apikey: "token",
|
||||||
wantfail: false,
|
wantfail: false,
|
||||||
route: "/list/",
|
route: "/uploads",
|
||||||
sendcode: 200,
|
sendcode: 200,
|
||||||
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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list-catch-empty-json",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: true,
|
||||||
|
route: "/uploads",
|
||||||
|
sendcode: 404,
|
||||||
|
sendjson: "",
|
||||||
|
files: []string{},
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list-catch-no-access",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: true,
|
||||||
|
route: "/uploads",
|
||||||
|
sendcode: 503,
|
||||||
|
sendjson: listingnoaccess,
|
||||||
|
files: []string{},
|
||||||
|
method: "GET",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, unit := range tests {
|
||||||
testname := fmt.Sprintf("List-%s-%t", tt.name, tt.wantfail)
|
var w bytes.Buffer
|
||||||
Intercept(tt)
|
Intercept(unit)
|
||||||
err := List(conf, []string{})
|
Check(t, unit, &w, List(&w, conf, []string{}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil && !tt.wantfail {
|
func TestDescribe(t *testing.T) {
|
||||||
t.Errorf("%s failed! wantfail: %t, error: %s", testname, tt.wantfail, err.Error())
|
conf := &cfg.Config{
|
||||||
}
|
Mock: true,
|
||||||
|
Apikey: "token",
|
||||||
|
Endpoint: endpoint,
|
||||||
|
Silent: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listing := `{"uploads":[
|
||||||
|
{
|
||||||
|
"id":"cc2c965a","expire":"asap","file":"t1","members":["t1"],
|
||||||
|
"uploaded":1679396814.890502,"context":"foo","url":""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"success":true,
|
||||||
|
"message":"",
|
||||||
|
"code":200}`
|
||||||
|
|
||||||
|
listingnoaccess := `{"success":false,"message":"invalid context","code":503}`
|
||||||
|
|
||||||
|
tests := []Unit{
|
||||||
|
{
|
||||||
|
name: "describe",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: false,
|
||||||
|
route: "/uploads/",
|
||||||
|
sendcode: 200,
|
||||||
|
sendjson: listing,
|
||||||
|
files: []string{"cc2c965a"},
|
||||||
|
method: "GET",
|
||||||
|
expect: `Created: 2023-03-21 12:06:54.890501888`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "describe-catch-empty-json",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: true,
|
||||||
|
route: "/uploads/",
|
||||||
|
sendcode: 200,
|
||||||
|
sendjson: "",
|
||||||
|
files: []string{"cc2c965a"},
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "describe-catch-no-access",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: true,
|
||||||
|
route: "/uploads/",
|
||||||
|
sendcode: 503,
|
||||||
|
sendjson: listingnoaccess,
|
||||||
|
files: []string{"cc2c965a"},
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, unit := range tests {
|
||||||
|
var w bytes.Buffer
|
||||||
|
unit.route += unit.files[0]
|
||||||
|
Intercept(unit)
|
||||||
|
Check(t, unit, &w, Describe(&w, conf, unit.files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
conf := &cfg.Config{
|
||||||
|
Mock: true,
|
||||||
|
Apikey: "token",
|
||||||
|
Endpoint: endpoint,
|
||||||
|
Silent: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
listingnoaccess := `{"success":false,"message":"invalid context","code":503}`
|
||||||
|
|
||||||
|
tests := []Unit{
|
||||||
|
{
|
||||||
|
name: "delete",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: false,
|
||||||
|
route: "/uploads/",
|
||||||
|
sendcode: 200,
|
||||||
|
sendjson: `{"success":true,"message":"","code":200}`,
|
||||||
|
files: []string{"cc2c965a"},
|
||||||
|
method: "DELETE",
|
||||||
|
expect: `Upload cc2c965a successfully deleted`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete-catch-empty-json",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: true,
|
||||||
|
route: "/uploads/",
|
||||||
|
sendcode: 200,
|
||||||
|
sendjson: "",
|
||||||
|
files: []string{"cc2c965a"},
|
||||||
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete-catch-no-access",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: true,
|
||||||
|
route: "/uploads/",
|
||||||
|
sendcode: 503,
|
||||||
|
sendjson: listingnoaccess,
|
||||||
|
files: []string{"cc2c965a"},
|
||||||
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, unit := range tests {
|
||||||
|
var w bytes.Buffer
|
||||||
|
unit.route += unit.files[0] + "/"
|
||||||
|
Intercept(unit)
|
||||||
|
Check(t, unit, &w, Delete(&w, conf, unit.files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownload(t *testing.T) {
|
||||||
|
conf := &cfg.Config{
|
||||||
|
Mock: true,
|
||||||
|
Apikey: "token",
|
||||||
|
Endpoint: endpoint,
|
||||||
|
Silent: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
listingnoaccess := `{"success":false,"message":"invalid context","code":503}`
|
||||||
|
|
||||||
|
tests := []Unit{
|
||||||
|
{
|
||||||
|
name: "download",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: false,
|
||||||
|
route: "/uploads/",
|
||||||
|
sendcode: 200,
|
||||||
|
sendfile: "../t/t1",
|
||||||
|
files: []string{"cc2c965a"},
|
||||||
|
method: "GET",
|
||||||
|
expect: `cc2c965a successfully downloaded to file t1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "download-catch-empty-response",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: true,
|
||||||
|
route: "/uploads/",
|
||||||
|
sendcode: 200,
|
||||||
|
files: []string{"cc2c965a"},
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "download-catch-no-access",
|
||||||
|
apikey: "token",
|
||||||
|
wantfail: true,
|
||||||
|
route: "/uploads/",
|
||||||
|
sendcode: 503,
|
||||||
|
sendjson: listingnoaccess,
|
||||||
|
files: []string{"cc2c965a"},
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, unit := range tests {
|
||||||
|
var w bytes.Buffer
|
||||||
|
unit.route += unit.files[0] + "/file"
|
||||||
|
Intercept(unit)
|
||||||
|
Check(t, unit, &w, Download(&w, conf, unit.files))
|
||||||
|
|
||||||
|
if unit.sendfile != "" {
|
||||||
|
file := filepath.Base(unit.sendfile)
|
||||||
|
if _, err := os.Stat(file); err == nil {
|
||||||
|
os.Remove(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/imroc/req/v3"
|
"github.com/imroc/req/v3"
|
||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/tlinden/cenophane/common"
|
"github.com/tlinden/ephemerup/common"
|
||||||
"os"
|
"io"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,15 +35,17 @@ func prepareExpire(expire string, start common.Timestamp) string {
|
|||||||
case "asap":
|
case "asap":
|
||||||
return "On first access"
|
return "On first access"
|
||||||
default:
|
default:
|
||||||
return time.Unix(start.Unix()+int64(common.Duration2int(expire)), 0).Format("2006-01-02 15:04:05")
|
return time.Unix(start.Unix()+int64(common.Duration2int(expire)), 0).
|
||||||
|
Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// generic table writer
|
// generic table writer
|
||||||
func WriteTable(headers []string, data [][]string) {
|
func WriteTable(w io.Writer, headers []string, data [][]string) {
|
||||||
table := tablewriter.NewWriter(os.Stdout)
|
tableString := &strings.Builder{}
|
||||||
|
table := tablewriter.NewWriter(tableString)
|
||||||
|
|
||||||
table.SetHeader(headers)
|
table.SetHeader(headers)
|
||||||
table.AppendBulk(data)
|
table.AppendBulk(data)
|
||||||
@@ -60,76 +63,95 @@ func WriteTable(headers []string, data [][]string) {
|
|||||||
table.SetNoWhiteSpace(true)
|
table.SetNoWhiteSpace(true)
|
||||||
|
|
||||||
table.Render()
|
table.Render()
|
||||||
|
|
||||||
|
fmt.Fprintln(w, tableString.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// output like psql \x
|
/* Print output like psql \x
|
||||||
func WriteExtended(uploads *common.Uploads) {
|
|
||||||
|
Prints all Uploads and Forms which exist in common.Response,
|
||||||
|
however, we expect only one kind of them to be actually filled, so
|
||||||
|
the function can be used for forms and uploads.
|
||||||
|
*/
|
||||||
|
func WriteExtended(w io.Writer, response *common.Response) {
|
||||||
format := fmt.Sprintf("%%%ds: %%s\n", Maxwidth)
|
format := fmt.Sprintf("%%%ds: %%s\n", Maxwidth)
|
||||||
|
|
||||||
// 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 uploads.Entries {
|
for _, entry := range response.Uploads {
|
||||||
expire := prepareExpire(entry.Expire, entry.Uploaded)
|
expire := prepareExpire(entry.Expire, entry.Created)
|
||||||
fmt.Printf(format, "Id", entry.Id)
|
fmt.Fprintf(w, format, "Id", entry.Id)
|
||||||
fmt.Printf(format, "Expire", expire)
|
fmt.Fprintf(w, format, "Expire", expire)
|
||||||
fmt.Printf(format, "Context", entry.Context)
|
fmt.Fprintf(w, format, "Context", entry.Context)
|
||||||
fmt.Printf(format, "Uploaded", entry.Uploaded)
|
fmt.Fprintf(w, format, "Created", entry.Created)
|
||||||
fmt.Printf(format, "Filename", entry.File)
|
fmt.Fprintf(w, format, "Filename", entry.File)
|
||||||
fmt.Printf(format, "Url", entry.Url)
|
fmt.Fprintf(w, format, "Url", entry.Url)
|
||||||
fmt.Println()
|
fmt.Fprintln(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range response.Forms {
|
||||||
|
expire := prepareExpire(entry.Expire, entry.Created)
|
||||||
|
fmt.Fprintf(w, format, "Id", entry.Id)
|
||||||
|
fmt.Fprintf(w, format, "Expire", expire)
|
||||||
|
fmt.Fprintf(w, format, "Context", entry.Context)
|
||||||
|
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, "Url", entry.Url)
|
||||||
|
fmt.Fprintln(w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract an common.Uploads{} struct from json response
|
// extract an common.Uploads{} struct from json response
|
||||||
func GetUploadsFromResponse(resp *req.Response) (*common.Uploads, error) {
|
func GetResponse(resp *req.Response) (*common.Response, error) {
|
||||||
uploads := common.Uploads{}
|
response := common.Response{}
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(resp.String()), &uploads); err != nil {
|
if err := json.Unmarshal([]byte(resp.String()), &response); err != nil {
|
||||||
return nil, errors.New("Could not unmarshall JSON response: " + err.Error())
|
return nil, errors.New("Could not unmarshall JSON response: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !uploads.Success {
|
if !response.Success {
|
||||||
return nil, errors.New(uploads.Message)
|
return nil, errors.New(response.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &uploads, nil
|
return &response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// turn the Uploads{} struct into a table and print it
|
// turn the Uploads{} struct into a table and print it
|
||||||
func RespondTable(resp *req.Response) error {
|
func UploadsRespondTable(w io.Writer, resp *req.Response) error {
|
||||||
uploads, err := GetUploadsFromResponse(resp)
|
response, err := GetResponse(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if uploads.Message != "" {
|
if response.Message != "" {
|
||||||
fmt.Println(uploads.Message)
|
fmt.Fprintln(w, response.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tablewriter
|
// tablewriter
|
||||||
data := [][]string{}
|
data := [][]string{}
|
||||||
for _, entry := range uploads.Entries {
|
for _, entry := range response.Uploads {
|
||||||
data = append(data, []string{
|
data = append(data, []string{
|
||||||
entry.Id, entry.Expire, entry.Context, entry.Uploaded.Format("2006-01-02 15:04:05"),
|
entry.Id, entry.Expire, entry.Context, entry.Created.Format("2006-01-02 15:04:05"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteTable([]string{"ID", "EXPIRE", "CONTEXT", "UPLOADED"}, data)
|
WriteTable(w, []string{"ID", "EXPIRE", "CONTEXT", "CREATED"}, data)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// turn the Uploads{} struct into xtnd output and print it
|
// turn the Uploads{} struct into xtnd output and print it
|
||||||
func RespondExtended(resp *req.Response) error {
|
func RespondExtended(w io.Writer, resp *req.Response) error {
|
||||||
uploads, err := GetUploadsFromResponse(resp)
|
response, err := GetResponse(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if uploads.Message != "" {
|
if response.Message != "" {
|
||||||
fmt.Println(uploads.Message)
|
fmt.Fprintln(w, response.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteExtended(uploads)
|
WriteExtended(w, response)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/tlinden/cenophane/upctl/cmd"
|
"github.com/tlinden/ephemerup/upctl/cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
endpoint = "http://localhost:8080/api/v1"
|
endpoint = "http://localhost:8080/v1"
|
||||||
apikey = "970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a"
|
apikey = "970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a"
|
||||||
|
|||||||
Reference in New Issue
Block a user