6 Commits

Author SHA1 Message Date
c6eb147e2c Use configured store
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-01-25 01:57:28 +01:00
150ffc3400 Fix error in memorystore
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-01-24 23:18:13 +01:00
ee761d4006 Change access logger
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-01-24 22:53:46 +01:00
8ae5ee64bb Fix scoop url
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-01-24 20:50:42 +01:00
5e186b28ce Fix scoop folder for goreleaser
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-01-24 20:46:46 +01:00
26c9fb5bfd Update scoop with goreleaser
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-01-24 20:43:07 +01:00
8 changed files with 279 additions and 95 deletions

View File

@@ -6,6 +6,7 @@ before:
- go mod tidy - go mod tidy
# you may remove this if you don't need go generate # you may remove this if you don't need go generate
- go generate ./... - go generate ./...
builds: builds:
- id: "gpaste-client" - id: "gpaste-client"
binary: "gpaste" binary: "gpaste"
@@ -28,22 +29,42 @@ builds:
goarch: goarch:
- amd64 - amd64
main: ./cmd/server/server.go main: ./cmd/server/server.go
archives: archives:
- format_overrides: - format_overrides:
- goos: windows - goos: windows
format: zip format: zip
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
snapshot: snapshot:
name_template: "{{ incpatch .Version }}-next" name_template: "{{ incpatch .Version }}-next"
changelog: changelog:
sort: asc sort: asc
filters: filters:
exclude: exclude:
- '^docs:' - '^docs:'
- '^test:' - '^test:'
scoop:
url_template: "https://git.t-juice.club/torjus/gpaste/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
bucket:
owner: torjus
name: scoop-tjuice
branch: master
folder: bucket
commit_author:
name: ci.t-juice.club
email: ci@t-juice.club
commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}"
homepage: "https://git.t-juice.club/torjus/gpaste"
description: "Simple pastebin-style thingie."
gitea_urls: gitea_urls:
api: https://git.t-juice.club/api/v1/ api: https://git.t-juice.club/api/v1/
download: https://git.t-juice.club download: https://git.t-juice.club
env_files: env_files:
gitea_token: gitea_token gitea_token: gitea_token

View File

@@ -35,16 +35,11 @@ func NewHTTPServer(cfg *gpaste.ServerConfig) *HTTPServer {
config: cfg, config: cfg,
Logger: zap.NewNop().Sugar(), Logger: zap.NewNop().Sugar(),
AccessLogger: zap.NewNop().Sugar(), AccessLogger: zap.NewNop().Sugar(),
Files: files.NewMemoryFileStore(),
Users: users.NewMemoryUserStore(),
} }
srv.Files = files.NewMemoryFileStore() signingSecret, _ := uuid.Must(uuid.NewRandom()).MarshalBinary()
srv.Users = users.NewMemoryUserStore() srv.Auth = gpaste.NewAuthService(srv.Users, signingSecret)
srv.Auth = gpaste.NewAuthService(srv.Users, []byte(srv.config.SigningSecret))
// Create initial user
// TODO: Do properly
user := &users.User{Username: "admin", Role: users.RoleAdmin}
_ = user.SetPassword("admin")
_ = srv.Users.Store(user)
r := chi.NewRouter() r := chi.NewRouter()
r.Use(middleware.RealIP) r.Use(middleware.RealIP)

View File

@@ -10,6 +10,7 @@ import (
"git.t-juice.club/torjus/gpaste" "git.t-juice.club/torjus/gpaste"
"git.t-juice.club/torjus/gpaste/users" "git.t-juice.club/torjus/gpaste/users"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"go.uber.org/zap"
) )
type authCtxKey int type authCtxKey int
@@ -27,14 +28,38 @@ func (s *HTTPServer) MiddlewareAccessLogger(next http.Handler) http.Handler {
reqID := middleware.GetReqID(r.Context()) reqID := middleware.GetReqID(r.Context())
// TODO: Maybe desugar in HTTPServer to avoid doing for all requests
logger := s.AccessLogger.Desugar()
defer func() { defer func() {
s.AccessLogger.Infow(r.Method, // DEBUG level
"path", r.URL.Path, if ce := logger.Check(zap.DebugLevel, r.Method); ce != nil {
"status", ww.Status(), ct := r.Header.Get("Content-Type")
"written", ww.BytesWritten(), ce.Write(
"remote_addr", r.RemoteAddr, zap.String("req_id", reqID),
"processing_time_ms", time.Since(t1).Milliseconds(), zap.String("path", r.URL.Path),
"req_id", reqID) zap.Int("status", ww.Status()),
zap.String("remote_addr", r.RemoteAddr),
zap.Int("bytes_written", ww.BytesWritten()),
zap.Duration("processing_time", time.Since(t1)),
zap.String("content_type", ct),
zap.Any("headers", r.Header),
)
} else {
// INFO level
if ce := logger.Check(zap.InfoLevel, r.Method); ce != nil {
ce.Write(
zap.String("req_id", reqID),
zap.String("path", r.URL.Path),
zap.Int("status", ww.Status()),
zap.String("remote_addr", r.RemoteAddr),
zap.Int("bytes_written", ww.BytesWritten()),
zap.Duration("processing_time", time.Since(t1)),
)
}
}
_ = logger.Sync()
}() }()
next.ServeHTTP(ww, r) next.ServeHTTP(ww, r)

View File

@@ -6,11 +6,15 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"strings" "strings"
"time" "time"
"git.t-juice.club/torjus/gpaste" "git.t-juice.club/torjus/gpaste"
"git.t-juice.club/torjus/gpaste/api" "git.t-juice.club/torjus/gpaste/api"
"git.t-juice.club/torjus/gpaste/files"
"git.t-juice.club/torjus/gpaste/users"
"github.com/google/uuid"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
@@ -62,11 +66,49 @@ func ActionServe(c *cli.Context) error {
httpShutdownCtx, httpShutdownCancel := context.WithCancel(context.Background()) httpShutdownCtx, httpShutdownCancel := context.WithCancel(context.Background())
defer httpShutdownCancel() defer httpShutdownCancel()
// Setup stores
// Files
fileStore, fileClose, err := getFileStore(cfg)
if err != nil {
return err
}
defer fileClose() // nolint: errcheck
// Users
userStore, userClose, err := getUserStore(cfg)
if err != nil {
return err
}
defer userClose() // nolint: errcheck
if userList, err := userStore.List(); err != nil {
serverLogger.Panicw("Error checking userstore for users.", "error", err)
} else if len(userList) < 1 {
admin := users.User{
Username: "admin",
Role: users.RoleAdmin,
}
password := uuid.NewString()
if err := admin.SetPassword(password); err != nil {
serverLogger.DPanic("Error setting admin-user password.", "error", err)
}
serverLogger.Warnw("Created admin-user.", "username", admin.Username, "password", password)
}
// Auth
auth := gpaste.NewAuthService(userStore, []byte(cfg.SigningSecret))
go func() { go func() {
srv := api.NewHTTPServer(cfg) srv := api.NewHTTPServer(cfg)
srv.Users = userStore
srv.Files = fileStore
srv.Addr = cfg.ListenAddr srv.Addr = cfg.ListenAddr
srv.Logger = serverLogger srv.Logger = serverLogger
srv.AccessLogger = accessLogger srv.AccessLogger = accessLogger
srv.Auth = auth
// Wait for cancel // Wait for cancel
go func() { go func() {
@@ -126,3 +168,48 @@ func getRootLogger(level string) *zap.SugaredLogger {
return rootLogger.Sugar() return rootLogger.Sugar()
} }
// nolint: ireturn
func getUserStore(cfg *gpaste.ServerConfig) (users.UserStore, func() error, error) {
closer := func() error { return nil }
switch cfg.Store.Type {
case "memory":
return users.NewMemoryUserStore(), closer, nil
case "fs":
path := filepath.Join(cfg.Store.FS.Dir, "gpaste-users.db")
bs, err := users.NewBoltUserStore(path)
if err != nil {
return nil, closer, cli.Exit("error setting up user store", 1)
}
return bs, bs.Close, nil
default:
return nil, closer, cli.Exit("no userstore configured", 1)
}
}
// nolint: ireturn
func getFileStore(cfg *gpaste.ServerConfig) (files.FileStore, func() error, error) {
closer := func() error { return nil }
switch cfg.Store.Type {
case "memory":
return files.NewMemoryFileStore(), closer, nil
case "fs":
var err error
s, err := files.NewFSFileStore(cfg.Store.FS.Dir)
if err != nil {
return nil, closer, cli.Exit("error setting up filestore", 1)
}
return s, closer, nil
default:
return nil, closer, cli.Exit("No store configured", 1)
}
}

View File

@@ -7,20 +7,23 @@ import (
) )
func TestFSFileStore(t *testing.T) { func TestFSFileStore(t *testing.T) {
dir := t.TempDir() newFunc := func() files.FileStore {
s, err := files.NewFSFileStore(dir) dir := t.TempDir()
if err != nil { s, err := files.NewFSFileStore(dir)
t.Fatalf("Error creating store: %s", err) if err != nil {
t.Fatalf("Error creating store: %s", err)
}
return s
} }
RunFilestoreTest(s, t) RunFilestoreTest(newFunc, t)
persistentDir := t.TempDir() persistentDir := t.TempDir()
newFunc := func() files.FileStore { persistentFunc := func() files.FileStore {
s, err := files.NewFSFileStore(persistentDir) s, err := files.NewFSFileStore(persistentDir)
if err != nil { if err != nil {
t.Fatalf("Error creating store: %s", err) t.Fatalf("Error creating store: %s", err)
} }
return s return s
} }
RunPersistentFilestoreTest(newFunc, t) RunPersistentFilestoreTest(persistentFunc, t)
} }

View File

@@ -57,11 +57,16 @@ func (s *MemoryFileStore) Get(id string) (*File, error) {
return nil, fmt.Errorf("no such item") return nil, fmt.Errorf("no such item")
} }
body := new(bytes.Buffer)
if _, err := body.Write(fd.Body.Bytes()); err != nil {
return nil, err
}
f := &File{ f := &File{
ID: fd.ID, ID: fd.ID,
MaxViews: fd.MaxViews, MaxViews: fd.MaxViews,
ExpiresOn: fd.ExpiresOn, ExpiresOn: fd.ExpiresOn,
Body: io.NopCloser(&fd.Body), Body: io.NopCloser(body),
FileSize: fd.FileSize, FileSize: fd.FileSize,
} }

View File

@@ -7,7 +7,9 @@ import (
) )
func TestMemoryFileStore(t *testing.T) { func TestMemoryFileStore(t *testing.T) {
s := files.NewMemoryFileStore() newFunc := func() files.FileStore {
return files.NewMemoryFileStore()
}
RunFilestoreTest(s, t) RunFilestoreTest(newFunc, t)
} }

View File

@@ -12,8 +12,11 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
func RunFilestoreTest(s files.FileStore, t *testing.T) { var ignoreBody = cmp.FilterPath(func(p cmp.Path) bool { return p.String() == "Body" }, cmp.Ignore())
func RunFilestoreTest(newStoreFunc func() files.FileStore, t *testing.T) {
t.Run("Basic", func(t *testing.T) { t.Run("Basic", func(t *testing.T) {
s := newStoreFunc()
// Create // Create
dataString := "TEST_LOL_OMG" dataString := "TEST_LOL_OMG"
id := uuid.Must(uuid.NewRandom()).String() id := uuid.Must(uuid.NewRandom()).String()
@@ -58,7 +61,6 @@ func RunFilestoreTest(s files.FileStore, t *testing.T) {
FileSize: int64(len(dataString)), FileSize: int64(len(dataString)),
} }
ignoreBody := cmp.FilterPath(func(p cmp.Path) bool { return p.String() == "Body" }, cmp.Ignore())
if diff := cmp.Diff(retrieved, expected, ignoreBody); diff != "" { if diff := cmp.Diff(retrieved, expected, ignoreBody); diff != "" {
t.Errorf("File comparison failed: %s", diff) t.Errorf("File comparison failed: %s", diff)
} }
@@ -88,84 +90,128 @@ func RunFilestoreTest(s files.FileStore, t *testing.T) {
t.Fatalf("List after delete has wrong length: %d", len(ids)) t.Fatalf("List after delete has wrong length: %d", len(ids))
} }
}) })
t.Run("MultipleGet", func(t *testing.T) {
s := newStoreFunc()
fileContents := "multiple get test !"
body := io.NopCloser(strings.NewReader(fileContents))
file := &files.File{
ID: uuid.NewString(),
OriginalFilename: "multiple.txt",
MaxViews: 999,
ExpiresOn: time.Now().Add(1 * time.Hour),
Body: body,
FileSize: int64(len(fileContents)),
}
if err := s.Store(file); err != nil {
t.Fatalf("Error storing file: %s", err)
}
first, err := s.Get(file.ID)
if err != nil {
t.Errorf("Error retrieving first file: %s", err)
}
firstBody := new(bytes.Buffer)
io.Copy(firstBody, first.Body)
first.Body.Close()
if diff := cmp.Diff(firstBody.String(), fileContents); diff != "" {
t.Fatalf("File contents mismatch: %s", diff)
}
second, err := s.Get(file.ID)
if err != nil {
t.Errorf("Error retrieving first file: %s", err)
}
secondBody := new(bytes.Buffer)
io.Copy(secondBody, second.Body)
first.Body.Close()
if diff := cmp.Diff(secondBody.String(), fileContents); diff != "" {
t.Fatalf("File contents mismatch: %s", diff)
}
})
} }
func RunPersistentFilestoreTest(newStoreFunc func() files.FileStore, t *testing.T) { func RunPersistentFilestoreTest(newStoreFunc func() files.FileStore, t *testing.T) {
s := newStoreFunc() t.Run("Basics", func(t *testing.T) {
s := newStoreFunc()
files := []struct { files := []struct {
File *files.File File *files.File
ExpectedData string ExpectedData string
}{ }{
{ {
File: &files.File{ File: &files.File{
ID: uuid.NewString(), ID: uuid.NewString(),
OriginalFilename: "testfile.txt", OriginalFilename: "testfile.txt",
MaxViews: 5, MaxViews: 5,
ExpiresOn: time.Now().Add(10 * time.Minute), ExpiresOn: time.Now().Add(10 * time.Minute),
Body: io.NopCloser(strings.NewReader("cocks!")), Body: io.NopCloser(strings.NewReader("cocks!")),
FileSize: 6, FileSize: 6,
},
ExpectedData: "cocks!",
}, },
ExpectedData: "cocks!", {
}, File: &files.File{
{ ID: uuid.NewString(),
File: &files.File{ OriginalFilename: "testfile2.txt",
ID: uuid.NewString(), MaxViews: 5,
OriginalFilename: "testfile2.txt", ExpiresOn: time.Now().Add(10 * time.Minute),
MaxViews: 5, Body: io.NopCloser(strings.NewReader("derps!")),
ExpiresOn: time.Now().Add(10 * time.Minute), FileSize: 6,
Body: io.NopCloser(strings.NewReader("derps!")), },
FileSize: 6, ExpectedData: "derps!",
}, },
ExpectedData: "derps!",
},
}
for _, f := range files {
err := s.Store(f.File)
if err != nil {
t.Fatalf("Error storing file: %s", err)
}
}
for _, f := range files {
retrieved, err := s.Get(f.File.ID)
if err != nil {
t.Fatalf("Unable to retrieve file: %s", err)
} }
ignoreBody := cmp.FilterPath(func(p cmp.Path) bool { return p.String() == "Body" }, cmp.Ignore()) for _, f := range files {
if !cmp.Equal(retrieved, f.File, ignoreBody) { err := s.Store(f.File)
t.Errorf("Mismatch: %s", cmp.Diff(retrieved, f.File)) if err != nil {
t.Fatalf("Error storing file: %s", err)
}
} }
buf := new(strings.Builder) for _, f := range files {
if _, err := io.Copy(buf, retrieved.Body); err != nil { retrieved, err := s.Get(f.File.ID)
t.Fatalf("Error reading from body: %s", err) if err != nil {
} t.Fatalf("Unable to retrieve file: %s", err)
retrieved.Body.Close() }
if buf.String() != f.ExpectedData {
t.Fatalf("Data does not match. %s", cmp.Diff(buf.String(), f.ExpectedData))
}
}
// Reopen store, and fetch again if !cmp.Equal(retrieved, f.File, ignoreBody) {
s = newStoreFunc() t.Errorf("Mismatch: %s", cmp.Diff(retrieved, f.File))
for _, f := range files { }
retrieved, err := s.Get(f.File.ID) buf := new(strings.Builder)
if err != nil { if _, err := io.Copy(buf, retrieved.Body); err != nil {
t.Fatalf("Unable to retrieve file: %s", err) t.Fatalf("Error reading from body: %s", err)
}
retrieved.Body.Close()
if buf.String() != f.ExpectedData {
t.Fatalf("Data does not match. %s", cmp.Diff(buf.String(), f.ExpectedData))
}
} }
ignoreBody := cmp.FilterPath(func(p cmp.Path) bool { return p.String() == "Body" }, cmp.Ignore()) // Reopen store, and fetch again
if !cmp.Equal(retrieved, f.File, ignoreBody) { s = newStoreFunc()
t.Errorf("Mismatch: %s", cmp.Diff(retrieved, f.File)) for _, f := range files {
retrieved, err := s.Get(f.File.ID)
if err != nil {
t.Fatalf("Unable to retrieve file: %s", err)
}
if !cmp.Equal(retrieved, f.File, ignoreBody) {
t.Errorf("Mismatch: %s", cmp.Diff(retrieved, f.File))
}
buf := new(strings.Builder)
if _, err := io.Copy(buf, retrieved.Body); err != nil {
t.Fatalf("Error reading from body: %s", err)
}
retrieved.Body.Close()
if buf.String() != f.ExpectedData {
t.Fatalf("Data does not match. %s", cmp.Diff(buf.String(), f.ExpectedData))
}
} }
buf := new(strings.Builder) })
if _, err := io.Copy(buf, retrieved.Body); err != nil {
t.Fatalf("Error reading from body: %s", err)
}
retrieved.Body.Close()
if buf.String() != f.ExpectedData {
t.Fatalf("Data does not match. %s", cmp.Diff(buf.String(), f.ExpectedData))
}
}
} }