Browse Source

Refactor, add lib, add some tests

Signed-off-by: jolheiser <john.olheiser@gmail.com>
main
jolheiser 7 months ago
parent
commit
15d69e947d
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
  1. 3
      .gitignore
  2. 116
      cabinet.go
  3. 23
      cmd/cabinet/command.go
  4. 5
      cmd/cabinet/main.go
  5. 3
      go.mod
  6. 2
      go.sum
  7. 4
      internal/gc/gc.go
  8. 0
      internal/gc/sort.go
  9. 2
      internal/router/rate.go
  10. 8
      internal/router/router.go
  11. 43
      internal/router/router_test.go
  12. 2
      internal/router/token.go
  13. 0
      internal/static/index.tmpl
  14. 0
      internal/static/sakura.css
  15. 0
      internal/static/static.go
  16. 0
      internal/workspace/meta.go
  17. 2
      internal/workspace/mock/file.go
  18. 12
      internal/workspace/mock/mock.go
  19. 0
      internal/workspace/token.go
  20. 0
      internal/workspace/workspace.go
  21. 1
      router/logger.go

3
.gitignore vendored

@ -1,3 +1,4 @@
.idea/
.cabinet/
/cabinet*
/cabinet
/cabinet.exe

116
cabinet.go

@ -0,0 +1,116 @@
package cabinet
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
)
type Client struct {
BaseURL string
HTTP *http.Client
Token string
}
func New(baseURL string, opts ...ClientOption) *Client {
c := &Client{
BaseURL: baseURL,
HTTP: http.DefaultClient,
}
for _, opt := range opts {
opt(c)
}
return c
}
type ClientOption func(*Client)
func WithHTTPClient(client *http.Client) ClientOption {
return func(c *Client) {
c.HTTP = client
}
}
func WithToken(token string) ClientOption {
return func(c *Client) {
c.Token = token
}
}
func (c *Client) Redirect(u string) (string, *http.Response, error) {
vals := url.Values{
"url": []string{u},
}
if c.Token != "" {
vals["token"] = []string{c.Token}
}
resp, err := c.HTTP.PostForm(fmt.Sprintf("%s/r", c.BaseURL), vals)
if err != nil {
return "", nil, err
}
if resp.StatusCode != 200 {
return "", resp, fmt.Errorf("non-200 status code: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", resp, err
}
resp.Body = io.NopCloser(bytes.NewReader(body))
return string(body), resp, nil
}
func (c *Client) File(name string, f io.Reader) (string, *http.Response, error) {
var buf bytes.Buffer
mp := multipart.NewWriter(&buf)
token, err := mp.CreateFormField("token")
if err != nil {
return "", nil, err
}
if _, err := token.Write([]byte(c.Token)); err != nil {
return "", nil, err
}
file, err := mp.CreateFormFile("file", name)
if err != nil {
return "", nil, err
}
if _, err := io.Copy(file, f); err != nil {
return "", nil, err
}
if err := mp.Close(); err != nil {
return "", nil, err
}
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/f", c.BaseURL), &buf)
if err != nil {
return "", nil, err
}
req.Header.Set("Content-Type", mp.FormDataContentType())
resp, err := c.HTTP.Do(req)
if err != nil {
return "", nil, err
}
if resp.StatusCode != 200 {
return "", resp, fmt.Errorf("non-200 status code: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", resp, err
}
resp.Body = io.NopCloser(bytes.NewReader(body))
return string(body), resp, nil
}

23
command.go → cmd/cabinet/command.go

@ -7,13 +7,14 @@ import (
"os"
"time"
"go.jolheiser.com/cabinet/internal/gc"
router2 "go.jolheiser.com/cabinet/internal/router"
workspace2 "go.jolheiser.com/cabinet/internal/workspace"
"github.com/AlecAivazis/survey/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.jolheiser.com/cabinet/gc"
"go.jolheiser.com/cabinet/router"
"go.jolheiser.com/cabinet/workspace"
)
type serveOpts struct {
@ -44,14 +45,14 @@ var serveCmd = func(opts *serveOpts) func(context.Context, []string) error {
opts.domain = fmt.Sprintf("http://localhost:%d", opts.port)
}
ws, err := workspace.New(opts.workspacePath)
ws, err := workspace2.New(opts.workspacePath)
if err != nil {
log.Fatal().Err(err).Msg("could not open workspace")
}
go gc.Start(ws, opts.maxDiskSize, opts.gcInterval)
r := router.New(opts.domain, ws, router.NewLimit(opts.requestPerMinute, opts.sizePerMinute, opts.burstSize, opts.memPerRequest))
r := router2.New(opts.domain, ws, router2.NewLimit(opts.requestPerMinute, opts.sizePerMinute, opts.burstSize, opts.memPerRequest))
portStr := fmt.Sprintf(":%d", opts.port)
log.Info().Msgf("Listening at http://localhost%s", portStr)
@ -64,7 +65,7 @@ var serveCmd = func(opts *serveOpts) func(context.Context, []string) error {
type tokenOpts struct {
token string
perm workspace.TokenPermission
perm workspace2.TokenPermission
desc string
workspacePath string
delete bool
@ -72,7 +73,7 @@ type tokenOpts struct {
var tokenCmd = func(opts *tokenOpts) func(context.Context, []string) error {
return func(_ context.Context, _ []string) error {
ws, err := workspace.New(opts.workspacePath)
ws, err := workspace2.New(opts.workspacePath)
if err != nil {
log.Fatal().Err(err).Msg("could not open workspace")
}
@ -84,7 +85,7 @@ var tokenCmd = func(opts *tokenOpts) func(context.Context, []string) error {
}
}
func addToken(ws *workspace.Workspace, opts *tokenOpts) error {
func addToken(ws *workspace2.Workspace, opts *tokenOpts) error {
token := opts.token
if token == "" {
r, err := randToken()
@ -118,7 +119,7 @@ func addToken(ws *workspace.Workspace, opts *tokenOpts) error {
if err != nil {
return err
}
perm, err = workspace.ParseTokenPermission(p)
perm, err = workspace2.ParseTokenPermission(p)
if err != nil {
return err
}
@ -136,7 +137,7 @@ func addToken(ws *workspace.Workspace, opts *tokenOpts) error {
}
}
if err := ws.AddToken(workspace.Token{
if err := ws.AddToken(workspace2.Token{
Key: token,
Permission: perm,
Description: desc,
@ -148,7 +149,7 @@ func addToken(ws *workspace.Workspace, opts *tokenOpts) error {
return nil
}
func deleteTokens(ws *workspace.Workspace, opts *tokenOpts) error {
func deleteTokens(ws *workspace2.Workspace, opts *tokenOpts) error {
if opts.token != "" {
if err := ws.DeleteTokens(opts.token); err != nil {
return err

5
main.go → cmd/cabinet/main.go

@ -13,12 +13,13 @@ import (
"strings"
"time"
"go.jolheiser.com/cabinet/internal/workspace"
"github.com/peterbourgon/ff/v3"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/peterbourgon/ff/v3/fftoml"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.jolheiser.com/cabinet/workspace"
)
func main() {
@ -39,7 +40,7 @@ func main() {
serveFS.Func("max-disk-size", "Max size of all disk space usage", fileSizeParse(&serveOpts.maxDiskSize))
serveFS.StringVar(&serveOpts.workspacePath, "workspace", ".cabinet", "Workspace for DB, files, tokens, etc.")
serveFS.DurationVar(&serveOpts.gcInterval, "gc-interval", time.Hour, "GC interval for cleaning up files")
serveFS.IntVar(&serveOpts.requestPerMinute, "request-per-minute", 12, "Request limit per-second")
serveFS.IntVar(&serveOpts.requestPerMinute, "request-per-minute", 12, "Request limit per-minute")
serveFS.Func("size-per-second", "File size limit per-second", fileSizeParse(&serveOpts.sizePerMinute))
serveFS.Func("burst-size", "Burst size for files", fileSizeParse(&serveOpts.burstSize))
serveFS.Func("mem-per-request", "Memory per request before storing on disk temporarily", fileSizeParse(&serveOpts.memPerRequest))

3
go.mod

@ -1,11 +1,12 @@
module go.jolheiser.com/cabinet
go 1.17
go 1.18
require (
github.com/AlecAivazis/survey/v2 v2.3.2
github.com/bwmarrin/snowflake v0.3.0
github.com/go-chi/chi/v5 v5.0.7
github.com/matryer/is v1.4.0
github.com/peterbourgon/ff/v3 v3.1.2
github.com/rs/zerolog v1.26.1
go.etcd.io/bbolt v1.3.6

2
go.sum

@ -18,6 +18,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=

4
gc/gc.go → internal/gc/gc.go

@ -29,10 +29,6 @@ func Start(g GC, maxSize int, interval time.Duration) {
size += int(info.Size())
}
if size < maxSize {
continue
}
sort.Sort(sorter(infos))
for size > maxSize {
fi := infos[0]

0
gc/sort.go → internal/gc/sort.go

2
router/rate.go → internal/router/rate.go

@ -82,7 +82,7 @@ func (l *Limit) hasHitRequestLimit(remoteAddr string) bool {
if l.request[ip] == nil {
l.request[ip] = rate.NewLimiter(rate.Limit(l.rpm/60), l.rpm)
l.size[ip] = rate.NewLimiter(rate.Limit(l.spm), l.burst)
l.size[ip] = rate.NewLimiter(rate.Limit(l.spm/60), l.burst)
}
return !l.request[ip].Allow()

8
router/router.go → internal/router/router.go

@ -7,12 +7,12 @@ import (
"net/url"
"strings"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog/log"
"go.jolheiser.com/cabinet/static"
"go.jolheiser.com/cabinet/internal/static"
"go.jolheiser.com/cabinet/internal/workspace"
"github.com/go-chi/chi/v5"
"go.jolheiser.com/cabinet/workspace"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog/log"
)
type Cabinet interface {

43
internal/router/router_test.go

@ -0,0 +1,43 @@
package router
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/matryer/is"
"go.jolheiser.com/cabinet"
"go.jolheiser.com/cabinet/internal/workspace/mock"
)
func TestRouter(t *testing.T) {
assert := is.New(t)
m := mock.New()
server := httptest.NewUnstartedServer(nil)
server.Start()
defer server.Close()
r := New(server.URL, m, NewLimit(12, 5*1024*1024, 100*1024*1024, 1024*1024))
server.Config.Handler = r
c := cabinet.New(server.URL, cabinet.WithHTTPClient(server.Client()))
// Redirect
redir := "https://duckduckgo.com"
u, _, err := c.Redirect(redir)
assert.NoErr(err) // Creating a redirect should succeed
resp, err := http.Get(u)
assert.NoErr(err)
assert.Equal(redir, resp.Request.URL.String()) // The redirect should match what was given
file := "foobar"
f, _, err := c.File("test.txt", strings.NewReader(file))
assert.NoErr(err) // Creating a file should succeed
resp, err = http.Get(f)
assert.NoErr(err)
b, err := io.ReadAll(resp.Body)
assert.NoErr(err)
assert.Equal(file, string(b)) // The file should match what was given
}

2
router/token.go → internal/router/token.go

@ -3,7 +3,7 @@ package router
import (
"net/http"
"go.jolheiser.com/cabinet/workspace"
"go.jolheiser.com/cabinet/internal/workspace"
)
func tokenMiddleware(c Cabinet, perm workspace.TokenPermission) func(handler http.Handler) http.Handler {

0
static/index.tmpl → internal/static/index.tmpl

0
static/sakura.css → internal/static/sakura.css

0
static/static.go → internal/static/static.go

0
workspace/meta.go → internal/workspace/meta.go

2
workspace/mock/file.go → internal/workspace/mock/file.go

@ -4,7 +4,7 @@ import (
"os"
"time"
"go.jolheiser.com/cabinet/workspace"
"go.jolheiser.com/cabinet/internal/workspace"
)
type File struct {

12
workspace/mock/mock.go → internal/workspace/mock/mock.go

@ -8,7 +8,7 @@ import (
"strings"
"time"
"go.jolheiser.com/cabinet/workspace"
"go.jolheiser.com/cabinet/internal/workspace"
)
type Mock struct {
@ -18,7 +18,8 @@ type Mock struct {
func New() *Mock {
return &Mock{
files: make(map[string]*File),
files: make(map[string]*File),
tokens: make(map[string]workspace.Token),
}
}
@ -72,6 +73,13 @@ func (m Mock) IsProtected() (bool, error) {
return len(m.tokens) > 0, nil
}
func (m Mock) Token(key string) (workspace.Token, error) {
if token, ok := m.tokens[key]; ok {
return token, nil
}
return workspace.Token{}, fmt.Errorf("no token found for %s", key)
}
func (m Mock) AddToken(token workspace.Token) error {
m.tokens[token.Key] = token
return nil

0
workspace/token.go → internal/workspace/token.go

0
workspace/workspace.go → internal/workspace/workspace.go

1
router/logger.go

@ -1 +0,0 @@
package router
Loading…
Cancel
Save