mv to codeberg

This commit is contained in:
2025-12-02 22:31:21 +01:00
parent 47042e915b
commit eaa2285d04
69 changed files with 5 additions and 7091 deletions

View File

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

View File

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

View File

@@ -1,65 +0,0 @@
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
- go mod tidy
gitea_urls:
api: https://codeberg.org/api/v1
download: https://codeberg.org
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- freebsd
archives:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}_{{ .Tag }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: [zip]
- goos: linux
formats: [tar.gz,binary]
files:
- src: "*.md"
strip_parent: true
- src: Makefile.dist
dst: Makefile
wrap_in_directory: true
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
groups:
- title: Improved
regexp: '^.*?(feat|add|new)(\([[:word:]]+\))??!?:.+$'
order: 0
- title: Fixed
regexp: '^.*?(bug|fix)(\([[:word:]]+\))??!?:.+$'
order: 1
- title: Changed
order: 999
release:
header: "# Release Notes"
footer: >-
---
Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/ephemerup/compare/{{ .PreviousTag }}...{{ .Tag }})

View File

@@ -1,28 +0,0 @@
matrix:
platform:
- linux/amd64
goversion:
- 1.24
labels:
platform: ${platform}
steps:
build:
when:
event: [push]
image: golang:${goversion}
commands:
- go get
- go build
test:
when:
event: [push]
image: golang:${goversion}
commands:
- go get
- go test -v -cover ./...
- cd upctl
- go test -v -cover ./...
depends_on: [build]

View File

@@ -1,32 +0,0 @@
# https://woodpecker-ci.org/plugins/docker-buildx
# enable Package unit and go to /scip/-/packages after building to link to proj
variables:
- &repo codeberg.org/${CI_REPO_OWNER}/ephemerup
steps:
dryrun:
image: docker.io/woodpeckerci/plugin-docker-buildx:latest
settings:
dockerfile: Dockerfile
platforms: linux/amd64
dry_run: true
repo: *repo
tags: latest
when:
event: [push,manual]
publish:
image: docker.io/woodpeckerci/plugin-docker-buildx:latest
settings:
dockerfile: Dockerfile
platforms: linux/amd64
repo: *repo
registry: codeberg.org
tags: latest,${CI_COMMIT_SHA:0:8},${CI_COMMIT_TAG}
username: ${CI_REPO_OWNER}
password:
from_secret: REGISTRY_TOKEN
when:
event: [tag,manual]
branch: main

View File

@@ -1,15 +0,0 @@
# build release
labels:
platform: linux/amd64
steps:
goreleaser:
image: goreleaser/goreleaser
when:
event: [tag]
environment:
GITEA_TOKEN:
from_secret: DEPLOY_TOKEN
commands:
- goreleaser release --clean --verbose

View File

@@ -1,33 +0,0 @@
# syntax=docker/dockerfile:1 -*-python-*-
FROM golang:1.24-alpine as builder
RUN apk update
RUN apk upgrade
RUN apk add --no-cache git make bash binutils
RUN git --version
WORKDIR /work
COPY go.mod .
COPY . .
RUN go mod download
RUN make && strip ephemerupd
FROM alpine:3.22
LABEL maintainer="Uploads Author <info@daemon.de>"
RUN install -o 1001 -g 1001 -d /data
WORKDIR /app
COPY --from=builder /work/ephemerupd /app/ephemerupd
ENV EPHEMERUPD_LISTEN=:8080
ENV EPHEMERUPD_STORAGEDIR=/data
ENV EPHEMERUPD_DBFILE=/data/bbolt.db
ENV EPHEMERUPD_DEBUG=1
USER 1001:1001
EXPOSE 8080
VOLUME /data
CMD ["/app/ephemerupd"]

106
Makefile
View File

@@ -1,106 +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/>.
#
# no need to modify anything below
version = $(shell egrep "= .v" cfg/config.go | cut -d'=' -f2 | cut -d'"' -f 2)
archs = android darwin freebsd linux netbsd openbsd windows
PREFIX = /usr/local
UID = root
GID = 0
BRANCH = $(shell git branch --show-current)
COMMIT = $(shell git rev-parse --short=8 HEAD)
BUILD = $(shell date +%Y.%m.%d.%H%M%S)
VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version))
BR_MAIN := $(if $(filter $(BRANCH), main),"main","")
HAVE_POD := $(shell pod2text -h 2>/dev/null)
HAVE_LINT:= $(shell golangci-lint -h 2>/dev/null)
DAEMON := ephemerupd
CLIENT := upctl
DATE = $(shell date +%Y-%m-%d)
all: cmd/formtemplate.go lint buildlocal buildlocalctl
lint:
ifdef HAVE_LINT
golangci-lint run
endif
buildlocalctl:
make -C upctl
buildlocal:
# go build -ldflags "-X 'codeberg.org/scip/ephemerup/cfg.VERSION=$(VERSION)'" -o $(DAEMON)
CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags "-extldflags=-static -s -X 'codeberg.org/scip/ephemerup/cfg.VERSION=$(VERSION)'" -o $(DAEMON)
buildimage: clean
docker-compose --verbose build
release:
ifdef BR_MAIN
git tag -a $(version) -m "$(version) released on $(DATE)"
git push origin --tags
./mkrel.sh $(DAEMON) $(CLIENT) $(version)
gh release create $(version) --generate-notes releases/*
else
@echo "Cannot create release on branch $(BRANCH), checkout main and retry!"
endif
install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -o $(UID) -g $(GID) -m 555 $(DAEMON) $(PREFIX)/sbin/
cleanctl:
make -C upctl clean
clean: cleanctl
rm -rf releases coverage.out $(DAEMON)
test:
go test -v ./...
# bash t/test.sh
singletest:
@echo "Call like this: ''make singletest TEST=TestX1 MOD=lib"
go test -run $(TEST) codeberg.org/scip/ephemerup/$(MOD)
cover-report:
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out
show-versions: buildlocal
@echo "### ephemerupd version:"
@./ephemerupd --version
@echo
@echo "### go module versions:"
@go list -m all
@echo
@echo "### go version used for building:"
@grep -m 1 go go.mod
goupdate:
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

View File

@@ -1,18 +0,0 @@
# -*-make-*-
.PHONY: install all
tool = rpn
PREFIX = /usr/local
UID = root
GID = 0
all:
@echo "Type 'sudo make install' to install the tool."
@echo "To change prefix, type 'sudo make install PREFIX=/opt'"
install:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/share/doc
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 *.md $(PREFIX)/share/doc/

View File

@@ -2,8 +2,11 @@
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://codeberg.org/scip/ephemerup/raw/branch/main/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/codeberg.org/scip/ephemerup)](https://goreportcard.com/report/codeberg.org/scip/ephemerup)
# ephemerup
Simple standalone file upload server with expiration and commandline client.
# ephemerup - Simple standalone file upload server with expiration and commandline client.
> [!CAUTION]
> This app is now being maintained on [Codeberg](https://codeberg.org/scip/ephemerup/).
## Introduction

View File

@@ -1,128 +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 api
import (
"crypto/sha256"
"crypto/subtle"
"errors"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/keyauth/v2"
"codeberg.org/scip/ephemerup/cfg"
"codeberg.org/scip/ephemerup/common"
)
// these vars can be savely global, since they don't change ever
var (
errMissing = &fiber.Error{
Code: 403000,
Message: "Missing API key",
}
errInvalid = &fiber.Error{
Code: 403001,
Message: "Invalid API key",
}
Apikeys []cfg.Apicontext
)
// fill from server: accepted keys
func AuthSetApikeys(keys []cfg.Apicontext) {
Apikeys = keys
}
// make sure we always return JSON encoded errors
func AuthErrHandler(ctx *fiber.Ctx, err error) error {
ctx.Status(fiber.StatusForbidden)
if err == errMissing {
return ctx.JSON(errMissing)
}
return ctx.JSON(errInvalid)
}
// validator hook, 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)
if err != nil {
return false, errors.New("Could not retrieve session from Sessionstore: " + err.Error())
}
// store the result into the session, the 'formid' key tells the
// 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()
func AuthValidateAPIKey(c *fiber.Ctx, key string) (bool, error) {
// create a new session, it will be thrown away if something fails
sess, err := Sessionstore.Get(c)
if err != nil {
return false, errors.New("Unable to initialize session store from context!")
}
// if Apikeys is empty, the server works unauthenticated
// FIXME: maybe always reject?
if len(Apikeys) == 0 {
sess.Set("apicontext", "default")
if err := sess.Save(); err != nil {
return false, errors.New("Unable to save session store!")
}
return true, nil
}
// actual key comparison
for _, apicontext := range Apikeys {
hashedAPIKey := sha256.Sum256([]byte(apicontext.Key))
hashedKey := sha256.Sum256([]byte(key))
if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 {
// apikey matches, register apicontext for later use by the handlers
sess.Set("apicontext", apicontext.Context)
if err := sess.Save(); err != nil {
return false, errors.New("Unable to save session store!")
}
return true, nil
}
}
return false, keyauth.ErrMissingOrMalformedAPIKey
}

View File

@@ -1,83 +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 api
import (
"fmt"
//"github.com/alecthomas/repr"
"encoding/json"
"codeberg.org/scip/ephemerup/cfg"
"codeberg.org/scip/ephemerup/common"
bolt "go.etcd.io/bbolt"
"path/filepath"
"time"
)
func DeleteExpiredUploads(conf *cfg.Config, db *Db) error {
err := db.bolt.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(Bucket))
if bucket == nil {
return nil // nothin to delete so far
}
err := bucket.ForEach(func(id, j []byte) error {
upload := &common.Upload{}
if err := json.Unmarshal(j, &upload); err != nil {
return fmt.Errorf("unable to unmarshal json: %s", err)
}
if IsExpired(conf, upload.Created.Time, upload.Expire) {
if err := bucket.Delete([]byte(id)); err != nil {
return nil
}
cleanup(filepath.Join(conf.StorageDir, upload.Id))
Log("Cleaned up upload " + upload.Id)
}
return nil
})
return err
})
return err
}
func BackgroundCleaner(conf *cfg.Config, db *Db) chan bool {
ticker := time.NewTicker(conf.CleanInterval)
fmt.Println(conf.CleanInterval)
done := make(chan bool)
go func() {
for {
select {
case <-ticker.C:
if err := DeleteExpiredUploads(conf, db); err != nil {
Log("Failed to delete eypired uploads: %s", err.Error())
}
case <-done:
ticker.Stop()
return
}
}
}()
return done
}

233
api/db.go
View File

@@ -1,233 +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 api
import (
"fmt"
"codeberg.org/scip/ephemerup/cfg"
"codeberg.org/scip/ephemerup/common"
//"github.com/alecthomas/repr"
bolt "go.etcd.io/bbolt"
"regexp"
)
const Bucket string = "data"
// wrapper for bolt db
type Db struct {
bolt *bolt.DB
cfg *cfg.Config
}
func NewDb(c *cfg.Config) (*Db, error) {
b, err := bolt.Open(c.DbFile, 0600, nil)
db := Db{bolt: b, cfg: c}
return &db, err
}
func (db *Db) Close() {
db.bolt.Close()
}
func (db *Db) Insert(id string, entry common.Dbentry) error {
err := db.bolt.Update(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte(Bucket))
if err != nil {
return fmt.Errorf("create bucket: %s", err)
}
jsonentry, err := entry.Marshal()
if err != nil {
return fmt.Errorf("json marshalling failure: %s", err)
}
err = bucket.Put([]byte(id), []byte(jsonentry))
if err != nil {
return fmt.Errorf("insert data: %s", err)
}
return nil
})
if err != nil {
Log("DB error: %s", err.Error())
}
return err
}
func (db *Db) Delete(apicontext string, id string) error {
err := db.bolt.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(Bucket))
if bucket == nil {
return fmt.Errorf("id %s not found", id)
}
j := bucket.Get([]byte(id))
if len(j) == 0 {
return fmt.Errorf("id %s not found", id)
}
entryContext, err := common.GetContext(j)
if err != nil {
return fmt.Errorf("unable to unmarshal json: %s", err)
}
if (apicontext != "" && (db.cfg.Super == apicontext || entryContext == apicontext)) || apicontext == "" {
return bucket.Delete([]byte(id))
}
return nil
})
if err != nil {
Log("DB error: %s", err.Error())
}
return err
}
func (db *Db) List(apicontext string, filter string, query string, t int) (*common.Response, error) {
response := &common.Response{}
qr := regexp.MustCompile(query)
err := db.bolt.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(Bucket))
if bucket == nil {
return nil
}
err := bucket.ForEach(func(id, j []byte) error {
allowed := false
entry, err := common.Unmarshal(j, t)
if err != nil {
return fmt.Errorf("unable to unmarshal json: %s", err)
}
if !entry.IsType(t) {
return nil
}
var entryContext string
if t == common.TypeUpload {
entryContext = entry.(*common.Upload).Context
} else {
entryContext = entry.(*common.Form).Context
}
// check if the user is allowed to list this entry
if apicontext != "" && db.cfg.Super != apicontext {
// authenticated user but not member of super
// only return the uploads matching her context
if apicontext == entryContext {
// unless a filter OR no filter specified
if (filter != "" && entryContext == filter) || filter == "" {
allowed = true
}
}
} else {
// return all, because we operate a public service or current==super
if (filter != "" && entryContext == filter) || filter == "" {
allowed = true
}
}
if allowed {
// user is allowed to view this entry, check if she also wants to see it
if query != "" {
if entry.MatchDescription(qr) ||
entry.MatchExpire(qr) ||
entry.MatchCreated(qr) ||
entry.MatchFile(qr) {
allowed = true
} else {
allowed = false
}
}
}
if allowed {
// ok, legit and wanted
response.Append(entry)
}
return nil
})
return err // might be nil as well
})
return response, err
}
// we only return one obj here, but could return more later
// FIXME: turn the id into a filter and call (Uploads|Forms)List(), same code!
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 {
bucket := tx.Bucket([]byte(Bucket))
if bucket == nil {
return nil
}
j := bucket.Get([]byte(id))
if j == nil {
return fmt.Errorf("No upload object found with id %s", id)
}
entry, err := common.Unmarshal(j, t)
if err != nil {
return fmt.Errorf("unable to unmarshal json: %s", err)
}
var entryContext string
if t == common.TypeUpload {
entryContext = entry.(*common.Upload).Context
} else {
entryContext = entry.(*common.Form).Context
}
if (apicontext != "" && (db.cfg.Super == apicontext || entryContext == apicontext)) || apicontext == "" {
// allowed if no context (public or download)
// or if context matches or if context==super
response.Append(entry)
}
return nil
})
return response, err
}
// a wrapper around Lookup() which extracts the 1st upload, if any
func (db *Db) Lookup(apicontext string, id string, t int) (*common.Response, error) {
response, err := db.Get(apicontext, id, t)
if err != nil {
// non existent db entry with that id, or other db error, see logs
return &common.Response{}, fmt.Errorf("No upload object found with id %s", id)
}
if len(response.Uploads) == 0 {
return &common.Response{}, fmt.Errorf("No upload object found with id %s", id)
}
return response, nil
}

View File

@@ -1,239 +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 api
import (
//"github.com/alecthomas/repr"
"os"
"testing"
"time"
"codeberg.org/scip/ephemerup/cfg"
"codeberg.org/scip/ephemerup/common"
"github.com/maxatome/go-testdeep/td"
)
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.Error("expected: &Db{}, got err: " + err.Error())
}
if err == nil && tt.wantfail {
t.Error("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
query 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{}, Type: common.TypeUpload},
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{}, Type: common.TypeForm},
},
}
/*
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.Error("Could not open new DB: " + err.Error())
}
if tt.upload.Id != "" {
// set ts
ts, err := time.Parse(timeformat, tt.ts)
if err != nil {
t.Error("Could not parse time: " + err.Error())
}
tt.upload.Created = common.Timestamp{Time: ts}
// create new upload db object
err = db.Insert(tt.id, tt.upload)
if err != nil {
t.Error("Could not insert new upload object: " + err.Error())
}
// fetch it
response, err := db.Get(tt.context, tt.id, common.TypeUpload)
if err != nil {
t.Error("Could not fetch upload object: " + err.Error())
}
// is it there?
if len(response.Uploads) != 1 {
t.Error("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, tt.query, common.TypeUpload)
if err != nil {
t.Error("Could not fetch uploads list: " + err.Error())
}
// is it there?
if len(response.Uploads) != 1 {
t.Error("db.List() did not return upload obj[s]")
}
// delete
err = db.Delete(tt.context, tt.id)
if err != nil {
t.Error("Could not delete upload obj: " + err.Error())
}
// fetch again, shall return empty
_, err = db.Get(tt.context, tt.id, common.TypeUpload)
if err == nil {
t.Error("Could fetch upload object again although we deleted it")
}
}
if tt.form.Id != "" {
// set ts
ts, err := time.Parse(timeformat, tt.ts)
if err != nil {
t.Error("Could not parse time: " + err.Error())
}
tt.form.Created = common.Timestamp{Time: ts}
// create new form db object
err = db.Insert(tt.id, tt.form)
if err != nil {
t.Error("Could not insert new form object: " + err.Error())
}
// fetch it
response, err := db.Get(tt.context, tt.id, common.TypeForm)
if err != nil {
t.Error("Could not fetch form object: " + err.Error())
}
// is it there?
if len(response.Forms) != 1 {
t.Error("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, tt.query, common.TypeForm)
if err != nil {
t.Error("Could not fetch forms list: " + err.Error())
}
// is it there?
if len(response.Forms) != 1 {
t.Error("db.FormsList() did not return form obj[s]")
}
// delete
err = db.Delete(tt.context, tt.id)
if err != nil {
t.Error("Could not delete form obj: " + err.Error())
}
// fetch again, shall return empty
_, err = db.Get(tt.context, tt.id, common.TypeForm)
if err == nil {
t.Error("Could fetch form object again although we deleted it")
}
}
})
}
}

View File

@@ -1,196 +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 api
import (
"archive/zip"
"errors"
"github.com/gofiber/fiber/v2"
"codeberg.org/scip/ephemerup/cfg"
"codeberg.org/scip/ephemerup/common"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"sync"
)
// cleanup an upload directory, either because we got an error in the
// middle of an upload or something else went wrong.
func cleanup(dir string) {
err := os.RemoveAll(dir)
if err != nil {
Log("Failed to remove dir %s: %s", dir, err.Error())
}
}
// Extract form file[s] and store them on disk, returns a list of files
func SaveFormFiles(c *fiber.Ctx, cfg *cfg.Config, files []*multipart.FileHeader, id string) ([]string, error) {
members := []string{}
for _, file := range files {
filename, _ := common.Untaint(filepath.Base(file.Filename), cfg.RegNormalizedFilename)
path := filepath.Join(cfg.StorageDir, id, filename)
members = append(members, filename)
Log("Received: %s => %s/%s", file.Filename, id, filename)
if err := c.SaveFile(file, path); err != nil {
cleanup(filepath.Join(cfg.StorageDir, id))
return nil, err
}
}
return members, nil
}
// generate return url. in case of multiple files, zip and remove them
func ProcessFormFiles(cfg *cfg.Config, members []string, id string) (string, string, error) {
returnUrl := ""
Filename := ""
if len(members) == 1 {
returnUrl = strings.Join([]string{cfg.Url, "download", id, members[0]}, "/")
Filename = members[0]
} else {
zipfile := Ts() + "data.zip"
tmpzip := filepath.Join(cfg.StorageDir, zipfile)
finalzip := filepath.Join(cfg.StorageDir, id, zipfile)
iddir := filepath.Join(cfg.StorageDir, id)
if err := ZipDir(iddir, tmpzip); err != nil {
cleanup(iddir)
Log("zip error")
return "", "", err
}
if err := os.Rename(tmpzip, finalzip); err != nil {
cleanup(iddir)
return "", "", err
}
returnUrl = strings.Join([]string{cfg.Url, "download", id, zipfile}, "/")
Filename = zipfile
// clean up after us
go func() {
for _, file := range members {
if err := os.Remove(filepath.Join(cfg.StorageDir, id, file)); err != nil {
Log("ERROR: unable to delete %s: %s", file, err)
}
}
}()
}
return returnUrl, Filename, nil
}
// Create a zip archive from a directory
// FIXME: -e option, if any, goes here
func ZipDir(directory, zipfilename string) error {
f, err := os.Create(zipfilename)
if err != nil {
return err
}
defer f.Close()
writer := zip.NewWriter(f)
defer writer.Close()
var wg sync.WaitGroup
wg.Add(1)
failure := make(chan string)
// don't chdir the server itself
go func() {
defer wg.Done()
if err := os.Chdir(directory); err != nil {
failure <- "Failed to changedir: " + err.Error()
return
}
newDir, err := os.Getwd()
if err != nil {
failure <- "Failed to get cwd: " + err.Error()
}
if newDir != directory {
failure <- "Failed to changedir!"
}
err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
// 2. Go through all the files of the directory
if err != nil {
return err
}
// 3. Create a local file header
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
// set compression
header.Method = zip.Deflate
// 4. Set relative path of a file as the header name
header.Name = path
//Log("a: <%s>, b: <%s>, rel: <%s>", filepath.Dir(directory), path, header.Name)
if err != nil {
return err
}
if info.IsDir() {
header.Name += "/"
}
// 5. Create writer for the file header and save content of the file
headerWriter, err := writer.CreateHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(headerWriter, f)
return err
})
if err != nil {
failure <- "Failed to zip directory: " + err.Error()
} else {
close(failure)
}
}()
wg.Wait()
goterr := <-failure
if goterr != "" {
return errors.New(goterr)
}
return err
}

View File

@@ -1,328 +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 api
import (
//"github.com/alecthomas/repr"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"codeberg.org/scip/ephemerup/cfg"
"codeberg.org/scip/ephemerup/common"
"bytes"
"html/template"
"regexp"
"strings"
"time"
)
/*
Validate a fied by untainting it, modifies field value inplace.
*/
func untaintField(c *fiber.Ctx, orig *string, r *regexp.Regexp, caption string) error {
if len(*orig) != 0 {
nt, err := common.Untaint(*orig, r)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid "+caption+": "+err.Error())
}
*orig = nt
}
return nil
}
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()}, Type: common.TypeForm}
// 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 auxiliary form data (expire field et al)
if err := c.BodyParser(&formdata); err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"bodyparser error : "+err.Error())
}
// post process inputdata
if len(formdata.Expire) == 0 {
entry.Expire = "asap"
} else {
if err := untaintField(c, &formdata.Expire, cfg.RegDuration, "expire data"); err != nil {
return err
}
entry.Expire = formdata.Expire
}
if err := untaintField(c, &formdata.Notify, cfg.RegDuration, "email address"); err != nil {
return err
}
entry.Notify = formdata.Notify
if err := untaintField(c, &formdata.Description, cfg.RegDuration, "description"); err != nil {
return err
}
entry.Description = formdata.Description
// 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 func() {
if err := db.Insert(id, entry); err != nil {
Log("Failed to insert: " + err.Error())
}
}()
// 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!")
}
query, err := common.Untaint(setcontext.Query, cfg.RegQuery)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid query provided!")
}
// retrieve the API Context name from the session
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, query, 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 || len(response.Forms) == 0 {
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())
}
func FormModify(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
var formdata common.Form
// 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())
}
id, err := common.Untaint(c.Params("id"), cfg.RegKey)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid id provided!")
}
// extract form data
if err := c.BodyParser(&formdata); err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"bodyparser error : "+err.Error())
}
// post process input data
if err := untaintField(c, &formdata.Expire, cfg.RegDuration, "expire data"); err != nil {
return err
}
if err := untaintField(c, &formdata.Notify, cfg.RegDuration, "email address"); err != nil {
return err
}
if err := untaintField(c, &formdata.Description, cfg.RegDuration, "description"); err != nil {
return err
}
// lookup orig entry
response, err := db.Get(apicontext, id, common.TypeForm)
if err != nil || len(response.Forms) == 0 {
return JsonStatus(c, fiber.StatusForbidden,
"No form with that id could be found!")
}
form := response.Forms[0]
// modify fields
if formdata.Expire != "" {
form.Expire = formdata.Expire
}
if formdata.Notify != "" {
form.Notify = formdata.Notify
}
if formdata.Description != "" {
form.Description = formdata.Description
}
// run in foreground because we need the feedback here
if err := db.Insert(id, form); err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Failed to insert: "+err.Error())
}
res := &common.Response{Forms: []*common.Form{form}}
res.Success = true
res.Code = fiber.StatusOK
return c.Status(fiber.StatusOK).JSON(res)
}

View File

@@ -1,53 +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 api
import (
"fmt"
"codeberg.org/scip/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
}

View File

@@ -1,244 +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 api
import (
"errors"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/gofiber/fiber/v2/middleware/session"
"github.com/gofiber/keyauth/v2"
"codeberg.org/scip/ephemerup/cfg"
"codeberg.org/scip/ephemerup/common"
)
// sessions are context specific and can be global savely
var Sessionstore *session.Store
const shallExpire = true
func Runserver(conf *cfg.Config, args []string) error {
// required for authenticated routes, used to store the api context
Sessionstore = session.New()
// bbolt db setup
db, err := NewDb(conf)
if err != nil {
return err
}
defer db.Close()
// setup authenticated endpoints
auth := SetupAuthStore(conf, db)
// setup api server
router := SetupServer(conf)
// authenticated routes
api := router.Group(conf.ApiPrefix + ApiVersion)
{
// upload
api.Post("/uploads", auth, func(c *fiber.Ctx) error {
return UploadPost(c, conf, db)
})
// remove
api.Delete("/uploads/:id", auth, func(c *fiber.Ctx) error {
err := UploadDelete(c, conf, db)
return SendResponse(c, "", err)
})
// listing
api.Get("/uploads", auth, func(c *fiber.Ctx) error {
return UploadsList(c, conf, db)
})
// info/describe
api.Get("/uploads/:id", auth, func(c *fiber.Ctx) error {
return UploadDescribe(c, conf, db)
})
// modify
api.Put("/uploads/:id", auth, func(c *fiber.Ctx) error {
return UploadModify(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)
})
// modify
api.Put("/forms/:id", auth, func(c *fiber.Ctx) error {
return FormModify(c, conf, db)
})
}
// public routes
{
router.Get("/", func(c *fiber.Ctx) error {
return c.Send([]byte(conf.Frontpage))
})
router.Get("/download/:id/:file", func(c *fiber.Ctx) error {
return UploadFetch(c, conf, db, shallExpire)
})
router.Get("/download/:id", func(c *fiber.Ctx) error {
return UploadFetch(c, conf, db, shallExpire)
})
router.Get("/form/:id", func(c *fiber.Ctx) error {
return FormPage(c, conf, db, shallExpire)
})
router.Get("/status", func(c *fiber.Ctx) error {
return Status(c, conf)
})
}
// setup cleaner
quitcleaner := BackgroundCleaner(conf, db)
router.Hooks().OnShutdown(func() error {
Log("Shutting down cleaner")
close(quitcleaner)
return nil
})
return router.Listen(conf.Listen)
}
func SetupAuthStore(conf *cfg.Config, db *Db) func(*fiber.Ctx) error {
AuthSetApikeys(conf.Apicontexts)
return keyauth.New(keyauth.Config{
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,
})
}
func SetupServer(conf *cfg.Config) *fiber.App {
router := fiber.New(fiber.Config{
CaseSensitive: true,
StrictRouting: true,
Immutable: true,
Prefork: conf.Prefork,
ServerHeader: "ephemerup Server",
AppName: conf.AppName,
BodyLimit: conf.BodyLimit,
Network: conf.Network,
})
router.Use(requestid.New())
router.Use(logger.New(logger.Config{
Format: "${pid} ${locals:requestid} ${status} - ${method} ${path}\n",
}))
router.Use(cors.New(cors.Config{
AllowMethods: "GET,PUT,POST,DELETE",
ExposeHeaders: "Content-Type,Authorization,Accept",
}))
router.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed,
}))
return router
}
/*
Wrapper to respond with proper json status, message and code,
shall be prepared and called by the handlers directly.
*/
func JsonStatus(c *fiber.Ctx, code int, msg string) error {
success := true
if code != fiber.StatusOK {
success = false
}
return c.Status(code).JSON(common.Result{
Code: code,
Message: msg,
Success: success,
})
}
/*
Used for non json-aware handlers, called by server
*/
func SendResponse(c *fiber.Ctx, msg string, err error) error {
if err != nil {
code := fiber.StatusInternalServerError
var e *fiber.Error
if errors.As(err, &e) {
code = e.Code
}
return c.Status(code).JSON(common.Result{
Code: code,
Message: err.Error(),
Success: false,
})
}
return c.Status(fiber.StatusOK).JSON(common.Result{
Code: fiber.StatusOK,
Message: msg,
Success: true,
})
}

View File

@@ -1,24 +0,0 @@
/*
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/gofiber/fiber/v2"
"codeberg.org/scip/ephemerup/cfg"
"codeberg.org/scip/ephemerup/common"
)
func Status(c *fiber.Ctx, cfg *cfg.Config) error {
res := &common.Response{}
res.Success = true
res.Code = fiber.StatusOK
res.Message = "up and running"
return c.Status(fiber.StatusOK).JSON(res)
}

View File

@@ -1,390 +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 api
import (
//"github.com/alecthomas/repr"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"codeberg.org/scip/ephemerup/cfg"
"codeberg.org/scip/ephemerup/common"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
type SetContext struct {
Apicontext string `json:"apicontext" form:"apicontext"`
Query string `json:"query" form:"query"`
}
func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
// supports upload of multiple files with:
//
// curl -X POST localhost:8080/putfile \
// -F "upload[]=@/home/scip/2023-02-06_10-51.png" \
// -F "upload[]=@/home/scip/pgstat.png" \
// -H "Content-Type: multipart/form-data"
//
// If multiple files are uploaded, they are zipped, otherwise
// the file is being stored as is.
//
// Returns the name of the uploaded file.
id := uuid.NewString()
var returnUrl string
var formdata Meta
if err := os.MkdirAll(filepath.Join(cfg.StorageDir, id), os.ModePerm); err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize directories: "+err.Error())
}
// fetch auxiliary form data
form, err := c.MultipartForm()
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"mime/multipart error "+err.Error())
}
// init upload obj
entry := &common.Upload{Id: id, Created: common.Timestamp{Time: time.Now()}, Type: common.TypeUpload}
// 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
// retrieve files, if any
files := form.File["upload[]"]
members, err := SaveFormFiles(c, cfg, files, id)
if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"Could not store uploaded file[s]: "+err.Error())
}
entry.Members = members
// extract auxiliary form data (expire field et al)
if err := c.BodyParser(&formdata); err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"bodyparser error : "+err.Error())
}
// post process expire
if len(formdata.Expire) == 0 {
entry.Expire = "asap"
} else {
ex, err := common.Untaint(formdata.Expire, cfg.RegDuration) // duration or asap allowed
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid data: "+err.Error())
}
entry.Expire = ex
}
// get url [and zip if there are multiple files]
returnUrl, Newfilename, err := ProcessFormFiles(cfg, entry.Members, id)
if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"Could not process uploaded file[s]: "+err.Error())
}
entry.File = Newfilename
entry.Url = returnUrl
Log("Now serving %s from %s/%s", returnUrl, cfg.StorageDir, id)
Log("Expire set to: %s", entry.Expire)
Log("Uploaded with API-Context %s", entry.Context)
// we do this in the background to not thwart the server
go func() {
if err := db.Insert(id, entry); err != nil {
Log("Failed to insert: " + err.Error())
}
}()
// everything went well so far
res := &common.Response{Uploads: []*common.Upload{entry}}
res.Success = true
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" {
if err := db.Delete(apicontext, formid); err != nil {
Log("Failed to delete formid %s: %s", formid, err.Error())
}
}
// 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)
}
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
// we ignore c.Params("file"), cause it may be malign. Also we've
// got it in the db anyway
id, err := common.Untaint(c.Params("id"), cfg.RegKey)
if err != nil {
return fiber.NewError(403, "Invalid id provided!")
}
// retrieve the API Context name from the session
apicontext, err := SessionGetApicontext(c)
if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error())
}
response, err := db.Lookup(apicontext, id, common.TypeUpload)
if err != nil {
// non existent db entry with that id, or other db error, see logs
return fiber.NewError(404, "No download with that id could be found!")
}
var upload *common.Upload
if len(response.Uploads) > 0 {
upload = response.Uploads[0]
}
file := upload.File
filename := filepath.Join(cfg.StorageDir, id, file)
if _, err := os.Stat(filename); err != nil {
// db entry is there, but file isn't (anymore?)
go func() {
if err := db.Delete(apicontext, id); err != nil {
Log("Unable to delete entry id %s: %s", id, err.Error())
}
}()
return fiber.NewError(404, "No download with that id could be found!")
}
// finally put the file to the client
err = c.Download(filename, file)
if len(shallExpire) > 0 {
if shallExpire[0] {
go func() {
// check if we need to delete the file now and do it in the background
if upload.Expire == "asap" {
cleanup(filepath.Join(cfg.StorageDir, id))
if err := db.Delete(apicontext, id); err != nil {
Log("Unable to delete entry id %s: %s", id, err.Error())
}
}
}()
}
}
return err
}
// delete file, id dir and db entry
func UploadDelete(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 upload with that id could be found!")
}
cleanup(filepath.Join(cfg.StorageDir, id))
return nil
}
// returns the whole list + error code, no post processing by server
func UploadsList(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
// fetch apifilter+query 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())
}
apifilter, err := common.Untaint(setcontext.Apicontext, cfg.RegKey)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid api context apifilter provided!")
}
query, err := common.Untaint(setcontext.Query, cfg.RegQuery)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid query provided!")
}
// retrieve the API Context name from the session
apicontext, err := SessionGetApicontext(c)
if err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"Unable to initialize session store from context: "+err.Error())
}
// get list
uploads, err := db.List(apicontext, apifilter, query, common.TypeUpload)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to list uploads: "+err.Error())
}
// if we reached this point we can signal success
uploads.Success = true
uploads.Code = fiber.StatusOK
return c.Status(fiber.StatusOK).JSON(uploads)
}
// returns just one upload obj + error code, no post processing by server
func UploadDescribe(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.TypeUpload)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"No upload with that id could be found!")
}
for _, upload := range response.Uploads {
upload.Url = strings.Join([]string{cfg.Url, "download", id, upload.File}, "/")
}
// if we reached this point we can signal success
response.Success = true
response.Code = fiber.StatusOK
return c.Status(fiber.StatusOK).JSON(response)
}
func UploadModify(c *fiber.Ctx, cfg *cfg.Config, db *Db) error {
var formdata common.Upload
// 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())
}
id, err := common.Untaint(c.Params("id"), cfg.RegKey)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Invalid id provided!")
}
// extract form data
if err := c.BodyParser(&formdata); err != nil {
return JsonStatus(c, fiber.StatusInternalServerError,
"bodyparser error : "+err.Error())
}
// post process input data
if err := untaintField(c, &formdata.Expire, cfg.RegDuration, "expire data"); err != nil {
return err
}
if err := untaintField(c, &formdata.Description, cfg.RegDuration, "description"); err != nil {
return err
}
// lookup orig entry
response, err := db.Get(apicontext, id, common.TypeUpload)
if err != nil || len(response.Uploads) == 0 {
return JsonStatus(c, fiber.StatusForbidden,
"No upload with that id could be found!")
}
upload := response.Uploads[0]
// modify fields
if formdata.Expire != "" {
upload.Expire = formdata.Expire
}
if formdata.Description != "" {
upload.Description = formdata.Description
}
// run in foreground because we need the feedback here
if err := db.Insert(id, upload); err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Failed to insert: "+err.Error())
}
res := &common.Response{Uploads: []*common.Upload{upload}}
res.Success = true
res.Code = fiber.StatusOK
return c.Status(fiber.StatusOK).JSON(res)
}

View File

@@ -1,116 +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 api
import (
"fmt"
"time"
"codeberg.org/scip/ephemerup/cfg"
"codeberg.org/scip/ephemerup/common"
"github.com/gofiber/fiber/v2"
)
const ApiVersion string = "/v1"
// Binding from JSON, data coming from user, not tainted
type Meta struct {
Expire string `json:"expire" form:"expire"`
}
// incoming id
type Id struct {
Id string `json:"name" xml:"name" form:"name"`
}
// vaious helbers
func Log(format string, values ...any) {
fmt.Printf("[DEBUG] "+format+"\n", values...)
}
func Ts() string {
t := time.Now()
return t.Format("2006-01-02-15-04-")
}
/*
Retrieve the API Context name from the session, assuming is has
been successfully authenticated. However, if there are no api
contexts defined, we'll use 'default' (set in
auth.validateAPIKey()).
If there's no apicontext in the session, assume unauth user, return ""
*/
func SessionGetApicontext(c *fiber.Ctx) (string, error) {
sess, err := Sessionstore.Get(c)
if err != nil {
return "", fmt.Errorf("Unable to initialize session store from context: %s", err.Error())
}
apicontext := sess.Get("apicontext")
if apicontext != nil {
return apicontext.(string), 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: %s", 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
duration. Returns true if time is expired. Start time comes from
the database.
aka:
if(now - start) >= duration { time is up}
*/
func IsExpired(conf *cfg.Config, start time.Time, duration string) bool {
var expiretime int // seconds
now := time.Now()
if duration == "asap" {
expiretime = conf.DefaultExpire
} else {
expiretime = common.Duration2int(duration)
}
if now.Unix()-start.Unix() >= int64(expiretime) {
return true
}
return false
}

View File

@@ -1,130 +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 cfg
import (
"fmt"
"regexp"
"strings"
"time"
)
const Version string = "v0.0.3"
var VERSION string // maintained by -x
type Apicontext struct {
Context string `koanf:"context"` // aka name or tenant
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
type Config struct {
// Flags+config file settings
ApiPrefix string `koanf:"apiprefix"` // path prefix
Debug bool `koanf:"debug"`
Listen string `koanf:"listen"` // [host]:port
StorageDir string `koanf:"storagedir"` // db and uploads go there
Url string `koanf:"url"` // public visible url, might be different from Listen
DbFile string `koanf:"dbfile"`
Super string `koanf:"super"` // the apicontext which has all permissions
Frontpage string `koanf:"frontpage"` // a html file
Formpage string `koanf:"formpage"` // a html file
// fiber settings, see:
// https://docs.gofiber.io/api/fiber/#config
Prefork bool `koanf:"prefork"` // default: nope
AppName string `koanf:"appname"` // "upd"
BodyLimit int `koanf:"bodylimit"` // much
V4only bool `koanf:"ipv4"`
V6only bool `koanf:"ipv6"`
Network string
// only settable via config
Apicontexts []Apicontext `koanf:"apicontexts"`
// smtp settings
Mail Mailsettings `koanf:"mail"`
// Internals only
RegNormalizedFilename *regexp.Regexp
RegDuration *regexp.Regexp
RegKey *regexp.Regexp
RegEmail *regexp.Regexp
RegText *regexp.Regexp
RegQuery *regexp.Regexp
CleanInterval time.Duration
DefaultExpire int
}
func Getversion() string {
// main program version
// generated version string, used by -v contains cfg.Version on
// main branch, and cfg.Version-$branch-$lastcommit-$date on
// development branch
return fmt.Sprintf("This is ephemerup server version %s", VERSION)
}
func (c *Config) GetVersion() string {
return VERSION
}
// post processing of options, if any
func (c *Config) ApplyDefaults() {
if len(c.Url) == 0 {
if strings.HasPrefix(c.Listen, ":") {
c.Url = "http://localhost" + c.Listen
} else {
c.Url = "http://" + c.Listen
}
}
switch {
case c.V4only && c.V6only:
c.Network = "tcp" // dual stack
case c.V4only:
c.Network = "tcp4"
case c.V6only:
c.Network = "tcp6"
default:
if c.Prefork {
c.Network = "tcp4"
} else {
c.Network = "tcp" // dual stack
}
}
c.RegNormalizedFilename = regexp.MustCompile(`[^\w\d\-_\.]`)
c.RegDuration = regexp.MustCompile(`[^dhms0-9]`)
c.RegKey = regexp.MustCompile(`[^a-zA-Z0-9\-]`)
c.RegEmail = regexp.MustCompile(`[^a-zA-Z0-9._%+\-@0-9]`)
c.RegText = regexp.MustCompile(`[^a-zA-Z0-9_%+\-@0-9 #/\.]`)
c.RegQuery = regexp.MustCompile(`[^a-zA-Z0-9_%+\-@0-9 #/\.\*\[\]\(\)\\]`)
c.CleanInterval = 10 * time.Second
c.DefaultExpire = 30 * 86400 // 1 month
}

View File

@@ -1,24 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: eph-ephemerup-config
namespace: "eph"
labels:
app.kubernetes.io/name: ephemerup
helm.sh/chart: ephemerup-1.0.0
app.kubernetes.io/instance: eph
app.kubernetes.io/managed-by: Helm
annotations:
app: ephemerup
data:
listen = "8080"
bodylimit = "1024"
super = "root"
mail = {
server =
port =
from =
password =
}
apicontexts = [
]

View File

@@ -1,21 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj

View File

@@ -1,21 +0,0 @@
apiVersion: v2
name: ephemerup
description: |
A Helm chart for Ephemerup.
type: application
sources:
- https://codeberg.org/scip/ephemerup
version: 1.0.0
appVersion: "0.0.2"
dependencies:
- name: common
repository: https://charts.bitnami.com/bitnami
tags:
- bitnami-common
version: 1.x.x
# icon: ""

View File

@@ -1,126 +0,0 @@
# ephemerup
![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.2](https://img.shields.io/badge/AppVersion-0.0.2-informational?style=flat-square)
A Helm chart for Ephemerup.
## Source Code
* <https://codeberg.org/scip/ephemerup>
## Requirements
| Repository | Name | Version |
|------------|------|---------|
| https://charts.bitnami.com/bitnami | common | 1.x.x |
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | |
| clusterDomain | string | `"cluster.local"` | |
| commonAnnotations.app | string | `"ephemerup"` | |
| commonLabels | object | `{}` | |
| config.apicontexts[0].context | string | `"root"` | |
| config.apicontexts[0].key | string | `"0fddbff5d8010f81cd28a7d77f3e38981b13d6164c2fd6e1c3f60a4287630c37"` | |
| config.bodylimit | int | `1024` | |
| config.listen | int | `8080` | |
| config.mail.from | string | `"root@localhost"` | |
| config.mail.port | int | `25` | |
| config.mail.server | string | `"localhost"` | |
| config.super | string | `"root"` | |
| containerSecurityContext.allowPrivilegeEscalation | bool | `false` | |
| containerSecurityContext.capabilities.drop[0] | string | `"ALL"` | |
| containerSecurityContext.enabled | bool | `false` | |
| containerSecurityContext.privileged | bool | `false` | |
| containerSecurityContext.runAsNonRoot | bool | `false` | |
| containerSecurityContext.runAsUser | int | `0` | |
| customLivenessProbe | object | `{}` | |
| customReadinessProbe | object | `{}` | |
| customStartupProbe | object | `{}` | |
| env | list | `[]` | |
| fullnameOverride | string | `""` | |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.pullSecrets | list | `[]` | |
| image.registry | string | `"ghcr.io/tlinden"` | |
| image.repository | string | `"ephemerup"` | |
| image.tag | string | `"latest"` | |
| ingress.annotations | object | `{}` | |
| ingress.apiVersion | string | `""` | |
| ingress.enabled | bool | `false` | |
| ingress.extraHosts | list | `[]` | |
| ingress.extraPaths | list | `[]` | |
| ingress.extraRules | list | `[]` | |
| ingress.extraTls | list | `[]` | |
| ingress.hostname | string | `"ephemerup.local"` | |
| ingress.ingressClassName | string | `"nginx"` | |
| ingress.path | string | `"/"` | |
| ingress.pathType | string | `"Prefix"` | |
| ingress.secrets | list | `[]` | |
| ingress.selfSigned | bool | `false` | |
| ingress.tls | bool | `false` | |
| ingress.tlsSecretName | string | `""` | |
| kubeVersion | string | `""` | |
| lifecycleHooks | object | `{}` | |
| livenessProbe.enabled | bool | `true` | |
| livenessProbe.failureThreshold | int | `6` | |
| livenessProbe.initialDelaySeconds | int | `5` | |
| livenessProbe.periodSeconds | int | `20` | |
| livenessProbe.successThreshold | int | `1` | |
| livenessProbe.timeoutSeconds | int | `1` | |
| logLevel | string | `"info"` | |
| metrics.serviceMonitor.enabled | bool | `false` | |
| metrics.serviceMonitor.interval | string | `"30s"` | |
| metrics.serviceMonitor.namespace | string | `""` | |
| metrics.serviceMonitor.port | string | `"http"` | |
| metrics.serviceMonitor.scrapeTimeout | string | `"10s"` | |
| mountSecrets | list | `[]` | |
| nameOverride | string | `""` | |
| namespaceOverride | string | `""` | |
| nodeAffinityPreset.key | string | `""` | |
| nodeAffinityPreset.type | string | `""` | |
| nodeAffinityPreset.values | list | `[]` | |
| nodeSelector | object | `{}` | |
| podAffinityPreset | string | `""` | |
| podAnnotations | object | `{}` | |
| podAntiAffinityPreset | string | `"soft"` | |
| podLabels | object | `{}` | |
| podSecurityContext.fsGroup | int | `65534` | |
| readinessProbe.enabled | bool | `true` | |
| readinessProbe.failureThreshold | int | `6` | |
| readinessProbe.initialDelaySeconds | int | `5` | |
| readinessProbe.periodSeconds | int | `20` | |
| readinessProbe.successThreshold | int | `1` | |
| readinessProbe.timeoutSeconds | int | `1` | |
| replicaCount | int | `1` | |
| resources.limits.cpu | string | `"500m"` | |
| resources.limits.memory | string | `"256Mi"` | |
| resources.requests.cpu | string | `"100m"` | |
| resources.requests.memory | string | `"128Mi"` | |
| secrets | object | `{}` | |
| service.annotations | object | `{}` | |
| service.clusterIP | string | `""` | |
| service.externalTrafficPolicy | string | `"Cluster"` | |
| service.extraPorts | list | `[]` | |
| service.loadBalancerIP | string | `""` | |
| service.loadBalancerSourceRanges | list | `[]` | |
| service.nodePorts.http | string | `""` | |
| service.ports.http | int | `8080` | |
| service.sessionAffinity | string | `"None"` | |
| service.sessionAffinityConfig | object | `{}` | |
| service.type | string | `"ClusterIP"` | |
| sidecars | list | `[]` | |
| startupProbe.enabled | bool | `true` | |
| startupProbe.failureThreshold | int | `6` | |
| startupProbe.initialDelaySeconds | int | `10` | |
| startupProbe.periodSeconds | int | `20` | |
| startupProbe.successThreshold | int | `1` | |
| startupProbe.timeoutSeconds | int | `1` | |
| storage.longTerm | object | `{"name":"ephemerup-storage","spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}},"storageClassName":"standard"}}` | Persistent volume for bolt database and uploads |
| storage.tmp | object | `{"name":"ephemerup-tmp","spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}},"storageClassName":"standard"}}` | Persistent volume for temporary files |
| tolerations | list | `[]` | |
| updateStrategy.type | string | `"RollingUpdate"` | |
----------------------------------------------
Autogenerated from chart metadata using [helm-docs v1.11.2](https://github.com/norwoodj/helm-docs/releases/v1.11.2)

View File

@@ -1,51 +0,0 @@
CHART NAME: {{ .Chart.Name }}
CHART VERSION: {{ .Chart.Version }}
APP VERSION: {{ .Chart.AppVersion }}
** Please be patient while the chart is being deployed **
Application can be accessed through the following DNS name from within your cluster:
{{ include "common.names.fullname" . }}.{{ include "common.names.namespace" . }}.svc.{{ .Values.clusterDomain }} (port {{ .Values.service.ports.http }})
To access Application from outside the cluster execute the following commands:
{{- if .Values.ingress.enabled }}
1. Get the Application URL and associate its hostname to your cluster external IP:
export CLUSTER_IP=$(minikube ip) # On Minikube. Use: `kubectl cluster-info` on others K8s clusters
echo "Application URL: http{{ if .Values.ingress.tls }}s{{ end }}://{{ .Values.ingress.hostname }}"
echo "$CLUSTER_IP {{ .Values.ingress.hostname }}" | sudo tee -a /etc/hosts
{{- else }}
1. Get the Application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ include "common.names.namespace" . }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "common.names.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ include "common.names.namespace" . }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo "http://${NODE_IP}:${NODE_PORT}"
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ include "common.names.namespace" . }} svc -w {{ include "common.names.fullname" . }}'
export SERVICE_PORT=$(kubectl get --namespace {{ include "common.names.namespace" . }} -o jsonpath="{.spec.ports[0].port}" services {{ include "common.names.fullname" . }})
export SERVICE_IP=$(kubectl get svc --namespace {{ include "common.names.namespace" . }} {{ include "common.names.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "http://${SERVICE_IP}:${SERVICE_PORT}"
{{- else if contains "ClusterIP" .Values.service.type }}
export SERVICE_PORT=$(kubectl get --namespace {{ include "common.names.namespace" . }} -o jsonpath="{.spec.ports[0].port}" services {{ include "common.names.fullname" . }})
kubectl port-forward --namespace {{ include "common.names.namespace" . }} svc/{{ include "common.names.fullname" . }} ${SERVICE_PORT}:${SERVICE_PORT} &
echo "http://127.0.0.1:${SERVICE_PORT}"
{{- end }}
{{- end }}
2. Access Application using the obtained URL.
{{- include "common.warnings.rollingTag" .Values.image }}

View File

@@ -1,8 +0,0 @@
{{/*
Return the proper image name
*/}}
{{- define "ephemerup.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.image "global" .Values.global) }}
{{- end -}}

View File

@@ -1,39 +0,0 @@
{{- if (.Values.config) }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "common.names.fullname" . }}-config
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "common.labels.standard" . | nindent 4 }}
{{- if .Values.commonLabels }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- end }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
data:
ephemerup.hcl: |-
listen = {{ .Values.config.listen | quote }}
bodylimit = {{ .Values.config.bodylimit | quote }}
{{- if .Values.config.url }}
url = {{ .Values.config.url | quote }}
{{- end }}
super = {{ .Values.config.super | quote }}
mail = {
server = {{ .Values.config.mail.server | quote }}
port = {{ .Values.config.mail.port | quote }}
from = {{ .Values.config.mail.from | quote }}
{{- if .Values.config.password }}
password = {{ .Values.config.password | quote }}
{{- end }}
}
apicontexts = [
{{- range $context := .Values.config.apicontexts }}
{
context = {{ $context.context | quote }}
key = {{ $context.key | quote }}
}
{{- end }}
]
storagedir = "/data"
{{- end }}

View File

@@ -1,63 +0,0 @@
{{- if .Values.ingress.enabled -}}
apiVersion: {{ include "common.capabilities.ingress.apiVersion" . }}
kind: Ingress
metadata:
name: {{ include "common.names.fullname" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "common.labels.standard" . | nindent 4 }}
{{- if .Values.commonLabels }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- end }}
annotations:
{{- if .Values.ingress.certManager }}
kubernetes.io/tls-acme: "true"
{{- end }}
{{- if .Values.ingress.annotations }}
{{- include "common.tplvalues.render" ( dict "value" .Values.ingress.annotations "context" $) | nindent 4 }}
{{- end }}
{{- if .Values.commonAnnotations }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.ingressClassName (include "common.ingress.supportsIngressClassname" .) }}
ingressClassName: {{ .Values.ingress.ingressClassName | quote }}
{{- end }}
rules:
{{- if .Values.ingress.hostname }}
- host: {{ .Values.ingress.hostname }}
http:
paths:
{{- if .Values.ingress.extraPaths }}
{{- toYaml .Values.ingress.extraPaths | nindent 10 }}
{{- end }}
- path: {{ .Values.ingress.path }}
{{- if eq "true" (include "common.ingress.supportsPathType" .) }}
pathType: {{ .Values.ingress.pathType }}
{{- end }}
backend: {{- include "common.ingress.backend" (dict "serviceName" (include "common.names.fullname" .) "servicePort" "http" "context" $) | nindent 14 }}
{{- end }}
{{- range .Values.ingress.extraHosts }}
- host: {{ .name }}
http:
paths:
- path: {{ default "/" .path }}
{{- if eq "true" (include "common.ingress.supportsPathType" $) }}
pathType: {{ default "ImplementationSpecific" .pathType }}
{{- end }}
backend: {{- include "common.ingress.backend" (dict "serviceName" (include "common.names.fullname" $) "servicePort" "http" "context" $) | nindent 14 }}
{{- end }}
{{- if .Values.ingress.extraRules }}
{{- include "common.tplvalues.render" (dict "value" .Values.ingress.extraRules "context" $) | nindent 4 }}
{{- end }}
{{- if or .Values.ingress.tls .Values.ingress.extraTls .Values.ingress.hosts }}
tls:
{{- if .Values.ingress.tls }}
- hosts:
- {{ .Values.ingress.hostname }}
secretName: {{ default (printf "%s-tls" .Values.ingress.hostname) .Values.ingress.tlsSecretName }}
{{- end }}
{{- if .Values.ingress.extraTls }}
{{- toYaml .Values.ingress.extraTls | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -1,18 +0,0 @@
{{- if .Values.secrets }}
apiVersion: v1
kind: Secret
metadata:
name: {{ template "common.names.fullname" . }}-config
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "common.labels.standard" . | nindent 4 }}
{{- if .Values.commonLabels }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- end }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
data:
{{- with .Values.secrets }}
{{ toYaml . | nindent 2 }}
{{- end }}
{{ end }}

View File

@@ -1,33 +0,0 @@
{{- if .Values.metrics.serviceMonitor.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "common.names.fullname" . }}-sm
{{- if .Values.metrics.serviceMonitor.namespace }}
namespace: {{ .Values.metrics.serviceMonitor.namespace }}
{{- end }}
labels: {{- include "common.labels.standard" . | nindent 4 }}
{{- if .Values.commonLabels }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- end }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "common.labels.standard" . | nindent 6 }}
endpoints:
- path: /metrics
port: {{ .Values.metrics.serviceMonitor.port }}
{{- if .Values.metrics.serviceMonitor.interval }}
interval: {{ .Values.metrics.serviceMonitor.interval }}
{{- end }}
{{- if .Values.metrics.serviceMonitor.scrapeTimeout }}
scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }}
{{- end }}
namespaceSelector:
matchNames:
- {{ .Release.Namespace }}
{{- end }}

View File

@@ -1,49 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "common.names.fullname" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "common.labels.standard" . | nindent 4 }}
{{- if .Values.commonLabels }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- end }}
annotations:
{{- if .Values.service.annotations }}
{{- include "common.tplvalues.render" ( dict "value" .Values.service.annotations "context" $) | nindent 4 }}
{{- end }}
{{- if .Values.commonAnnotations }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if and .Values.service.clusterIP (eq .Values.service.type "ClusterIP") }}
clusterIP: {{ .Values.service.clusterIP }}
{{- end }}
{{- if and (eq .Values.service.type "LoadBalancer") (not (empty .Values.service.loadBalancerIP)) }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}
{{- if and (eq .Values.service.type "LoadBalancer") (not (empty .Values.service.loadBalancerSourceRanges)) }}
loadBalancerSourceRanges: {{ .Values.service.loadBalancerSourceRanges }}
{{- end }}
{{- if or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort") }}
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy | quote }}
{{- end }}
{{- if .Values.service.sessionAffinity }}
sessionAffinity: {{ .Values.service.sessionAffinity }}
{{- end }}
{{- if .Values.service.sessionAffinityConfig }}
sessionAffinityConfig: {{- include "common.tplvalues.render" (dict "value" .Values.service.sessionAffinityConfig "context" $) | nindent 4 }}
{{- end }}
ports:
- name: http
port: {{ .Values.service.ports.http }}
targetPort: http
{{- if (and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) (not (empty .Values.service.nodePorts.http))) }}
nodePort: {{ .Values.service.nodePorts.http }}
{{- else if eq .Values.service.type "ClusterIP" }}
nodePort: null
{{- end }}
{{- if .Values.service.extraPorts }}
{{- include "common.tplvalues.render" (dict "value" .Values.service.extraPorts "context" $) | nindent 4 }}
{{- end }}
selector: {{- include "common.labels.matchLabels" . | nindent 4 }}

View File

@@ -1,127 +0,0 @@
{{- $fullName := include "common.names.fullname" . -}}
apiVersion: {{ include "common.capabilities.statefulset.apiVersion" . }}
kind: StatefulSet
metadata:
name: {{ include "common.names.fullname" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "common.labels.standard" . | nindent 4 }}
{{- if .Values.commonLabels }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- end }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
spec:
serviceName: {{ include "common.names.fullname" . }}
replicas: {{ .Values.replicaCount }}
selector:
matchLabels: {{- include "common.labels.matchLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/configMap: {{ toYaml .Values.config | sha256sum }}
{{- if .Values.podAnnotations }}
{{- include "common.tplvalues.render" (dict "value" .Values.podAnnotations "context" $) | nindent 8 }}
{{- end }}
labels: {{- include "common.labels.standard" . | nindent 8 }}
{{- if .Values.podLabels }}
{{- include "common.tplvalues.render" (dict "value" .Values.podLabels "context" $) | nindent 8 }}
{{- end }}
spec:
containers:
- name: ephemerup
image: {{ include "ephemerup.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy | quote}}
command: ['/app/ephemerupd', '-c', '/config/ephemerup.hcl']
env:
{{- range $envVar := .Values.env }}
- name: {{ $envVar.name }}
value: {{ $envVar.value }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.service.ports.http }}
protocol: TCP
{{- if .Values.livenessProbe.enabled }}
livenessProbe:
httpGet:
port: {{ .Values.service.ports.http }}
path: /status
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
successThreshold: {{ .Values.livenessProbe.successThreshold }}
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
{{- else if .Values.customLivenessProbe }}
livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.customLivenessProbe "context" $) | nindent 12 }}
{{- end }}
{{- if .Values.readinessProbe.enabled }}
readinessProbe:
httpGet:
port: {{ .Values.service.ports.http }}
path: /status
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
successThreshold: {{ .Values.readinessProbe.successThreshold }}
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
{{- else if .Values.customReadinessProbe }}
readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.customReadinessProbe "context" $) | nindent 12 }}
{{- end }}
{{- if .Values.startupProbe.enabled }}
startupProbe:
tcpSocket:
port: {{ .Values.service.ports.http }}
initialDelaySeconds: {{ .Values.startupProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.startupProbe.periodSeconds }}
timeoutSeconds: {{ .Values.startupProbe.timeoutSeconds }}
successThreshold: {{ .Values.startupProbe.successThreshold }}
failureThreshold: {{ .Values.startupProbe.failureThreshold }}
{{- else if .Values.customStartupProbe }}
startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.customStartupProbe "context" $) | nindent 12 }}
{{- end }}
{{- if .Values.resources }}
resources: {{- toYaml .Values.resources | nindent 12 }}
{{- end }}
{{- if .Values.containerSecurityContext.enabled }}
securityContext: {{- omit .Values.containerSecurityContext "enabled" | toYaml | nindent 10 }}
{{- end }}
volumeMounts:
- name: "ephemerup-storage"
mountPath: "/data"
- name: "ephemerup-tmp"
mountPath: "/tmp"
- mountPath: "/config/ephemerup.hcl"
name: config
subPath: "ephemerup.hcl"
{{- range $secret := .Values.mountSecrets }}
- mountPath: "/secret/{{ $secret.name }}"
name: {{ $secret.name }}
{{- end }}
securityContext:
{{ toYaml .Values.podSecurityContext | nindent 8 | trim }}
{{- if .Values.nodeSelector }}
nodeSelector: {{- include "common.tplvalues.render" ( dict "value" .Values.nodeSelector "context" $) | nindent 8 }}
{{- end }}
volumes:
- name: config
configMap:
name: {{ template "common.names.fullname" . }}-config
items:
- key: ephemerup.hcl
path: ephemerup.hcl
{{- range $secret := .Values.mountSecrets }}
- name: {{ $secret.name }}
secret:
secretName: {{ $secret.name }}
{{- end }}
volumeClaimTemplates:
- metadata:
name: {{ .Values.storage.longTerm.name }}
spec:
{{ toYaml .Values.storage.longTerm.spec | nindent 6 | trim }}
- metadata:
name: {{ .Values.storage.tmp.name }}
spec:
{{ toYaml .Values.storage.tmp.spec | nindent 6 | trim }}

View File

@@ -1,12 +0,0 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"title": "Values schema",
"type": "object",
"properties": {
"replicaCount": {
"type": "integer",
"enum": [0, 1]
}
},
"required": ["replicaCount"]
}

View File

@@ -1,472 +0,0 @@
## @section Common parameters
##
## @param kubeVersion Override Kubernetes version
##
kubeVersion: ""
## @param nameOverride String to partially override aspnet-core.fullname
##
nameOverride: ""
## @param fullnameOverride String to fully override aspnet-core.fullname
##
fullnameOverride: ""
## @param namespaceOverride String to fully override common.names.namespace
##
namespaceOverride: ""
## @param commonLabels Labels to add to all deployed objects
##
commonLabels: {}
## @param commonAnnotations Annotations to add to all deployed objects
##
commonAnnotations:
app: ephemerup
## @param clusterDomain Kubernetes cluster domain name
##
clusterDomain: cluster.local
logLevel: info
##
image:
registry: ghcr.io/tlinden
repository: ephemerup
tag: "latest"
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
## e.g:
## pullSecrets:
## - myRegistryKeySecretName
##
pullSecrets: []
secrets: {}
mountSecrets: []
# - name: my-secret
# Environment variables
env: []
# - name: HTTPS_PROXY
# value: "http://localhost:3128"
# Ephemerup configuration
config:
## must be the same as in the service spec below
listen: 8080
## max bytes allowed to upload
bodylimit: 1024
## optional public visible url
#url:
## root context which has all permissions
super: "root"
## mail config
mail:
server: "localhost"
port: 25
from: "root@localhost"
## required when using SMTP Auth
#password: ""
## context config, add more as needed
apicontexts:
- context: "root"
key: "0fddbff5d8010f81cd28a7d77f3e38981b13d6164c2fd6e1c3f60a4287630c37"
## @param replicaCount Number of application replicas to deploy
##
replicaCount: 1
## @param sidecars Add additional sidecar containers to the application pods
## e.g:
## sidecars:
## - name: your-image-name
## image: your-image
## imagePullPolicy: Always
## ports:
## - name: portname
## containerPort: 1234
##
sidecars: []
## @param lifecycleHooks Add lifecycle hooks to the application deployment
##
lifecycleHooks: {}
## @param podAnnotations Annotations for application pods
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
##
podAnnotations: {}
## @param podLabels Extra labels for application pods
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
##
podLabels: {}
## @param updateStrategy.type Deployment strategy type
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies
##
updateStrategy:
## StrategyType
## Can be set to RollingUpdate or OnDelete
##
type: RollingUpdate
## @param podAffinityPreset Pod affinity preset. Ignored if `affinity` is set. Allowed values: `soft` or `hard`
## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity
##
podAffinityPreset: ""
## @param podAntiAffinityPreset Pod anti-affinity preset. Ignored if `affinity` is set. Allowed values: `soft` or `hard`
## Ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity
##
podAntiAffinityPreset: soft
## Node affinity preset
## Ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity
##
nodeAffinityPreset:
## @param nodeAffinityPreset.type Node affinity preset type. Ignored if `affinity` is set. Allowed values: `soft` or `hard`
##
type: ""
## @param nodeAffinityPreset.key Node label key to match. Ignored if `affinity` is set
##
key: ""
## @param nodeAffinityPreset.values Node label values to match. Ignored if `affinity` is set
## E.g.
## values:
## - e2e-az1
## - e2e-az2
##
values: []
## @param affinity Affinity for pod assignment
## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
## NOTE: podAffinityPreset, podAntiAffinityPreset, and nodeAffinityPreset will be ignored when it's set
##
affinity: {}
## @param nodeSelector Node labels for pod assignment
## ref: https://kubernetes.io/docs/user-guide/node-selection/
##
nodeSelector: {}
## @param tolerations Tolerations for pod assignment
## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
##
tolerations: []
## application containers' resource requests and limits
## ref: https://kubernetes.io/docs/user-guide/compute-resources/
## @param resources.limits The resources limits for the application container
## @param resources.requests The requested resources for the application container
##
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
## Configure Pods Security Context
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
## @param podSecurityContext.fsGroup Set Security Context fsGroup
podSecurityContext:
fsGroup: 65534
## Configure Container Security Context (only main container)
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container
## @param containerSecurityContext.enabled Enabled application containers' Security Context
## @param containerSecurityContext.runAsUser Set application container's Security Context runAsUser
## @param containerSecurityContext.runAsNonRoot Set container's Security Context runAsNonRoot
##
containerSecurityContext:
enabled: false
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
runAsUser: 0
runAsNonRoot: false
## Configure extra options for application containers' liveness and readiness probes
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes
## @param livenessProbe.enabled Enable livenessProbe
## @param livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe
## @param livenessProbe.periodSeconds Period seconds for livenessProbe
## @param livenessProbe.timeoutSeconds Timeout seconds for livenessProbe
## @param livenessProbe.failureThreshold Failure threshold for livenessProbe
## @param livenessProbe.successThreshold Success threshold for livenessProbe
##
livenessProbe:
enabled: true
initialDelaySeconds: 5
timeoutSeconds: 1
periodSeconds: 20
failureThreshold: 6
successThreshold: 1
## @param readinessProbe.enabled Enable readinessProbe
## @param readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe
## @param readinessProbe.periodSeconds Period seconds for readinessProbe
## @param readinessProbe.timeoutSeconds Timeout seconds for readinessProbe
## @param readinessProbe.failureThreshold Failure threshold for readinessProbe
## @param readinessProbe.successThreshold Success threshold for readinessProbe
##
readinessProbe:
enabled: true
initialDelaySeconds: 5
timeoutSeconds: 1
periodSeconds: 20
failureThreshold: 6
successThreshold: 1
## Configure extra options for application containers' startup and readiness probes
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-startup-readiness-probes/#configure-probes
## @param startupProbe.enabled Enable startupProbe
## @param startupProbe.initialDelaySeconds Initial delay seconds for startupProbe
## @param startupProbe.periodSeconds Period seconds for startupProbe
## @param startupProbe.timeoutSeconds Timeout seconds for startupProbe
## @param startupProbe.failureThreshold Failure threshold for startupProbe
## @param startupProbe.successThreshold Success threshold for startupProbe
##
startupProbe:
enabled: true
initialDelaySeconds: 10
timeoutSeconds: 1
periodSeconds: 20
failureThreshold: 6
successThreshold: 1
## @param customLivenessProbe Custom livenessProbe that overrides the default one
##
customLivenessProbe: {}
## @param customStartupProbe Custom startupProbe that overrides the default one
##
customStartupProbe: {}
## @param customReadinessProbe Custom readinessProbe that overrides the default one
##
customReadinessProbe: {}
## @section Traffic Exposure Parameters
##
## application Service parameters.
##
service:
## @param service.type application service type
##
type: ClusterIP
## @param service.ports.http application service HTTP port
##
ports:
http: 8080
## @param service.nodePorts.http Node ports to expose
## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport
##
nodePorts:
http: ""
## @param service.clusterIP application service Cluster IP
## e.g.:
## clusterIP: None
##
clusterIP: ""
## @param service.extraPorts Extra ports to expose (normally used with the `sidecar` value)
##
extraPorts: []
## @param service.loadBalancerIP application service Load Balancer IP
## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-loadbalancer
##
loadBalancerIP: ""
## @param service.loadBalancerSourceRanges application service Load Balancer sources
## ref: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/#restrict-access-for-loadbalancer-service
## e.g:
## loadBalancerSourceRanges:
## - 10.10.10.0/24
##
loadBalancerSourceRanges: []
## @param service.externalTrafficPolicy application service external traffic policy
## ref https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip
##
externalTrafficPolicy: Cluster
## @param service.annotations Additional custom annotations for application service
##
annotations: {}
## @param service.sessionAffinity Session Affinity for Kubernetes service, can be "None" or "ClientIP"
## If "ClientIP", consecutive client requests will be directed to the same Pod
## ref: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
##
sessionAffinity: None
## @param service.sessionAffinityConfig Additional settings for the sessionAffinity
## sessionAffinityConfig:
## clientIP:
## timeoutSeconds: 300
##
sessionAffinityConfig: {}
## Configure the ingress resource that allows you to access the application app
## ref: https://kubernetes.io/docs/user-guide/ingress/
##
ingress:
## @param ingress.enabled Enable ingress record generation for application
##
enabled: false
## @param ingress.pathType Ingress path type
##
pathType: Prefix
## @param ingress.apiVersion Force Ingress API version (automatically detected if not set)
##
apiVersion: ""
## @param ingress.hostname Default host for the ingress resource, a host pointing to this will be created
##
hostname: ephemerup.local
## @param ingress.path Default path for the ingress record
##
path: /
## @param ingress.annotations Additional annotations for the Ingress resource. To enable certificate autogeneration, place here your cert-manager annotations.
## For a full list of possible ingress annotations, please see
## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md
## Use this parameter to set the required annotations for cert-manager, see
## ref: https://cert-manager.io/docs/usage/ingress/#supported-annotations
##
## e.g:
## annotations:
## kubernetes.io/ingress.class: nginx
## cert-manager.io/cluster-issuer: cluster-issuer-name
##
annotations: {}
## @param ingress.tls Enable TLS configuration for the host defined at `ingress.hostname` parameter
## TLS certificates will be retrieved from a TLS secret with name: `{{- printf "%s-tls" .Values.ingress.hostname }}`
## You can:
## - Use the `ingress.secrets` parameter to create this TLS secret
## - Rely on cert-manager to create it by setting the corresponding annotations
##
tls: false
tlsSecretName: ""
## @param ingress.extraPaths Any additional arbitrary paths that may need to be added to the ingress under the main host.
## For example: The ALB ingress controller requires a special rule for handling SSL redirection.
## extraPaths:
## - path: /*
## backend:
## serviceName: ssl-redirect
## servicePort: use-annotation
##
extraPaths: []
## @param ingress.selfSigned Create a TLS secret for this ingress record using self-signed certificates generated by Helm
##
selfSigned: false
## @param ingress.ingressClassName IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+)
## This is supported in Kubernetes 1.18+ and required if you have more than one IngressClass marked as the default for your cluster .
## ref: https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/
##
ingressClassName: "nginx"
## @param ingress.extraHosts An array with additional hostname(s) to be covered with the ingress record
## e.g:
## extraHosts:
## - name: aspnet-core.local
## path: /
##
extraHosts: []
## @param ingress.extraTls TLS configuration for additional hostname(s) to be covered with this ingress record
## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls
## e.g:
## extraTls:
## - hosts:
## - aspnet-core.local
## secretName: aspnet-core.local-tls
##
extraTls: []
## @param ingress.secrets Custom TLS certificates as secrets
## NOTE: 'key' and 'certificate' are expected in PEM format
## NOTE: 'name' should line up with a 'secretName' set further up
## If it is not set and you're using cert-manager, this is unneeded, as it will create a secret for you with valid certificates
## If it is not set and you're NOT using cert-manager either, self-signed certificates will be created
## It is also possible to create and manage the certificates outside of this helm chart
## Please see README.md for more information
## e.g:
## secrets:
## - name: aspnet-core.local-tls
## key: |-
## -----BEGIN RSA PRIVATE KEY-----
## ...
## -----END RSA PRIVATE KEY-----
## certificate: |-
## -----BEGIN CERTIFICATE-----
## ...
## -----END CERTIFICATE-----
##
secrets: []
## @param ingress.extraRules Additional rules to be covered with this ingress record
## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-rules
## e.g:
## extraRules:
## - host: aspnet-core.local
## http:
## path: /
## backend:
## service:
## name: aspnet-core-svc
## port:
## name: http
##
extraRules: []
## @section RBAC parameters
##
metrics:
serviceMonitor:
## @param metrics.serviceMonitor.enabled Creates a Prometheus Operator ServiceMonitor (also requires `metrics.enabled` to be `true`)
##
enabled: false
port: "http"
## @param metrics.serviceMonitor.namespace Namespace in which Prometheus is running
##
namespace: ""
## @param metrics.serviceMonitor.interval Interval at which metrics should be scraped.
## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint
## e.g:
## interval: 10s
##
interval: "30s"
## @param metrics.serviceMonitor.scrapeTimeout Timeout after which the scrape is ended
## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint
## e.g:
## scrapeTimeout: 10s
##
scrapeTimeout: "10s"
storage:
# -- Persistent volume for bolt database and uploads
longTerm:
name: "ephemerup-storage"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: standard
# -- Persistent volume for temporary files
tmp:
name: "ephemerup-tmp"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: standard

View File

@@ -1,106 +0,0 @@
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 class="col-sm-2 col-form-label">Description</label>
<label class="col-sm-10 col-form-label">{{ .Description}} </label>
</div>
<div class="mb-3 row">
<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 for download.<!-- '
+response.uploads[0].url+' -->');
$('#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>
`

View File

@@ -1,209 +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"
"fmt"
"github.com/knadh/koanf/parsers/hcl"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag"
"github.com/knadh/koanf/v2"
flag "github.com/spf13/pflag"
"github.com/alecthomas/repr"
"codeberg.org/scip/ephemerup/api"
"codeberg.org/scip/ephemerup/cfg"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
var (
cfgFile string
)
func Execute() error {
var (
conf cfg.Config
ShowVersion bool
)
f := flag.NewFlagSet("config", flag.ContinueOnError)
f.Usage = func() {
fmt.Println(f.FlagUsages())
os.Exit(0)
}
f.BoolVarP(&ShowVersion, "version", "v", false, "Print program version")
f.StringVarP(&cfgFile, "config", "c", "", "custom config file")
f.BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging")
f.StringVarP(&conf.Listen, "listen", "l", ":8080", "listen to custom ip:port (use [ip]:port for ipv6)")
f.StringVarP(&conf.StorageDir, "storagedir", "s", "/tmp", "storage directory for uploaded files")
f.StringVarP(&conf.ApiPrefix, "apiprefix", "a", "", "API endpoint path")
f.StringVarP(&conf.Url, "url", "u", "", "HTTP endpoint w/o path")
f.StringVarP(&conf.DbFile, "dbfile", "D", "/tmp/uploads.db", "Bold database file to use")
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!",
"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
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.Prefork, "prefork", "p", false, "Prefork server threads")
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")
if err := f.Parse(os.Args[1:]); err != nil {
return err
}
// exclude -6 and -4
if conf.V4only && conf.V6only {
return errors.New("You cannot mix -4 and -6!")
}
// config provider
var k = koanf.New(".")
// Load the config files provided in the commandline or the default locations
var configfiles []string
configfile, _ := f.GetString("config")
if configfile != "" {
configfiles = []string{configfile}
} else {
configfiles = []string{
"/etc/ephemerup.hcl", "/usr/local/etc/ephemerup.hcl", // unix variants
filepath.Join(os.Getenv("HOME"), ".config", "ephemerup", "ephemerup.hcl"),
filepath.Join(os.Getenv("HOME"), ".ephemerup"),
"ephemerup.hcl",
}
}
repr.Print(configfiles)
for _, cfgfile := range configfiles {
if _, err := os.Stat(cfgfile); err == nil {
if err := k.Load(file.Provider(cfgfile), hcl.Parser(true)); err != nil {
return errors.New("error loading config file: " + err.Error())
}
}
// else: we ignore the file if it doesn't exists
}
// env overrides config file
if err := k.Load(env.Provider("EPHEMERUPD_", ".", func(s string) string {
return strings.Replace(strings.ToLower(
strings.TrimPrefix(s, "EPHEMERUPD_")), "_", ".", -1)
}), nil); err != nil {
return errors.New("error loading environment: " + err.Error())
}
// command line overrides env
if err := k.Load(posflag.Provider(f, ".", k), nil); err != nil {
return errors.New("error loading config: " + err.Error())
}
// fetch values
if err := k.Unmarshal("", &conf); err != nil {
return errors.New("error unmarshalling: " + err.Error())
}
// there may exist some api context variables
GetApicontextsFromEnv(&conf)
if conf.Debug {
repr.Print(conf)
}
// Frontpage?
if conf.Frontpage != "" {
if _, err := os.Stat(conf.Frontpage); err == nil {
// it's a filename, try to use it
content, err := ioutil.ReadFile(conf.Frontpage)
if err != nil {
return errors.New("error loading config: " + err.Error())
}
// replace the filename
conf.Frontpage = string(content)
}
}
// 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 {
case ShowVersion:
fmt.Println(cfg.Getversion())
return nil
default:
conf.ApplyDefaults()
return api.Runserver(&conf, flag.Args())
}
}
/*
Get a list of Api Contexts from ENV. Useful for use with k8s secrets.
Multiple env vars are supported in this format:
EPHEMERUPD_CONTEXT_$(NAME)="<context>:<key>"
eg:
EPHEMERUPD_CONTEXT_SUPPORT="support:tymag-fycyh-gymof-dysuf-doseb-puxyx"
^^^^^^^- doesn't matter.
Modifies cfg.Config directly
*/
func GetApicontextsFromEnv(conf *cfg.Config) {
contexts := []cfg.Apicontext{}
for _, envvar := range os.Environ() {
pair := strings.SplitN(envvar, "=", 2)
if strings.HasPrefix(pair[0], "EPHEMERUPD_CONTEXT_") {
c := strings.SplitN(pair[1], ":", 2)
if len(c) == 2 {
contexts = append(contexts, cfg.Apicontext{Context: c[0], Key: c[1]})
}
}
}
contexts = append(contexts, conf.Apicontexts...)
conf.Apicontexts = contexts
}

View File

@@ -1,78 +0,0 @@
package common
import (
"fmt"
"testing"
"time"
)
func TestDuration2Seconds(t *testing.T) {
var tests = []struct {
dur string
expect int
}{
{"1d", 60 * 60 * 24},
{"1h", 60 * 60},
{"10m", 60 * 10},
{"2h4m10s", (60 * 120) + (4 * 60) + 10},
{"88u", 0},
{"19t77X what?4s", 4},
}
for _, tt := range tests {
testname := fmt.Sprintf("duration-%s", tt.dur)
t.Run(testname, func(t *testing.T) {
seconds := duration2int(tt.dur)
if seconds != tt.expect {
t.Errorf("got %d, want %d", seconds, tt.expect)
}
})
}
}
func TestIsExpired(t *testing.T) {
var tests = []struct {
expire string
start time.Time
expect bool
}{
{"3s", time.Now(), true},
{"1d", time.Now(), false},
}
for _, tt := range tests {
testname := fmt.Sprintf("isexpired-%s-%s", tt.start, tt.expire)
t.Run(testname, func(t *testing.T) {
time.Sleep(5 * time.Second)
got := IsExpired(tt.start, tt.expire)
if got != tt.expect {
t.Errorf("got %t, want %t", got, tt.expect)
}
})
}
}
func TestUntaint(t *testing.T) {
var tests = []struct {
want string
input string
expect string
wanterr bool
}{
{`[^a-zA-Z0-9\-]`, "ab23-bb43-beef", "ab23-bb43-beef", false},
{`[^a-zA-Z0-9\-]`, "`cat passwd`+ab23-bb43-beef", "catpasswdab23-bb43-beef", true},
}
for _, tt := range tests {
testname := fmt.Sprintf("untaint-%s-%s", tt.want, tt.expect)
t.Run(testname, func(t *testing.T) {
untainted, err := Untaint(tt.input, tt.want)
if untainted != tt.expect {
t.Errorf("got %s, want %s", untainted, tt.expect)
}
if err != nil && !tt.wanterr {
t.Errorf("got error: %s", err)
}
})
}
}

View File

@@ -1,3 +0,0 @@
module codeberg.org/scip/ephemerup/common
go 1.18

View File

@@ -1,96 +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 common
import (
"regexp"
"strconv"
"time"
)
// https://gist.github.com/rhcarvalho/9338c3ff8850897c68bc74797c5dc25b
// Timestamp is like time.Time, but knows how to unmarshal from JSON
// Unix timestamp numbers or RFC3339 strings, and marshal back into
// the same JSON representation.
type Timestamp struct {
time.Time
rfc3339 bool
}
func (t Timestamp) MarshalJSON() ([]byte, error) {
if t.rfc3339 {
return t.Time.MarshalJSON()
}
return t.formatUnix()
}
func (t *Timestamp) UnmarshalJSON(data []byte) error {
err := t.Time.UnmarshalJSON(data)
if err != nil {
return t.parseUnix(data)
}
t.rfc3339 = true
return nil
}
func (t Timestamp) formatUnix() ([]byte, error) {
sec := float64(t.Time.UnixNano()) * float64(time.Nanosecond) / float64(time.Second)
return strconv.AppendFloat(nil, sec, 'f', -1, 64), nil
}
func (t *Timestamp) parseUnix(data []byte) error {
f, err := strconv.ParseFloat(string(data), 64)
if err != nil {
return err
}
t.Time = time.Unix(0, int64(f*float64(time.Second/time.Nanosecond)))
return nil
}
/*
We could use time.ParseDuration(), but this doesn't support days.
We could also use github.com/xhit/go-str2duration/v2, which does
the job, but it's just another dependency, just for this little
gem. And we don't need a time.Time value.
Convert a duration into seconds (int).
Valid time units are "s", "m", "h" and "d".
*/
func Duration2int(duration string) int {
re := regexp.MustCompile(`(\d+)([dhms])`)
seconds := 0
for _, match := range re.FindAllStringSubmatch(duration, -1) {
if len(match) == 3 {
v, _ := strconv.Atoi(match[1])
switch match[2][0] {
case 'd':
seconds += v * 86400
case 'h':
seconds += v * 3600
case 'm':
seconds += v * 60
case 's':
seconds += v
}
}
}
return seconds
}

View File

@@ -1,219 +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 common
import (
"encoding/json"
"fmt"
"regexp"
)
// used to return to the api client
type Result struct {
Success bool `json:"success"`
Message string `json:"message"`
Code int `json:"code"`
}
// upload or form structs
type Dbentry interface {
Getcontext(j []byte) (string, error)
Marshal() ([]byte, error)
MatchExpire(r *regexp.Regexp) bool
MatchDescription(r *regexp.Regexp) bool
MatchFile(r *regexp.Regexp) bool
MatchCreated(r *regexp.Regexp) bool
IsType(t int) bool
}
type Upload struct {
Type int `json:"type"`
Id string `json:"id"`
Expire string `json:"expire"`
File string `json:"file"` // final filename (visible to the downloader)
Members []string `json:"members"` // contains multiple files, so File is an archive
Created Timestamp `json:"uploaded"`
Context string `json:"context"`
Description string `json:"description"`
Url string `json:"url"`
}
// this one is also used for marshalling to the client
type Response struct {
Uploads []*Upload `json:"uploads"`
Forms []*Form `json:"forms"`
// integrate the Result struct so we can signal success
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)
Type int `json:"type"`
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
}
func (upload Upload) MatchExpire(r *regexp.Regexp) bool {
return r.MatchString(upload.Expire)
}
func (upload Upload) MatchDescription(r *regexp.Regexp) bool {
return r.MatchString(upload.Description)
}
func (upload Upload) MatchCreated(r *regexp.Regexp) bool {
return r.MatchString(upload.Created.Time.String())
}
func (upload Upload) MatchFile(r *regexp.Regexp) bool {
return r.MatchString(upload.File)
}
func (form Form) MatchExpire(r *regexp.Regexp) bool {
return r.MatchString(form.Expire)
}
func (form Form) MatchDescription(r *regexp.Regexp) bool {
return r.MatchString(form.Description)
}
func (form Form) MatchCreated(r *regexp.Regexp) bool {
return r.MatchString(form.Created.Time.String())
}
func (form Form) MatchFile(r *regexp.Regexp) bool {
return false
}
func (upload Upload) IsType(t int) bool {
if upload.Type == t {
return true
}
return false
}
func (form Form) IsType(t int) bool {
if form.Type == t {
return true
}
return false
}
/*
Response methods
*/
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
}
}

View File

@@ -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 common
import (
"errors"
"regexp"
)
/*
Untaint user input, that is: remove all non supported chars.
wanted is a regexp matching chars we shall leave. Everything else
will be removed. Eg:
untainted := Untaint(input, `[^a-zA-Z0-9\-]`)
Returns a new string and an error if the input string has been
modified. It's the callers choice to decide what to do about
it. You may ignore the error and use the untainted string or bail
out.
*/
func Untaint(input string, wanted *regexp.Regexp) (string, error) {
untainted := wanted.ReplaceAllString(input, "")
if len(untainted) != len(input) {
return untainted, errors.New("Invalid input string!")
}
return untainted, nil
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 KiB

View File

@@ -1,6 +0,0 @@
version: "3.9"
services:
ephemerup:
build: .
ports:
- "8080:8080"

View File

@@ -1,26 +0,0 @@
# -*-ruby-*-
listen = ":8080"
bodylimit = 10000
apicontexts = [
{
context = "root"
key = "0fddbff5d8010f81cd28a7d77f3e38981b13d6164c2fd6e1c3f60a4287630c37",
},
{
context = "foo",
key = "970b391f22f515d96b3e9b86a2c62c627968828e47b356994d2e583188b4190a"
}
]
#url = "https://sokrates.daemon.de"
# this is the root context with all permissions
super = "root"
mail = {
server = "localhost"
port = "25"
from = "root@localhost"
password = ""
}

46
go.mod
View File

@@ -1,46 +0,0 @@
module codeberg.org/scip/ephemerup
go 1.24
require (
codeberg.org/scip/ephemerup/common v0.0.0-20231001134723-a1b3fe6f25d1
github.com/alecthomas/repr v0.2.0
github.com/gofiber/fiber/v2 v2.42.0
github.com/gofiber/keyauth/v2 v2.1.32
github.com/google/uuid v1.6.0
github.com/knadh/koanf/parsers/hcl v0.1.0
github.com/knadh/koanf/providers/env v0.1.0
github.com/knadh/koanf/providers/file v0.1.0
github.com/knadh/koanf/providers/posflag v0.1.0
github.com/knadh/koanf/v2 v2.0.1
github.com/maxatome/go-testdeep v1.13.0
github.com/spf13/pflag v1.0.10
go.etcd.io/bbolt v1.3.12
)
require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/klauspost/compress v1.15.15 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect
github.com/tinylib/msgp v1.1.9 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.44.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.29.0 // indirect
)
replace codeberg.org/scip/ephemerup/common => ./common

135
go.sum
View File

@@ -1,135 +0,0 @@
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gofiber/fiber/v2 v2.42.0 h1:Fnp7ybWvS+sjNQsFvkhf4G8OhXswvB6Vee8hM/LyS+8=
github.com/gofiber/fiber/v2 v2.42.0/go.mod h1:3+SGNjqMh5VQH5Vz2Wdi43zTIV16ktlFd3x3R6O1Zlc=
github.com/gofiber/keyauth/v2 v2.1.32 h1:ExnCEUlgF4pQn8BLPa4VMVR12R78KtrJe0h4SqQAK5Q=
github.com/gofiber/keyauth/v2 v2.1.32/go.mod h1:zeJzlvfvjMH31A1b0NaK3FkO7mCoOiK02Xl748XHHNI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/hcl v0.1.0 h1:PuAAdRMXbxmhwzZftiQBEtWIKc3EbRHk/Fi+olo02z4=
github.com/knadh/koanf/parsers/hcl v0.1.0/go.mod h1:7ClRvH1oP5ne8SfaDZZBK28/o9r4rek0PC4Vrc8qdvE=
github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg=
github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ=
github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U=
github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0=
github.com/knadh/koanf/v2 v2.0.0 h1:XPQ5ilNnwnNaHrfQ1YpTVhUAjcGHnEKA+lRpipQv02Y=
github.com/knadh/koanf/v2 v2.0.0/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g=
github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
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/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d h1:Q+gqLBOPkFGHyCJxXMRqtUgUbTjI8/Ze8vu8GGyNFwo=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE=
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.44.0 h1:R+gLUhldIsfg1HokMuQjdQ5bh9nuXHPIfvkYUu9eR5Q=
github.com/valyala/fasthttp v1.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.etcd.io/bbolt v1.3.12 h1:UAxZAIuJqzFwByP19gZC3zd5robK3FOangrGS+Fdczg=
go.etcd.io/bbolt v1.3.12/go.mod h1:Gi2toLZr1jFkuReJm+yEPn7H8wk6ooptePtHYCbCS1g=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

30
main.go
View File

@@ -1,30 +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 main
import (
"log"
"codeberg.org/scip/ephemerup/cmd"
)
func main() {
err := cmd.Execute()
if err != nil {
log.Fatal(err.Error())
}
}

View File

@@ -1,84 +0,0 @@
#!/bin/bash
# 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/>.
# get list with: go tool dist list
DIST="darwin/amd64
freebsd/amd64
linux/amd64
windows/amd64"
daemon="$1"
client="$2"
version="$3"
if test -z "$version"; then
echo "Usage: $0 <daemon name> <client name> <release version>"
exit 1
fi
rm -rf releases
mkdir -p releases
for D in $DIST; do
os=${D/\/*/}
arch=${D/*\//}
binfile="releases/${daemon}-${os}-${arch}-${version}"
clientfile="releases/${client}-${os}-${arch}-${version}"
tardir="${daemon}-${os}-${arch}-${version}"
tarfile="releases/${daemon}-${os}-${arch}-${version}.tar.gz"
set -x
GOOS=${os} GOARCH=${arch} CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags \
"-extldflags=-static -s -X 'codeberg.org/scip/ephemerup/cfg.VERSION=${version}'" -o ${binfile}
#GOOS=${os} GOARCH=${arch} go build -o ${binfile} -ldflags "-X 'codeberg.org/scip/ephemerup/cfg.VERSION=${version}'"
cd $client
GOOS=${os} GOARCH=${arch} go build -o ../${clientfile} -ldflags \
"-X 'codeberg.org/scip/ephemerup/upctl/cfg.VERSION=${version}'"
cd -
mkdir -p ${tardir}
cp ${binfile} ${clientfile} README.md LICENSE ${tardir}/
echo 'daemon = ephemerupd
client = upctl
PREFIX = /usr/local
UID = root
GID = 0
install-client:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(client) $(PREFIX)/bin/
install -o $(UID) -g $(GID) -m 444 $(client).1 $(PREFIX)/man/man1/
install: install-client
install -d -o $(UID) -g $(GID) $(PREFIX)/sbin
install -o $(UID) -g $(GID) -m 555 $(daemon) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(daemon).1 $(PREFIX)/man/man1/' > ${tardir}/Makefile
tar cpzf ${tarfile} ${tardir}
for file in ${binfile} ${tarfile} ${clientfile}; do
sha256sum ${file} | cut -d' ' -f1 > ${file}.sha256
done
rm -rf ${tardir}
set +x
done

View File

@@ -1,3 +0,0 @@
# -*-ruby-*-
listen = ":8080"
bodylimit = 10001

View File

@@ -1,102 +0,0 @@
<!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 class="col-sm-2 col-form-label">Description</label>
<label class="col-sm-10 col-form-label">{{ .Description}} </label>
</div>
<div class="mb-3 row">
<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 for download.<!-- '
+response.uploads[0].url+' -->');
$('#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>

View File

@@ -1,78 +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/>.
#
# no need to modify anything below
tool = upctl
version = $(shell egrep "= .v" cfg/config.go | cut -d'=' -f2 | cut -d'"' -f 2)
archs = android darwin freebsd linux netbsd openbsd windows
PREFIX = /usr/local
UID = root
GID = 0
BRANCH = $(shell git branch --show-current)
COMMIT = $(shell git rev-parse --short=8 HEAD)
BUILD = $(shell date +%Y.%m.%d.%H%M%S)
VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version))
HAVE_POD := $(shell pod2text -h 2>/dev/null)
HAVE_LINT:= $(shell golangci-lint -h 2>/dev/null)
all: lint buildlocal
lint:
ifdef HAVE_LINT
golangci-lint run
endif
buildlocal:
go build -ldflags "-X 'codeberg.org/scip/ephemerup/upctl/cfg.VERSION=$(VERSION)'"
release:
./mkrel.sh $(tool) $(version)
gh release create $(version) --generate-notes releases/*
install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
clean:
rm -rf $(tool) releases coverage.out
test:
go test -v ./...
singletest:
@echo "Call like this: ''make singletest TEST=TestX1 MOD=lib"
go test -run $(TEST) codeberg.org/scip/upctl/$(MOD)
cover-report:
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out
show-versions: buildlocal
@echo "### upctl version:"
@./upctl --version
@echo
@echo "### go module versions:"
@go list -m all
@echo
@echo "### go version used for building:"
@grep -m 1 go go.mod
goupdate:
go get -t -u=patch ./...

View File

@@ -1,14 +0,0 @@
## upload control client (upctl)
```
upctl access.log
Uploaded: access.log
Expires: after first access
Download URL: http://localhost:8080/api/getfile/cc70324b-5c8e-4ca2-bb32-10aa0eeb6dbf/2023-02-14-19-18-access.log
upctl access.log error.log
Uploaded: access.log
Uploaded: error.log
Expires: after first access
Download URL: http://localhost:8080/api/getfile/cc70324b-5c8e-4ca2-bb32-10aa0eeb6dbf/2023-02-14-19-18-data.zip
```

View File

@@ -1,63 +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 cfg
import (
"fmt"
//"strings"
)
const Version string = "v0.0.1"
var VERSION string // maintained by -x
type Config struct {
// globals
Endpoint string
Debug bool
Retries int
Silent bool
// used for authentication
Apikey string
// upload
Expire string
// used for filtering (list command)
Apicontext string
// required to intercept requests using httpmock in tests
Mock bool
// used to filter lists
Query string
// required for forms
Description string
Notify string
}
func Getversion() string {
// main program version
// generated version string, used by -v contains cfg.Version on
// main branch, and cfg.Version-$branch-$lastcommit-$date on
// development branch
return fmt.Sprintf("This is upctl version %s", VERSION)
}

View File

@@ -1,182 +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"
"errors"
"github.com/spf13/cobra"
"codeberg.org/scip/ephemerup/common"
"codeberg.org/scip/ephemerup/upctl/cfg"
"codeberg.org/scip/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 {
return cmd.Help()
}
return nil
},
}
formCmd.Aliases = append(formCmd.Aliases, "frm")
formCmd.Aliases = append(formCmd.Aliases, "f")
formCmd.AddCommand(FormCreateCommand(conf))
formCmd.AddCommand(FormListCommand(conf))
formCmd.AddCommand(FormDeleteCommand(conf))
formCmd.AddCommand(FormDescribeCommand(conf))
formCmd.AddCommand(FormModifyCommand(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
}
func FormModifyCommand(conf *cfg.Config) *cobra.Command {
var formModifyCmd = &cobra.Command{
Use: "modify [options] <id>",
Short: "Modify a form",
Long: `Modify an existing form.`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
return lib.Modify(os.Stdout, conf, args, common.TypeForm)
},
}
// options
formModifyCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "",
"Expire setting: asap or duration (accepted shortcuts: dmh)")
formModifyCmd.PersistentFlags().StringVarP(&conf.Description, "description", "D", "",
"Description of the form")
formModifyCmd.PersistentFlags().StringVarP(&conf.Notify, "notify", "n", "",
"Email address to get notified when consumer has uploaded files")
formModifyCmd.Aliases = append(formModifyCmd.Aliases, "mod")
formModifyCmd.Aliases = append(formModifyCmd.Aliases, "change")
return formModifyCmd
}
func FormListCommand(conf *cfg.Config) *cobra.Command {
var listCmd = &cobra.Command{
Use: "list [options]",
Short: "List formss",
Long: `List formss.`,
RunE: func(cmd *cobra.Command, args []string) error {
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
return lib.List(os.Stdout, conf, nil, common.TypeForm)
},
}
// options
listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context")
listCmd.PersistentFlags().StringVarP(&conf.Query, "query", "q", "", "Filter by given query regexp")
listCmd.Aliases = append(listCmd.Aliases, "ls")
listCmd.Aliases = append(listCmd.Aliases, "l")
return listCmd
}
func FormDeleteCommand(conf *cfg.Config) *cobra.Command {
var deleteCmd = &cobra.Command{
Use: "delete [options] <id>",
Short: "Delete an form",
Long: `Delete an form identified by its id`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("No id specified to delete!")
}
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
return lib.Delete(os.Stdout, conf, args, common.TypeForm)
},
}
deleteCmd.Aliases = append(deleteCmd.Aliases, "rm")
deleteCmd.Aliases = append(deleteCmd.Aliases, "d")
return deleteCmd
}
func FormDescribeCommand(conf *cfg.Config) *cobra.Command {
var listCmd = &cobra.Command{
Use: "describe [options] form-id",
Long: "Show detailed informations about an form object.",
Short: `Describe an form.`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("No id specified to delete!")
}
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
return lib.Describe(os.Stdout, conf, args, common.TypeForm)
},
}
listCmd.Aliases = append(listCmd.Aliases, "des")
listCmd.Aliases = append(listCmd.Aliases, "info")
listCmd.Aliases = append(listCmd.Aliases, "i")
return listCmd
}

View File

@@ -1,177 +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"
"codeberg.org/scip/ephemerup/common"
"codeberg.org/scip/ephemerup/upctl/cfg"
"codeberg.org/scip/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.PersistentFlags().StringVarP(&conf.Description, "description", "D", "", "Description of the form")
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, common.TypeUpload)
},
}
// options
listCmd.PersistentFlags().StringVarP(&conf.Apicontext, "apicontext", "", "", "Filter by given API context")
listCmd.PersistentFlags().StringVarP(&conf.Query, "query", "q", "", "Filter by given query regexp")
listCmd.Aliases = append(listCmd.Aliases, "ls")
listCmd.Aliases = append(listCmd.Aliases, "l")
return listCmd
}
func 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, common.TypeUpload)
},
}
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, common.TypeUpload)
},
}
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
}
func ModifyCommand(conf *cfg.Config) *cobra.Command {
var uploadModifyCmd = &cobra.Command{
Use: "modify [options] <id>",
Short: "Modify an upload",
Long: `Modify an existing upload.`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
return lib.Modify(os.Stdout, conf, args, common.TypeUpload)
},
}
// options
uploadModifyCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "",
"Expire setting: asap or duration (accepted shortcuts: dmh)")
uploadModifyCmd.PersistentFlags().StringVarP(&conf.Description, "description", "D", "",
"Description of the upload")
uploadModifyCmd.Aliases = append(uploadModifyCmd.Aliases, "mod")
uploadModifyCmd.Aliases = append(uploadModifyCmd.Aliases, "change")
return uploadModifyCmd
}

View File

@@ -1,156 +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"
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"codeberg.org/scip/ephemerup/upctl/cfg"
"os"
"strings"
)
var (
cfgFile string
)
func completion(cmd *cobra.Command, mode string) error {
switch mode {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
return errors.New("Invalid shell parameter! Valid ones: bash|zsh|fish|powershell")
}
}
func Execute() {
var (
conf cfg.Config
ShowVersion bool
ShowCompletion string
)
var rootCmd = &cobra.Command{
Use: "upctl [options]",
Short: "upload api client",
Long: `Manage files on an upload api server.`,
RunE: func(cmd *cobra.Command, args []string) error {
if ShowVersion {
fmt.Println(cfg.Getversion())
return nil
}
if len(ShowCompletion) > 0 {
return completion(cmd, ShowCompletion)
}
if len(args) == 0 {
return errors.New("No command specified!")
}
return nil
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initConfig(cmd, &conf)
},
}
// options
rootCmd.PersistentFlags().BoolVarP(&ShowVersion, "version", "v", false, "Print program version")
rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging")
rootCmd.PersistentFlags().BoolVarP(&conf.Silent, "silent", "s", false, "Disable progress bar and other noise")
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "custom config file")
rootCmd.PersistentFlags().IntVarP(&conf.Retries, "retries", "r", 3, "How often shall we retry to access our endpoint")
rootCmd.PersistentFlags().StringVarP(&conf.Endpoint, "endpoint", "p", "http://localhost:8080/api/v1", "upload api endpoint url")
rootCmd.PersistentFlags().StringVarP(&conf.Apikey, "apikey", "a", "", "Api key to use")
rootCmd.AddCommand(UploadCommand(&conf))
rootCmd.AddCommand(ListCommand(&conf))
rootCmd.AddCommand(DeleteCommand(&conf))
rootCmd.AddCommand(DescribeCommand(&conf))
rootCmd.AddCommand(DownloadCommand(&conf))
rootCmd.AddCommand(ModifyCommand(&conf))
// forms are being handled with its own subcommand
rootCmd.AddCommand(FormCommand(&conf))
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
// initialize viper, read config and ENV, bind flags
func initConfig(cmd *cobra.Command, cfg *cfg.Config) error {
v := viper.New()
viper.SetConfigType("hcl")
if cfgFile != "" {
// Use config file from the flag.
v.SetConfigFile(cfgFile)
} else {
v.SetConfigName("upctl")
// default location[s]
v.AddConfigPath(".")
v.AddConfigPath("$HOME/.config/upctl")
}
// ignore read errors, report all others
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return err
}
}
// fmt.Println("Using config file:", v.ConfigFileUsed())
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
v.AutomaticEnv()
v.SetEnvPrefix("upctl")
// map flags to viper
return bindFlags(cmd, v)
}
// bind flags to viper settings (env+cfgfile)
func bindFlags(cmd *cobra.Command, v *viper.Viper) error {
var fail error
cmd.Flags().VisitAll(func(f *pflag.Flag) {
// map flag name to config variable
configName := f.Name
// use config variable if flag is not set and config is set
if !f.Changed && v.IsSet(configName) {
val := v.Get(configName)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
fail = err
}
}
})
return fail
}

View File

@@ -1,46 +0,0 @@
module codeberg.org/scip/ephemerup/upctl
go 1.24.0
require (
codeberg.org/scip/ephemerup/common v0.0.0-20231001134723-a1b3fe6f25d1
github.com/imroc/req/v3 v3.56.0
github.com/jarcoal/httpmock v1.3.1
github.com/olekukonko/tablewriter v0.0.5
github.com/schollz/progressbar/v3 v3.13.1
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.15.0
)
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.56.0 // indirect
github.com/refraction-networking/utls v1.8.1 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace codeberg.org/scip/ephemerup/common => ../common

View File

@@ -1,552 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.56.0 h1:t6YdqqerYBXhZ9+VjqsQs5wlKxdUNEvsgBhxWc1AEEo=
github.com/imroc/req/v3 v3.56.0/go.mod h1:cUZSooE8hhzFNOrAbdxuemXDQxFXLQTnu3066jr7ZGk=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE=
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -1,421 +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 lib
import (
"encoding/json"
"errors"
"fmt"
//"github.com/alecthomas/repr"
"io"
"mime"
"os"
"path/filepath"
"regexp"
"time"
"codeberg.org/scip/ephemerup/common"
"codeberg.org/scip/ephemerup/upctl/cfg"
"github.com/imroc/req/v3"
"github.com/jarcoal/httpmock"
"github.com/schollz/progressbar/v3"
)
type Response struct {
Code int `json:"code"`
Success bool `json:"success"`
Message string `json:"message"`
}
type Request struct {
R *req.Request
Url string
}
type ListParams struct {
Apicontext string `json:"apicontext"`
Query string `json:"query"`
}
const Maxwidth = 12
/*
Create a new request object for outgoing queries
*/
func Setup(c *cfg.Config, path string) *Request {
client := req.C()
if c.Debug {
client.DevMode()
}
client.SetUserAgent("upctl-" + cfg.VERSION)
R := client.R()
if c.Retries > 0 {
// Enable retry and set the maximum retry count.
R.SetRetryCount(c.Retries).
// Set the retry sleep interval with a commonly used
// algorithm: capped exponential backoff with jitter
// (https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/).
SetRetryBackoffInterval(1*time.Second, 5*time.Second).
AddRetryHook(func(resp *req.Response, err error) {
req := resp.Request.RawRequest
if c.Debug {
fmt.Println("Retrying endpoint request:", req.Method, req.URL, err)
}
})
}
if len(c.Apikey) > 0 {
client.SetCommonBearerAuthToken(c.Apikey)
}
if c.Mock {
// intercept, used by unit tests
httpmock.ActivateNonDefault(client.GetClient())
}
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 {
for _, file := range args {
info, err := os.Stat(file)
if os.IsNotExist(err) {
return err
}
if info.IsDir() {
err := filepath.Walk(file, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
rq.R.SetFile("upload[]", path)
}
return nil
})
if err != nil {
return err
}
} else {
rq.R.SetFile("upload[]", file)
}
}
return nil
}
/*
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
rq := Setup(c, "/uploads")
// collect files to upload from @argv
if err := GatherFiles(rq, args); err != nil {
return err
}
if !c.Silent {
// progres bar
bar := progressbar.Default(100)
var left float64
rq.R.SetUploadCallbackWithInterval(func(info req.UploadInfo) {
left = float64(info.UploadedSize) / float64(info.FileSize) * 100.0
if err := bar.Add(int(left)); err != nil {
fmt.Print("\r")
}
}, 10*time.Millisecond)
}
// actual post w/ settings
resp, err := rq.R.
SetFormData(map[string]string{
"expire": c.Expire,
"description": c.Description,
}).
Post(rq.Url)
if err != nil {
return err
}
if err := HandleResponse(c, resp); err != nil {
return err
}
return RespondExtended(w, resp)
}
func List(w io.Writer, c *cfg.Config, args []string, typ int) error {
var rq *Request
switch typ {
case common.TypeUpload:
rq = Setup(c, "/uploads")
case common.TypeForm:
rq = Setup(c, "/forms")
}
params := &ListParams{Apicontext: c.Apicontext, Query: c.Query}
resp, err := rq.R.
SetBodyJsonMarshal(params).
Get(rq.Url)
if err != nil {
return err
}
if err := HandleResponse(c, resp); err != nil {
return err
}
switch typ {
case common.TypeUpload:
return UploadsRespondTable(w, resp)
case common.TypeForm:
return FormsRespondTable(w, resp)
}
return nil
}
func Delete(w io.Writer, c *cfg.Config, args []string, typ int) error {
for _, id := range args {
var rq *Request
caption := "Upload"
switch typ {
case common.TypeUpload:
rq = Setup(c, "/uploads/"+id)
case common.TypeForm:
rq = Setup(c, "/forms/"+id)
caption = "Form"
}
resp, err := rq.R.Delete(rq.Url)
if err != nil {
return err
}
if err := HandleResponse(c, resp); err != nil {
return err
}
fmt.Fprintf(w, "%s %s successfully deleted.\n", caption, id)
}
return nil
}
func Describe(w io.Writer, c *cfg.Config, args []string, typ int) error {
if len(args) == 0 {
return errors.New("No id provided!")
}
var rq *Request
id := args[0] // we describe only 1 object
switch typ {
case common.TypeUpload:
rq = Setup(c, "/uploads/"+id)
case common.TypeForm:
rq = Setup(c, "/forms/"+id)
}
resp, err := rq.R.Get(rq.Url)
if err != nil {
return err
}
if err := HandleResponse(c, resp); err != nil {
return err
}
return RespondExtended(w, resp)
}
func Download(w io.Writer, c *cfg.Config, args []string) error {
if len(args) == 0 {
return errors.New("No id provided!")
}
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 {
if err := bar.Add(1); err != nil {
fmt.Print("\r")
}
}
}
rq.R.SetDownloadCallback(callback)
}
resp, err := rq.R.
SetOutputFile(id).
Get(rq.Url)
if err != nil {
return err
}
if !resp.IsSuccessState() {
return fmt.Errorf("bad response: %s", resp.Status)
}
_, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
if err != nil {
os.Remove(id)
return err
}
filename := params["filename"]
if filename == "" {
os.Remove(id)
return fmt.Errorf("No filename provided!")
}
cleanfilename, _ := common.Untaint(filename, regexp.MustCompile(`[^a-zA-Z0-9\-\._]`))
if err := os.Rename(id, cleanfilename); err != nil {
os.Remove(id)
return fmt.Errorf("\nUnable to rename file: %s", err.Error())
}
fmt.Fprintf(w, "%s successfully downloaded to file %s.", id, cleanfilename)
return nil
}
func Modify(w io.Writer, c *cfg.Config, args []string, typ int) error {
id := args[0]
var rq *Request
// setup url, req.Request, timeout handling etc
switch typ {
case common.TypeUpload:
rq = Setup(c, "/uploads/"+id)
rq.R.
SetBody(&common.Upload{
Expire: c.Expire,
Description: c.Description,
})
case common.TypeForm:
rq = Setup(c, "/forms/"+id)
rq.R.
SetBody(&common.Form{
Expire: c.Expire,
Description: c.Description,
Notify: c.Notify,
})
}
// actual put w/ settings
resp, err := rq.R.Put(rq.Url)
if err != nil {
return err
}
if err := HandleResponse(c, resp); err != nil {
return err
}
return RespondExtended(w, resp)
}
/**** 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.
SetBody(&common.Form{
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)
}

View File

@@ -1,409 +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 lib
import (
//"github.com/alecthomas/repr"
"bytes"
"fmt"
"github.com/jarcoal/httpmock"
"codeberg.org/scip/ephemerup/common"
"codeberg.org/scip/ephemerup/upctl/cfg"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
)
const endpoint string = "http://localhost:8080/v1"
type Unit struct {
name string
apikey string // set to something else than "token" to fail auth
wantfail bool // true: expect to fail
files []string // path relative to ./t/
expect string // regex used to parse the output
sendcode int // for httpmock
sendjson string // struct to respond with
sendfile string // bare file content to be sent
route string // dito
method string // method to use
}
// simulate our ephemerup server
func Intercept(tt Unit) {
httpmock.RegisterResponder(tt.method, endpoint+tt.route,
func(request *http.Request) (*http.Response, error) {
var resp *http.Response
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
})
}
// 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) {
conf := &cfg.Config{
Mock: true,
Apikey: "token",
Endpoint: endpoint,
Silent: true,
}
tests := []Unit{
{
name: "upload-file",
apikey: "token",
wantfail: false,
route: "/uploads",
sendcode: 200,
sendjson: `{"success": true}`,
files: []string{"../t/t1"}, // pwd is lib/ !
method: "POST",
},
{
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",
wantfail: true,
route: "/uploads",
sendcode: 200,
sendjson: `{"success": false}`,
files: []string{"../t/none"},
method: "POST",
},
{
name: "upload-catch-no-access",
apikey: "token",
wantfail: true,
route: "/uploads",
sendcode: 403,
sendjson: `{"success": false}`,
files: []string{"../t/t1"},
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 _, unit := range tests {
var w bytes.Buffer
Intercept(unit)
Check(t, unit, &w, UploadFiles(&w, conf, unit.files))
}
}
func TestList(t *testing.T) {
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: "list",
apikey: "token",
wantfail: false,
route: "/uploads",
sendcode: 200,
sendjson: listing,
files: []string{},
method: "GET",
expect: `cc2c965a\s*asap\s*foo\s*2023-03-21`, // 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 _, unit := range tests {
var w bytes.Buffer
Intercept(unit)
Check(t, unit, &w, List(&w, conf, []string{}, common.TypeUpload))
}
}
func TestDescribe(t *testing.T) {
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`,
},
{
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, common.TypeUpload))
}
}
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, common.TypeUpload))
}
}
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)
}
}
}
}

View File

@@ -1,193 +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 lib
import (
"encoding/json"
"errors"
"fmt"
"io"
"sort"
"strings"
"time"
"codeberg.org/scip/ephemerup/common"
req "github.com/imroc/req/v3"
"github.com/olekukonko/tablewriter"
)
// make a human readable version of the expire setting
func prepareExpire(expire string, start common.Timestamp) string {
switch expire {
case "asap":
return "On first access"
default:
return time.Unix(start.Unix()+int64(common.Duration2int(expire)), 0).
Format("2006-01-02 15:04:05")
}
}
// generic table writer
func WriteTable(w io.Writer, headers []string, data [][]string) {
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
table.SetHeader(headers)
table.AppendBulk(data)
table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetTablePadding("\t")
table.SetNoWhiteSpace(true)
table.Render()
fmt.Fprintln(w, tableString.String())
}
/*
Print output like psql \x
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)
// we shall only have 1 element, however, if we ever support more, here we go
for _, entry := range response.Uploads {
expire := prepareExpire(entry.Expire, entry.Created)
fmt.Fprintf(w, format, "Upload-Id", entry.Id)
fmt.Fprintf(w, format, "Description", entry.Description)
fmt.Fprintf(w, format, "Expire", expire)
fmt.Fprintf(w, format, "Context", entry.Context)
fmt.Fprintf(w, format, "Created", entry.Created)
fmt.Fprintf(w, format, "Filename", entry.File)
fmt.Fprintf(w, format, "Url", entry.Url)
fmt.Fprintln(w)
}
for _, entry := range response.Forms {
expire := prepareExpire(entry.Expire, entry.Created)
fmt.Fprintf(w, format, "Form-Id", entry.Id)
fmt.Fprintf(w, format, "Description", entry.Description)
fmt.Fprintf(w, format, "Expire", expire)
fmt.Fprintf(w, format, "Context", entry.Context)
fmt.Fprintf(w, format, "Created", entry.Created)
fmt.Fprintf(w, format, "Notify", entry.Notify)
fmt.Fprintf(w, format, "Url", entry.Url)
fmt.Fprintln(w)
}
}
// extract an common.Response{} struct from json response
func GetResponse(resp *req.Response) (*common.Response, error) {
response := common.Response{}
if err := json.Unmarshal([]byte(resp.String()), &response); err != nil {
return nil, errors.New("Could not unmarshall JSON response: " + err.Error())
}
if !response.Success {
return nil, errors.New(response.Message)
}
return &response, nil
}
// turn the Uploads{} struct into a table and print it
func UploadsRespondTable(w io.Writer, resp *req.Response) error {
response, err := GetResponse(resp)
if err != nil {
return err
}
if response.Message != "" {
fmt.Fprintln(w, response.Message)
}
sort.SliceStable(response.Uploads, func(i, j int) bool {
return response.Uploads[i].Created.Time.Unix() < response.Uploads[j].Created.Time.Unix()
})
// tablewriter
data := [][]string{}
for _, entry := range response.Uploads {
data = append(data, []string{
entry.Id, entry.Description, entry.Expire, entry.Context,
entry.Created.Format("2006-01-02 15:04:05"), entry.File,
})
}
WriteTable(w, []string{"UPLOAD-ID", "DESCRIPTION", "EXPIRE", "CONTEXT", "CREATED", "FILE"}, data)
return nil
}
// turn the Forms{} struct into a table and print it
func FormsRespondTable(w io.Writer, resp *req.Response) error {
response, err := GetResponse(resp)
if err != nil {
return err
}
if response.Message != "" {
fmt.Fprintln(w, response.Message)
}
sort.SliceStable(response.Forms, func(i, j int) bool {
return response.Forms[i].Created.Time.Unix() < response.Forms[j].Created.Time.Unix()
})
// tablewriter
data := [][]string{}
for _, entry := range response.Forms {
data = append(data, []string{
entry.Id, entry.Description, entry.Expire, entry.Context,
entry.Created.Format("2006-01-02 15:04:05"), entry.Notify,
})
}
WriteTable(w, []string{"FORM-ID", "DESCRIPTION", "EXPIRE", "CONTEXT", "CREATED", "NOTIFY"}, data)
return nil
}
// turn the Uploads{} struct into xtnd output and print it
func RespondExtended(w io.Writer, resp *req.Response) error {
response, err := GetResponse(resp)
if err != nil {
return err
}
if response.Message != "" {
fmt.Fprintln(w, response.Message)
}
WriteExtended(w, response)
return nil
}

View File

@@ -1,25 +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 main
import (
"codeberg.org/scip/ephemerup/upctl/cmd"
)
func main() {
cmd.Execute()
}

View File

@@ -1,2 +0,0 @@
endpoint = "http://localhost:8080/api/v1"
apikey = "0fddbff5d8010f81cd28a7d77f3e38981b13d6164c2fd6e1c3f60a4287630c37"

View File

@@ -1 +0,0 @@
Wed Mar 29 03:01:21 PM CEST 2023

Binary file not shown.

View File

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