Compare commits

...

34 Commits

Author SHA1 Message Date
Caj Larsson 8cc8e6a464 Woodpecker CI badge
ci/woodpecker/push/woodpecker Pipeline was successful Details
2 years ago
Caj Larsson e398bcb7b0 Woodpecker CI 2 years ago
caj 96b44a3df9 Fixed logo link 3 years ago
Caj Larsson 336e7e14d3 Added logo 3 years ago
Caj Larsson 73ba8fc3b8 Formatting with gofmt 3 years ago
Caj Larsson d25f03e80c Renaming SwampfileService -> DataSwampService 3 years ago
Caj Larsson 3c12794d9c Testing the events on namespace Service 3 years ago
Caj Larsson d516f0aaae More progress on event and gofmt 3 years ago
Caj Larsson 3e6855d429 More progress on event and gofmt 3 years ago
Caj Larsson c609ca7b9a Progress on the great event refactoring 3 years ago
Caj Larsson 573ef316b4 util folder 3 years ago
Caj Larsson adbb5dbc43 Truncate and unreserve on file replacement 3 years ago
Caj Larsson d1e53c2337 Separate admin port 3 years ago
Caj Larsson 5bad2c5335 Immutable value objects 3 years ago
Caj Larsson ee58da4f2d Fix the dockerfile 3 years ago
Caj Larsson 53d5794c96 Upload on 404 and attachment response header 3 years ago
Caj Larsson 630f9220f3 Dashboard 3 years ago
Caj Larsson 6736229185 Fix an issue in sql repo errors 3 years ago
Caj Larsson 857a558544 Useragent statistics complete #3 3 years ago
Caj Larsson c8d5410936 Fix service usage of new stats 3 years ago
Caj Larsson d688900bae Use sqlc to generate typesafe repository tools 3 years ago
Caj Larsson 1c15742710 Namespace repo contract 3 years ago
Caj Larsson 443c9b9376 Usage value object 3 years ago
Caj Larsson 7274e08b9a fix #11 3 years ago
Caj Larsson 9e41b707bf Dockerfile solves #10 3 years ago
Caj Larsson 99bdf4e320 logger, implements #9 3 years ago
Caj Larsson 53531373b9 Delete expired files 3 years ago
Caj Larsson 2ce12745a6 swampfile.Repository.DeleteOlderThan 3 years ago
Caj Larsson 7bb9bf90c2 swampfile.Repository.DeleteOlderThan 3 years ago
Caj Larsson 0b6e27f129 fs swampfile repo flagfile to prevent deletion of non swamp files 3 years ago
Caj Larsson 67e137a474 fix: #8 content size lies 3 years ago
Caj Larsson 82067ca87c Path sanitation 3 years ago
Caj Larsson e6e2f372dd Use is. tests and fmt 3 years ago
Caj Larsson 8a0e6b80e3 Complete restructure of domain and infra layer 3 years ago

@ -0,0 +1,2 @@
Dockerfile
test

3
.gitignore vendored

@ -0,0 +1,3 @@
sql.db
bog
test

@ -0,0 +1,7 @@
pipeline:
build:
image: golang
commands:
- go test ./...
environment:
- GOPRIVATE=git.sg.caj.me/caj

@ -0,0 +1,41 @@
FROM golang:alpine as builder
ENV GO111MODULE=on
ENV USER=appuser
ENV UID=1000
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
RUN apk update && apk add --no-cache git ca-certificates tzdata sqlite build-base
RUN mkdir /build
COPY . /build/
WORKDIR /build
COPY ./ ./
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o bog .
FROM scratch AS final
LABEL author="Cajually <me@caj.me>"
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /build/bog /
COPY --from=builder /build/server/views /server/views
COPY --from=builder /build/default.toml /
WORKDIR /
USER appuser:appuser
ENTRYPOINT ["/bog"]
EXPOSE 8002

@ -1,4 +1,7 @@
[![status-badge](https://ci.sg.caj.me/api/badges/caj/bog/status.svg)](https://ci.sg.caj.me/caj/bog)
# Bog: The Dataswamp
![Logo](https://git.sg.caj.me/caj/bog/raw/branch/master/doc/logo.jpg)
Is the warehouse too strict? Maybe you don't know the schema yet. And
you know your lake is already full with a bunch of stuff that should
have been deleted long ago?
@ -10,21 +13,3 @@ Don't worry about access credentials, the datasawmp does authorization
without authentication the old school way: encryption. Pass a password
when you create your data and if you pass the same when you retrieve
it, you get the same bits back.
## TODO
Alpha
- [x] Concurrent access safety
- [x] Test domain
- [x] Test integration
- [ ] Test application
Beta
- [ ] Path Sanitation and rejection
- [ ] Usage statistics
- [ ] Clean up background process
1.0
- [ ] Rendered Dashboard
- [ ] Upload page helper for 404

@ -1,101 +0,0 @@
package application
import (
"net/http"
"fmt"
"strconv"
// "io"
"caj-larsson/bog/domain"
"caj-larsson/bog/integration"
)
type Router interface {
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
ServeHTTP(http.ResponseWriter, *http.Request)
}
type Bog struct {
router Router
file_service domain.BogFileService
address string
}
func buildFileDataRepository(config FileConfig) domain.FileDataRepository{
fsBogRepo := new(integration.FileSystemBogRepository)
fsBogRepo.Root = config.Path
return fsBogRepo
}
func buildUserAgentRepository(config DatabaseConfig) *integration.SQLiteUserAgentRepository{
if config.Backend != "sqlite" {
panic("Can only handle sqlite")
}
return integration.NewSQLiteUserAgentRepository(config.Connection)
}
func (b *Bog) fileHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
fmt.Fprintf(w, "Hi")
return
}
ref := domain.FileReference {r.URL.Path, r.Header["User-Agent"][0]}
switch r.Method {
case "GET":
bog_file, err := b.file_service.OpenOutFile(ref)
if err == domain.ErrNotExists {
http.NotFound(w, r)
return
}
if err != nil {
panic(err)
}
http.ServeContent(w, r, bog_file.Path(), bog_file.Modified(), bog_file)
case "POST":
fallthrough
case "PUT":
size_str := r.Header["Content-Length"][0]
size, err := strconv.ParseInt(size_str, 10, 64)
if err != nil {
w.WriteHeader(422)
return
}
err = b.file_service.SaveFile(ref, r.Body, size)
if err != nil {
panic(err)
}
}
return
}
func (b *Bog) routes() {
b.router.HandleFunc("/", b.fileHandler)
}
func New(config *Configuration) *Bog {
b := new(Bog)
b.address = config.bindAddress()
fsBogRepo := buildFileDataRepository(config.File)
uaRepo := buildUserAgentRepository(config.Database)
b.file_service = domain.NewBogFileService(
uaRepo, fsBogRepo, config.Quota.ParsedSizeBytes(), config.Quota.ParsedDuration(),
)
b.router = new(http.ServeMux)
b.routes()
return b
}
func (b *Bog) Run() {
http.ListenAndServe(b.address, b.router)
}

@ -1,39 +0,0 @@
package application
import (
"fmt"
"net/http/httptest"
"net/http"
"testing"
"strings"
"time"
"caj-larsson/bog/domain"
"caj-larsson/bog/test/mock"
)
func TestApplication(t *testing.T) {
file_service := domain.NewBogFileService(
mock.NewMockUserAgentRepository(),
mock.NewMockFileRepository(),
1000,
time.Hour,
)
bog := Bog {
router: new(http.ServeMux),
file_service: file_service,
address: "fake",
}
bog.routes()
req := httptest.NewRequest("POST", "/apath", strings.NewReader("testdata"))
req.Header.Add("User-Agent", "testingclient")
w := httptest.NewRecorder()
bog.router.ServeHTTP(w, req)
if (w.Code != 200){
fmt.Printf("%v", w)
t.Error("not ok")
}
}

@ -1,43 +0,0 @@
package application
import (
"time"
"testing"
)
func TestConfiguration(t *testing.T) {
c, _ := ConfigFromToml(
`[server]
port = 8002
host = "127.0.0.1"
[file]
path = "/tmp/datta2"
[database]
backend = "sqlite"
connection = "sql.db"
[quota]
default_size = "1MB"
default_duration = "72h"`,
)
if c.Server.Port != 8002 {
t.Errorf("port parsing failed")
}
if c.Server.Host != "127.0.0.1" {
t.Errorf("host parsing failed")
}
if c.Quota.ParsedSizeBytes() != 1024 * 1024 {
t.Errorf("quota size parsing failed")
}
if c.Quota.ParsedDuration() != time.Duration(time.Hour * 72) {
t.Errorf("quota size parsing failed")
}
}

@ -0,0 +1,7 @@
package dataswamp
import (
// "caj-larsson/bog/dataswamp/namespace"
)
type AdminService struct{}

@ -0,0 +1 @@
package dataswamp

@ -1,23 +1,18 @@
package domain
package namespace
import (
"time"
"errors"
"time"
)
type UserAgent struct {
ID int64
Name string
LastSeen time.Time
type Namespace struct {
ID int64
Name string
LastSeen time.Time
AllowanceDuration time.Duration
FileQuota FileSizeQuota
}
type BogFile struct {
UserAgentId int64
Path string
Size int64
CreatedAt time.Time
FileQuota FileSizeQuota
Download FileStat
Upload FileStat
}
var (

@ -0,0 +1,15 @@
package namespace
import "errors"
var (
ErrNoNamespace = errors.New("that namespace does not exist")
)
type Repository interface {
Create(namespace Namespace) (*Namespace, error)
All() ([]Namespace, error)
GetByName(name string) (*Namespace, error)
Update(id int64, namespace Namespace) (*Namespace, error)
Delete(id int64) error
}

@ -0,0 +1,57 @@
package namespace
import (
"github.com/matryer/is"
"testing"
"time"
)
func RepositoryContract(fac func() Repository, t *testing.T) {
basicNamespaceContract(fac, t)
}
func basicNamespaceContract(fac func() Repository, t *testing.T) {
is := is.New(t)
r := fac()
all, err := r.All()
is.NoErr(err)
is.Equal(len(all), 0)
ns := Namespace{
23,
"n1",
time.Now(),
time.Duration(time.Hour * 3),
FileSizeQuota{1000, 0},
FileStat{1, 2},
FileStat{3, 4},
}
ns1, err := r.Create(ns)
is.NoErr(err)
ns.Name = "n2"
ns2, err := r.Create(ns)
is.NoErr(err)
is.True(ns1.ID != ns2.ID)
all, err = r.All()
is.NoErr(err)
is.Equal(len(all), 2)
is.Equal(ns.ID, int64(23))
ns3, _ := r.GetByName("n2")
is.Equal(*ns3, *ns2)
is.NoErr(r.Delete(ns2.ID))
all, err = r.All()
is.NoErr(err)
is.Equal(len(all), 1)
}

@ -0,0 +1,111 @@
package namespace
import (
"caj-larsson/bog/util"
"fmt"
"time"
)
type Clock interface {
Now() time.Time
}
type NamespaceService struct {
repo Repository
outboxes []func(util.Event)
logger util.Logger
clock Clock
default_ttl time.Duration
default_quota_bytes int64
}
func NewNamespaceService(repo Repository, logger util.Logger, clock Clock, default_ttl time.Duration, default_quota_bytes int64) *NamespaceService {
return &NamespaceService{
repo,
nil,
logger,
clock,
default_ttl,
default_quota_bytes,
}
}
func (s *NamespaceService) GetOrCreateNs(name string) *Namespace {
ns, err := s.repo.GetByName(name)
if err == ErrNotExists {
new_ns := Namespace{
0,
name,
s.clock.Now(),
s.default_ttl,
FileSizeQuota{s.default_quota_bytes, 0},
FileStat{0, 0},
FileStat{0, 0},
}
created_ns, err := s.repo.Create(new_ns)
if err != nil {
panic(err)
}
return created_ns
}
if err != nil {
panic(err)
}
return ns
}
func (s *NamespaceService) Wire(reg func(string, util.EventHandler), outbox func(ev util.Event)) {
reg("FileUsed", s.handleFileUsed)
s.outboxes = append(s.outboxes, outbox)
reg("FileUsed", s.handleFileUsed)
reg("FileDeleted", s.handleFileDeleted)
reg("FileRecieved", s.handleFileRecieved)
}
func (s *NamespaceService) All() []Namespace {
nss, err := s.repo.All()
if err != nil {
panic(err)
}
return nss
}
func (s *NamespaceService) handleFileUsed(payload interface{}) {
var payload_s = payload.(struct {
Name string
Size int64
})
fmt.Printf("file used %v\n", payload_s)
ns := s.GetOrCreateNs(payload_s.Name)
ns.FileQuota = ns.FileQuota.Add(payload_s.Size)
s.repo.Update(ns.ID, *ns)
}
func (s *NamespaceService) handleFileDeleted(payload interface{}) {
var payload_s = payload.(struct {
Name string
Size int64
})
fmt.Printf("file deleted %v\n", payload_s)
ns := s.GetOrCreateNs(payload_s.Name)
ns.FileQuota = ns.FileQuota.Add(-payload_s.Size)
fmt.Printf("file usage %v\n", ns.FileQuota)
s.repo.Update(ns.ID, *ns)
}
func (s *NamespaceService) handleFileRecieved(payload interface{}) {
var payload_s = payload.(struct {
Name string
Size int64
})
fmt.Printf("file recieved %v\n", payload_s)
ns := s.GetOrCreateNs(payload_s.Name)
ns.FileQuota = ns.FileQuota.Add(payload_s.Size)
fmt.Printf("file usage %v\n", ns.FileQuota)
s.repo.Update(ns.ID, *ns)
}

@ -0,0 +1,38 @@
package namespace
import (
"caj-larsson/bog/util"
"testing"
)
func TestEventTest(t *testing.T) {
eb := util.NewEventBus()
svc := NamespaceService{}
svc.Wire(eb.Register, eb.Handle)
events := []util.Event{
*util.NewEvent("FileUsed", struct {
Name string
Size int64
}{
"asd",
int64(12),
}),
*util.NewEvent("FileDeleted", struct {
Name string
Size int64
}{
"asd",
int64(12),
}),
*util.NewEvent("FileRecieved", struct {
Name string
Size int64
}{
"asd",
int64(12),
}),
}
util.AcceptsMessage(t, eb, events)
}

@ -0,0 +1,40 @@
package namespace
type FileSizeQuota struct {
AllowanceKB int64
CurrentUsage int64
}
func (f FileSizeQuota) Allows(size int64) bool {
return f.Remaining() >= size
}
func (f FileSizeQuota) Remaining() int64 {
return f.AllowanceKB - f.CurrentUsage
}
func (f FileSizeQuota) Add(size int64) FileSizeQuota {
return FileSizeQuota{
f.AllowanceKB,
f.CurrentUsage + size,
}
}
func (f FileSizeQuota) Remove(size int64) FileSizeQuota {
return FileSizeQuota{
f.AllowanceKB,
f.CurrentUsage - size,
}
}
type FileStat struct {
Num int64
SizeB int64
}
func (s FileStat) Add(size int64) FileStat {
return FileStat{
s.Num + 1,
s.SizeB + size,
}
}

@ -0,0 +1,23 @@
package namespace
import (
"github.com/matryer/is"
"testing"
)
func TestQuota(t *testing.T) {
is := is.New(t)
quota := FileSizeQuota{1000, 0}
is.True(quota.Allows(1000))
is.True(!quota.Allows(1001))
}
func TestQuotaManipulation(t *testing.T) {
is := is.New(t)
quota := FileSizeQuota{1000, 0}
quota = quota.Add(500)
is.Equal(quota.CurrentUsage, int64(500))
quota = quota.Remove(1000)
is.Equal(quota.CurrentUsage, int64(-500))
}

@ -0,0 +1,138 @@
package dataswamp
import (
"caj-larsson/bog/dataswamp/namespace"
"caj-larsson/bog/dataswamp/swampfile"
"caj-larsson/bog/util"
"io"
"strconv"
"time"
// "errors"
// "fmt"
)
type DataSwampService struct {
ns_svc namespace.NamespaceService
swamp_file_repo swampfile.Repository
logger util.Logger
eventBus util.EventBus
}
func NewDataSwampService(
ns_svc namespace.NamespaceService,
swamp_file_repo swampfile.Repository,
logger util.Logger,
) *DataSwampService {
s := DataSwampService{ns_svc, swamp_file_repo, logger, *util.NewEventBus()}
ns_svc.Wire(s.eventBus.Register, s.eventBus.Handle)
return &s
}
func (s DataSwampService) NamespaceStats() []namespace.Namespace {
return s.ns_svc.All()
}
func (s DataSwampService) SaveFile(ref swampfile.FileReference, src io.Reader, size int64) error {
ns := s.ns_svc.GetOrCreateNs(ref.UserAgent)
r, err := ref.Clean(true)
if err != nil {
return err
}
if !ns.FileQuota.Allows(size) {
return namespace.ErrExceedQuota
}
f, err := s.swamp_file_repo.Create(r.Path, strconv.FormatInt(ns.ID, 10))
if err != nil {
// TODO: convert this into a different error.
return err
}
s.eventBus.Handle(*util.NewEvent("FileUsed", struct {
Name string
Size int64
}{
ns.Name,
f.Size(),
}))
// TODO: rewrite this into an interruptable loop that emits downloaded events
written, err := io.CopyN(f, src, size)
if written < size {
s.swamp_file_repo.Delete(r.Path, strconv.FormatInt(ns.ID, 10)) //
return swampfile.ErrContentSizeExaggerated
}
var buf = make([]byte, 1)
overread, err := src.Read(buf)
if overread > 0 || err != io.EOF {
s.swamp_file_repo.Delete(r.Path, strconv.FormatInt(ns.ID, 10))
return swampfile.ErrContentSizeExceeded
}
err = f.Close()
if err != nil {
panic(err)
}
s.eventBus.Handle(*util.NewEvent("FileRecieved", struct {
Name string
Size int64
}{
ns.Name,
written,
}))
return nil
}
func (s DataSwampService) OpenOutFile(ref swampfile.FileReference) (swampfile.SwampOutFile, error) {
ns := s.ns_svc.GetOrCreateNs(ref.UserAgent)
r, err := ref.Clean(true)
if err != nil {
return nil, err
}
f, err := s.swamp_file_repo.Open(r.Path, strconv.FormatInt(ns.ID, 10))
if err != nil {
return nil, err
}
s.eventBus.Handle(*util.NewEvent("FileSent", f.Size()))
return f, nil
}
func (s DataSwampService) CleanUpExpiredFiles() error {
s.logger.Info("Cleaning up expired files")
for _, ns := range s.ns_svc.All() {
expiry := time.Now().Add(-ns.AllowanceDuration)
dfs, err := s.swamp_file_repo.DeleteOlderThan(strconv.FormatInt(ns.ID, 10), expiry)
if err != nil {
panic(err)
}
for _, df := range dfs {
s.eventBus.Handle(*util.NewEvent("FileDeleted", struct {
Name string
Size int64
}{
ns.Name,
df.Size,
}))
}
}
return nil
}

@ -0,0 +1,187 @@
package dataswamp
import (
"bytes"
"caj-larsson/bog/dataswamp/namespace"
"caj-larsson/bog/dataswamp/swampfile"
m_namespace "caj-larsson/bog/infrastructure/memory/namespace"
m_swampfile "caj-larsson/bog/infrastructure/memory/swampfile"
"caj-larsson/bog/infrastructure/system_time"
"fmt"
"github.com/matryer/is"
"github.com/spf13/afero"
"testing"
"time"
)
type TestLogger struct{}
func (t TestLogger) Debug(format string, a ...interface{}) {}
func (t TestLogger) Info(format string, a ...interface{}) {}
func (t TestLogger) Warn(format string, a ...interface{}) {}
var file_ref1 = swampfile.FileReference{"/path1", "ns1"}
var file_ref2 = swampfile.FileReference{"/path1", "ns2"}
var file_ref3 = swampfile.FileReference{"/path2", "ns1"}
func NewTestDataSwampService() DataSwampService {
file_repo := m_swampfile.NewRepository()
ns_repo := m_namespace.NewRepository()
logger := TestLogger{}
ns_svc := namespace.NewNamespaceService(
ns_repo,
logger,
system_time.Clock{},
time.Hour,
1024,
)
return *NewDataSwampService(*ns_svc, file_repo, logger)
}
func TestFileDontExist(t *testing.T) {
is := is.New(t)
s := NewTestDataSwampService()
outfile, err := s.OpenOutFile(file_ref1)
is.True(err == swampfile.ErrNotExists)
is.True(outfile == nil)
}
func TestFileIsStored(t *testing.T) {
is := is.New(t)
s := NewTestDataSwampService()
fakefile := bytes.NewBufferString("My bog data")
err := s.SaveFile(file_ref1, fakefile, int64(fakefile.Len()))
is.NoErr(err)
largefakefile := bytes.NewBufferString("")
for largefakefile.Len() < 64000 {
_, err = largefakefile.WriteString("A very repetitive file")
}
err = s.SaveFile(file_ref3, largefakefile, int64(largefakefile.Len()))
is.Equal(err, swampfile.ErrExceedQuota)
}
func TestFileIsReadBack(t *testing.T) {
is := is.New(t)
s := NewTestDataSwampService()
infile := bytes.NewBufferString("My bog data")
_ = s.SaveFile(file_ref1, infile, int64(infile.Len()))
outswampfile, _ := s.OpenOutFile(file_ref1)
outfile := bytes.NewBufferString("")
_, _ = outfile.ReadFrom(outswampfile)
is.Equal(outfile.String(), "My bog data")
}
func TestNSIsolation(t *testing.T) {
is := is.New(t)
s := NewTestDataSwampService()
ns1_file := bytes.NewBufferString("My bog data ns1")
ns2_file := bytes.NewBufferString("My bog data ns2")
_ = s.SaveFile(file_ref1, ns1_file, int64(ns1_file.Len()))
_ = s.SaveFile(file_ref2, ns2_file, int64(ns2_file.Len()))
outswampfile, _ := s.OpenOutFile(file_ref1)
outfile := bytes.NewBufferString("")
_, _ = outfile.ReadFrom(outswampfile)
is.Equal(outfile.String(), "My bog data ns1")
}
func TestPathStrictMode(t *testing.T) {
is := is.New(t)
s := NewTestDataSwampService()
ns_file := bytes.NewBufferString("My bog data ns1")
ref := swampfile.FileReference{
"/path/../with/../backrefs",
"ns1",
}
outfile, err := s.OpenOutFile(ref)
is.Equal(err, swampfile.ErrUnacceptablePath)
is.Equal(outfile, nil)
err = s.SaveFile(ref, ns_file, int64(ns_file.Len()))
is.Equal(err, swampfile.ErrUnacceptablePath)
}
func TestQuotaWithContenSizeLieOver(t *testing.T) {
is := is.New(t)
s := NewTestDataSwampService()
largefakefile := bytes.NewBufferString("")
for largefakefile.Len() < 64000 {
_, _ = largefakefile.WriteString("A very repetitive file")
}
err := s.SaveFile(file_ref3, largefakefile, int64(10))
is.Equal(err, swampfile.ErrContentSizeExceeded)
}
func TestQuotaWithContenSizeLieUnder(t *testing.T) {
is := is.New(t)
s := NewTestDataSwampService()
largefakefile := bytes.NewBufferString("small")
err := s.SaveFile(file_ref3, largefakefile, int64(1024))
is.Equal(err, swampfile.ErrContentSizeExaggerated)
}
func TestCleanUpExpired(t *testing.T) {
is := is.New(t)
fs := afero.NewMemMapFs()
file_repo := m_swampfile.Repository{fs}
ns_repo := m_namespace.NewRepository()
logger := TestLogger{}
ns_svc := namespace.NewNamespaceService(
ns_repo,
logger,
system_time.Clock{},
time.Hour,
1024,
)
s := NewDataSwampService(*ns_svc, file_repo, logger)
fakefile := bytes.NewBufferString("My bog")
err := s.SaveFile(file_ref1, fakefile, int64(fakefile.Len()))
is.NoErr(err)
fakefile = bytes.NewBufferString("My bog")
err = s.SaveFile(file_ref3, fakefile, int64(fakefile.Len()))
is.NoErr(err)
err = fs.Chtimes("1/path1", time.Now(), time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))
is.NoErr(s.CleanUpExpiredFiles())
ns, err := ns_repo.GetByName("ns1")
fmt.Printf("file final usage %v\n", ns.FileQuota)
is.NoErr(err)
fmt.Printf("file\n")
is.Equal(ns.FileQuota.CurrentUsage, int64(len("My bog")))
}

@ -0,0 +1,26 @@
package swampfile
import (
"errors"
"time"
)
type SwampFile struct {
UserAgentId int64
Path string
Size int64
CreatedAt time.Time
}
var (
ErrDuplicate = errors.New("record already exists")
ErrExceedQuota = errors.New("file too large")
ErrQuotaInvalid = errors.New("quota invalid")
ErrContentSizeExceeded = errors.New("content size exceeded")
ErrContentSizeExaggerated = errors.New("content size exaggerated")
ErrNotExists = errors.New("row not exists")
ErrUpdateFailed = errors.New("update failed")
ErrDeleteFailed = errors.New("delete failed")
ErrUnacceptablePath = errors.New("unacceptable path")
ErrCorrupted = errors.New("repository contains unexpected data")
)

@ -0,0 +1,67 @@
package swampfile
import (
"github.com/matryer/is"
"testing"
"time"
)
func RepositoryContract(fac func() Repository, t *testing.T) {
basicFileOperationContract(fac, t)
}
func basicFileOperationContract(fac func() Repository, t *testing.T) {
is := is.New(t)
repo := fac()
not_file, err := repo.Open("doesnot", "exist")
is.Equal(err, ErrNotExists)
is.Equal(not_file, nil)
new_file, err := repo.Create("newfile.new", "ua1")
is.NoErr(err)
is.True(new_file != nil)
var testdata = "testingdata"
new_file.Write([]byte(testdata))
new_file.Close()
reopened_file, err := repo.Open("newfile.new", "ua1")
is.NoErr(err)
is.True(reopened_file != nil)
is.Equal(reopened_file.Size(), int64(len(testdata)))
readback := make([]byte, 128)
size, err := reopened_file.Read(readback)
is.Equal(string(readback[0:size]), testdata)
reopened_file.Close()
repo.Delete("newfile.new", "ua1")
deleted_file, err := repo.Open("newfile.new", "ua1")
is.Equal(err, ErrNotExists)
is.True(deleted_file == nil)
// DeleteOlderThan
expiring_file, err := repo.Create("deep/dir/expiring.new", "ua1")
is.NoErr(err)
expiring_file.Write([]byte(testdata))
expiring_file.Close()
_, err = repo.DeleteOlderThan("ua1", time.Now().Add(time.Hour))
is.NoErr(err)
expired_file, err := repo.Open("deep/dir/expiring.new", "ua1")
is.Equal(err, ErrNotExists)
is.True(expired_file == nil)
}

@ -0,0 +1,10 @@
package swampfile
import "time"
type Repository interface {
Create(filename string, namespace_stub string) (SwampInFile, error)
Open(filename string, namespace_stub string) (SwampOutFile, error)
Delete(filename string, namespace_stub string)
DeleteOlderThan(namespace_stub string, ts time.Time) ([]DeletedSwampFile, error)
}

@ -0,0 +1,52 @@
package swampfile
import (
"io"
"path"
"path/filepath"
"strings"
"time"
)
type SwampOutFile interface {
Path() string
Size() int64
Modified() time.Time
io.Reader
io.Seeker
io.Closer
}
type SwampInFile interface {
Path() string
Size() int64
Modified() time.Time
io.Writer
io.Seeker
io.Closer
}
type FileReference struct {
Path string
UserAgent string
}
type DeletedSwampFile struct {
Path string
Size int64
}
func (fr *FileReference) Clean(strict bool) (*FileReference, error) {
c := filepath.FromSlash(path.Clean("/" + strings.Trim(fr.Path, "/")))
if c != fr.Path && strict {
return nil, ErrUnacceptablePath
}
n := FileReference{
c,
fr.UserAgent,
}
return &n, nil
}

@ -0,0 +1,44 @@
package swampfile
import (
"github.com/matryer/is"
"testing"
)
func TestSwampPathNotStrict(t *testing.T) {
cases := []struct {
dirty string
clean string
}{
{"/", "/"},
{"..", "/"},
{"/../a", "/a"},
{"/../a/../a", "/a"},
{"../b", "/b"},
}
is := is.New(t)
for _, tc := range cases {
rf := FileReference{
tc.dirty,
"ns",
}
rc, err := rf.Clean(false)
is.NoErr(err)
is.Equal(rc.Path, tc.clean)
}
}
func TestSwampPathStrict(t *testing.T) {
is := is.New(t)
rf := FileReference{
"/a/../b",
"ns",
}
_, err := rf.Clean(true)
is.Equal(err, ErrUnacceptablePath)
}

@ -2,6 +2,10 @@
port = 8002
host = "127.0.0.1"
[admin]
port = 8003
host = "127.0.0.1"
[file]
path = "/tmp/datta2"

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@ -1,21 +0,0 @@
package domain
import "errors"
var (
ErrNoUserAgent = errors.New("that useragent does not exist")
)
type UserAgentRepository interface{
Create(useragent UserAgent) (*UserAgent, error)
All() ([]UserAgent, error)
GetByName(name string) (*UserAgent, error)
Update(id int64, useragent UserAgent) (*UserAgent, error)
Delete(id int64) error
}
type FileDataRepository interface {
Create(filename string, user_agent_label string) (BogInFile, error)
Open(filename string, user_agent_label string) (BogOutFile, error)
Delete(filename string, user_agent_label string)
}

@ -1,83 +0,0 @@
package domain
import (
"io"
"time"
"strconv"
)
type BogFileService struct {
user_agent_repo UserAgentRepository
file_data_repo FileDataRepository
default_allowance_bytes int64
default_allowance_duration time.Duration
}
func NewBogFileService(
user_agent_repo UserAgentRepository,
file_data_repo FileDataRepository,
da_bytes int64,
da_duration time.Duration,
) BogFileService {
return BogFileService {user_agent_repo, file_data_repo, da_bytes, da_duration}
}
func (b BogFileService) getOrCreateUA(useragent_in string) *UserAgent{
ua, err := b.user_agent_repo.GetByName(useragent_in)
if err == ErrNotExists {
new_ua := UserAgent {
0, useragent_in, time.Now(), b.default_allowance_duration, FileSizeQuota { b.default_allowance_bytes, 0 },
}
created_ua, err := b.user_agent_repo.Create(new_ua)
if err != nil {
panic(err)
}
return created_ua
}
if err != nil {
panic(err)
}
return ua
}
func (b BogFileService) SaveFile(ref FileReference, src io.Reader, size int64) error {
user_agent := b.getOrCreateUA(ref.UserAgent)
if !user_agent.FileQuota.Allows(size) {
return ErrExceedQuota
}
f, err := b.file_data_repo.Create(ref.Path, strconv.FormatInt(user_agent.ID, 10))
if err != nil {
return err
}
io.Copy(f, src)
f.Close()
user_agent.FileQuota.Add(size)
b.user_agent_repo.Update(user_agent.ID, *user_agent)
return nil
}
func (b BogFileService) OpenOutFile(ref FileReference) (BogOutFile, error) {
user_agent, err := b.user_agent_repo.GetByName(ref.UserAgent)
if err == ErrNotExists {
return nil, err
}
f, err := b.file_data_repo.Open(ref.Path, strconv.FormatInt(user_agent.ID, 10))
if err != nil {
return nil, err
}
return f, nil
}

@ -1,58 +0,0 @@
package domain
import (
"io"
"time"
)
type FileSizeQuota struct {
AllowanceKB int64
CurrentUsage int64
}
func (f *FileSizeQuota) Allows(size int64) bool {
return f.CurrentUsage + size <= f.AllowanceKB
}
func (f *FileSizeQuota) Add(size int64) error {
if !f.Allows(size) {
return ErrExceedQuota
}
f.CurrentUsage += size
return nil
}
func (f *FileSizeQuota) Remove(size int64) error {
if size > f.CurrentUsage {
return ErrQuotaInvalid
}
f.CurrentUsage -= size
return nil
}
type BogOutFile interface {
Path() string
Size() int64
Modified() time.Time
io.Reader
io.Seeker
io.Closer
}
type BogInFile interface {
Path() string
Size() int64
Modified() time.Time
io.Writer
io.Seeker
io.Closer
}
type FileReference struct {
Path string
UserAgent string
}

@ -5,6 +5,7 @@ go 1.18
require (
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 // indirect
github.com/matryer/is v1.4.0 // indirect
github.com/mattn/go-sqlite3 v1.14.12 // indirect
github.com/spf13/afero v1.8.2 // indirect
golang.org/x/text v0.3.4 // indirect

@ -125,6 +125,8 @@ 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/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/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

@ -0,0 +1,170 @@
package swampfile
import (
"caj-larsson/bog/dataswamp/swampfile"
"errors"
"io/fs"
"os"
"path"
"path/filepath"
"time"
)
var ErrDirtyRepo = errors.New("Dirty repository without flag")
type FileSystemSwampFileData struct {
path string
size int64
mod_time time.Time
file *os.File
}
func (f FileSystemSwampFileData) Read(p []byte) (int, error) {
return f.file.Read(p)
}
func (f FileSystemSwampFileData) Write(p []byte) (int, error) {
return f.file.Write(p)
}
func (f FileSystemSwampFileData) Close() error {
return f.file.Close()
}
func (f FileSystemSwampFileData) Seek(offset int64, whence int) (int64, error) {
return f.file.Seek(offset, whence)
}
func (f FileSystemSwampFileData) Path() string {
return f.path
}
func (f FileSystemSwampFileData) Size() int64 {
return f.size
}
func (f FileSystemSwampFileData) Modified() time.Time {
stat, _ := f.file.Stat()
return stat.ModTime()
}
func (f FileSystemSwampFileData) Truncate() {
err := f.file.Truncate(0)
if err != nil {
panic(err)
}
}
type Repository struct {
Root string
}
func NewRepository(root string) (*Repository, error) {
fi, err := os.ReadDir(root)
if err != nil {
return nil, err
}
flagpath := path.Join(root, ".bogrepo")
if len(fi) == 0 {
// New Repository, write flagfile
f, err := os.OpenFile(flagpath, os.O_CREATE, 0644)
if err != nil {
return nil, err
}
f.Close()
}
flagfile, err := os.OpenFile(flagpath, os.O_RDONLY, 0)
if err != nil {
return nil, ErrDirtyRepo
}
flagfile.Close()
return &Repository{root}, nil
}
func (f Repository) absPath(filename string, namespace_ns string) string {
return path.Join(f.Root, namespace_ns, filename)
}
func (f Repository) Create(filename string, namespace_ns string) (swampfile.SwampInFile, error) {
abs_path := f.absPath(filename, namespace_ns)
dir := path.Dir(abs_path)
os.MkdirAll(dir, 0750)
file, err := os.OpenFile(abs_path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
stat_info, err := file.Stat()
if err != nil {
panic(err)
}
file.Truncate(0)
bfd := FileSystemSwampFileData{filename, stat_info.Size(), stat_info.ModTime(), file}
return bfd, nil
}
func (f Repository) Open(filename string, namespace_ns string) (swampfile.SwampOutFile, error) {
abs_path := f.absPath(filename, namespace_ns)
dir := path.Dir(abs_path)
os.MkdirAll(dir, 0750)
file, err := os.OpenFile(abs_path, os.O_RDONLY, 0644)
if err != nil {
return nil, swampfile.ErrNotExists
}
stat, err := file.Stat()
if err != nil {
return nil, err
}
bfd := FileSystemSwampFileData{filename, stat.Size(), time.Now(), file}
return bfd, nil
}
func (f Repository) Delete(filename string, namespace_ns string) {
abs_path := f.absPath(filename, namespace_ns)
os.Remove(abs_path)
}
func (r Repository) DeleteOlderThan(namespace_stub string, ts time.Time) ([]swampfile.DeletedSwampFile, error) {
df := []swampfile.DeletedSwampFile{}
dr := path.Join(r.Root, namespace_stub)
err := filepath.Walk(dr, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
mode := info.Mode()
if mode.IsRegular() {
if info.ModTime().Before(ts) {
df = append(df, swampfile.DeletedSwampFile{path, info.Size()})
err = os.Remove(path)
if err != nil {
panic(err)
}
}
return nil
}
if !mode.IsDir() {
return swampfile.ErrCorrupted
}
return nil
})
return df, err
}

@ -0,0 +1,71 @@
package swampfile
import (
"caj-larsson/bog/dataswamp/swampfile"
"github.com/matryer/is"
"os"
"path"
"testing"
)
func newRepoDir(t *testing.T) string {
r := t.TempDir()
d := path.Join(r, "fs")
err := os.Mkdir(d, 0755)
if err != nil {
panic(err)
}
return d
}
func TestFsFileRepo(t *testing.T) {
var fac = func() swampfile.Repository {
repo, err := NewRepository(newRepoDir(t))
if err != nil {
panic(err)
}
return repo
}
swampfile.RepositoryContract(fac, t)
}
func TestEmptyDir(t *testing.T) {
is := is.New(t)
dir_path := newRepoDir(t)
_, err := NewRepository(dir_path)
is.NoErr(err)
_, err = os.OpenFile(path.Join(dir_path, ".bogrepo"), os.O_RDONLY, 0)
is.NoErr(err)
}
func TestDirtyDir(t *testing.T) {
is := is.New(t)
dir_path := newRepoDir(t)
_, err := os.OpenFile(path.Join(dir_path, "randomfile"), os.O_CREATE, 0644)
is.NoErr(err)
_, err = NewRepository(dir_path)
is.Equal(err, ErrDirtyRepo)
}
func TestDirtyWithFlag(t *testing.T) {
is := is.New(t)
dir_path := newRepoDir(t)
_, err := os.OpenFile(path.Join(dir_path, "randomfile"), os.O_CREATE, 0644)
is.NoErr(err)
_, err = os.OpenFile(path.Join(dir_path, ".bogrepo"), os.O_CREATE, 0644)
is.NoErr(err)
_, err = NewRepository(dir_path)
is.NoErr(err)
}

@ -0,0 +1,61 @@
package namespace
import (
// "time"
"caj-larsson/bog/dataswamp/namespace"
)
type Repository struct {
IdIdx map[int64]*namespace.Namespace
NameIdx map[string]*namespace.Namespace
NextId int64
}
func NewRepository() namespace.Repository {
r := new(Repository)
r.NextId = 0
r.IdIdx = make(map[int64]*namespace.Namespace)
r.NameIdx = make(map[string]*namespace.Namespace)
return r
}
func (r *Repository) Create(ns namespace.Namespace) (*namespace.Namespace, error) {
r.NextId += 1
ns.ID = r.NextId
r.IdIdx[ns.ID] = &ns
r.NameIdx[ns.Name] = &ns
return &ns, nil
}
func (r *Repository) All() ([]namespace.Namespace, error) {
ns := make([]namespace.Namespace, 0, len(r.IdIdx))
for _, value := range r.IdIdx {
ns = append(ns, *value)
}
return ns, nil
}
func (r *Repository) GetByName(name string) (*namespace.Namespace, error) {
ns, exists := r.NameIdx[name]
if exists {
return ns, nil
}
return nil, namespace.ErrNotExists
}
func (r *Repository) Update(id int64, ns namespace.Namespace) (*namespace.Namespace, error) {
original := *r.IdIdx[id]
ns.ID = id
r.IdIdx[id] = &ns
r.NameIdx[original.Name] = &ns
return &ns, nil
}
func (r *Repository) Delete(id int64) error {
original := *r.IdIdx[id]
delete(r.NameIdx, original.Name)
delete(r.IdIdx, original.ID)
return nil
}

@ -0,0 +1,10 @@
package namespace
import (
"caj-larsson/bog/dataswamp/namespace"
"testing"
)
func TestFileRepo(t *testing.T) {
namespace.RepositoryContract(NewRepository, t)
}

@ -0,0 +1,120 @@
package swampfile
import (
"caj-larsson/bog/dataswamp/swampfile"
"github.com/spf13/afero"
"io/fs"
"os"
"path"
"time"
)
type SwampFile struct {
filename string
file afero.File
}
func (f SwampFile) Path() string {
return f.filename
}
func (f SwampFile) Size() int64 {
stat, _ := f.file.Stat()
return int64(stat.Size())
}
func (f SwampFile) Read(p []byte) (int, error) {
return f.file.Read(p)
}
func (f SwampFile) Write(p []byte) (int, error) {
return f.file.Write(p)
}
func (f SwampFile) Close() error {
return f.file.Close()
}
func (f SwampFile) Seek(offset int64, whence int) (int64, error) {
return f.file.Seek(offset, whence)
}
func (f SwampFile) Modified() time.Time {
stat, _ := f.file.Stat()
return stat.ModTime()
}
func (f SwampFile) Truncate() {
err := f.file.Truncate(0)
if err != nil {
panic(err)
}
}
// The actual repository
type Repository struct {
Fs afero.Fs
}
func NewRepository() swampfile.Repository {
return Repository{afero.NewMemMapFs()}
}
func (r Repository) Create(filename string, namespace_stub string) (swampfile.SwampInFile, error) {
abs_path := path.Join(namespace_stub, filename)
dir := path.Dir(abs_path)
r.Fs.MkdirAll(dir, 0750)
file, err := r.Fs.OpenFile(abs_path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
bf := SwampFile{filename, file}
return bf, nil
}
func (r Repository) Open(filename string, namespace_stub string) (swampfile.SwampOutFile, error) {
abs_path := path.Join(namespace_stub, filename)
dir := path.Dir(abs_path)
r.Fs.MkdirAll(dir, 0750)
file, err := r.Fs.OpenFile(abs_path, os.O_RDONLY, 0644)
if err != nil {
return nil, swampfile.ErrNotExists
}
bf := SwampFile{filename, file}
return bf, nil
}
func (r Repository) Delete(filename string, namespace_stub string) {
abs_path := path.Join(namespace_stub, filename)
r.Fs.Remove(abs_path)
}
func (r Repository) DeleteOlderThan(namespace_stub string, ts time.Time) ([]swampfile.DeletedSwampFile, error) {
df := []swampfile.DeletedSwampFile{}
err := afero.Walk(r.Fs, namespace_stub, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.Mode().IsRegular() {
if info.ModTime().Before(ts) {
df = append(df, swampfile.DeletedSwampFile{path, info.Size()})
r.Fs.Remove(path)
}
return nil
}
if !info.Mode().IsDir() {
return swampfile.ErrCorrupted
}
return nil
})
return df, err
}

@ -0,0 +1,10 @@
package swampfile
import (
"caj-larsson/bog/dataswamp/swampfile"
"testing"
)
func TestFileRepo(t *testing.T) {
swampfile.RepositoryContract(NewRepository, t)
}

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
package namespace
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

@ -0,0 +1,26 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
package namespace
import (
"database/sql"
)
type FileStat struct {
ID int64
Num int64
SizeB int64
}
type Namespace struct {
ID int64
Name string
Lastseen int64
AllowanceTime sql.NullInt64
QuotaKb sql.NullInt64
QuotaUsageKb sql.NullInt64
DownloadID int64
UploadID int64
}

@ -0,0 +1,75 @@
-- name: CreateNamespace :one
INSERT INTO
namespace(
name,
lastseen,
allowance_time,
quota_kb,
quota_usage_kb,
download_id,
upload_id
)
values(?, ?, ?, ?, ?, ?, ?)
returning id;
-- name: CreateFileStats :one
INSERT INTO file_stats(num, size_b)
values(?, ?)
returning id;
-- name: AllNamespaces :many
SELECT
ns.id,
ns.name,
ns.lastseen,
ns.allowance_time,
ns.quota_kb,
ns.quota_usage_kb,
d.num as d_num,
d.size_b as d_size_b,
ul.num as ul_num,
ul.size_b as ul_size_b
FROM namespace as ns
JOIN file_stats as d
ON ns.download_id = d.id
JOIN file_stats as ul
ON ns.upload_id = ul.id;
-- name: GetNamespaceByName :one
SELECT
ns.id,
ns.name,
ns.lastseen,
ns.allowance_time,
ns.quota_kb,
ns.quota_usage_kb,
d.num as d_num,
d.size_b as d_size_b,
ul.num as ul_num,
ul.size_b as ul_size_b
FROM namespace as ns
JOIN file_stats as d
ON ns.download_id = d.id
JOIN file_stats as ul
ON ns.upload_id = ul.id
WHERE ns.name = ?;
-- name: GetFileStat :one
SELECT * FROM file_stats where id = ?;
-- name: UpdateFileStat :exec
UPDATE file_stats SET num = ?, size_b = ? where id = ?;
-- name: UpdateNamespace :one
UPDATE namespace SET
name = ?,
lastseen = ?,
allowance_time = ?,
quota_kb = ?,
quota_usage_kb = ?
WHERE id = ?
RETURNING download_id, upload_id;
-- name: DeleteNameSpace :exec
DELETE FROM namespace where id = ?;

@ -0,0 +1,260 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: queries.sql
package namespace
import (
"context"
"database/sql"
)
const allNamespaces = `-- name: AllNamespaces :many
SELECT
ns.id,
ns.name,
ns.lastseen,
ns.allowance_time,
ns.quota_kb,
ns.quota_usage_kb,
d.num as d_num,
d.size_b as d_size_b,
ul.num as ul_num,
ul.size_b as ul_size_b
FROM namespace as ns
JOIN file_stats as d
ON ns.download_id = d.id
JOIN file_stats as ul
ON ns.upload_id = ul.id
`
type AllNamespacesRow struct {
ID int64
Name string
Lastseen int64
AllowanceTime sql.NullInt64
QuotaKb sql.NullInt64
QuotaUsageKb sql.NullInt64
DNum int64
DSizeB int64
UlNum int64
UlSizeB int64
}
func (q *Queries) AllNamespaces(ctx context.Context) ([]AllNamespacesRow, error) {
rows, err := q.db.QueryContext(ctx, allNamespaces)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AllNamespacesRow
for rows.Next() {
var i AllNamespacesRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Lastseen,
&i.AllowanceTime,
&i.QuotaKb,
&i.QuotaUsageKb,
&i.DNum,
&i.DSizeB,
&i.UlNum,
&i.UlSizeB,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const createFileStats = `-- name: CreateFileStats :one
INSERT INTO file_stats(num, size_b)
values(?, ?)
returning id
`
type CreateFileStatsParams struct {
Num int64
SizeB int64
}
func (q *Queries) CreateFileStats(ctx context.Context, arg CreateFileStatsParams) (int64, error) {
row := q.db.QueryRowContext(ctx, createFileStats, arg.Num, arg.SizeB)
var id int64
err := row.Scan(&id)
return id, err
}
const createNamespace = `-- name: CreateNamespace :one
INSERT INTO
namespace(
name,
lastseen,
allowance_time,
quota_kb,
quota_usage_kb,
download_id,
upload_id
)
values(?, ?, ?, ?, ?, ?, ?)
returning id
`
type CreateNamespaceParams struct {
Name string
Lastseen int64
AllowanceTime sql.NullInt64
QuotaKb sql.NullInt64
QuotaUsageKb sql.NullInt64
DownloadID int64
UploadID int64
}
func (q *Queries) CreateNamespace(ctx context.Context, arg CreateNamespaceParams) (int64, error) {
row := q.db.QueryRowContext(ctx, createNamespace,
arg.Name,
arg.Lastseen,
arg.AllowanceTime,
arg.QuotaKb,
arg.QuotaUsageKb,
arg.DownloadID,
arg.UploadID,
)
var id int64
err := row.Scan(&id)
return id, err
}
const deleteNameSpace = `-- name: DeleteNameSpace :exec
DELETE FROM namespace where id = ?
`
func (q *Queries) DeleteNameSpace(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteNameSpace, id)
return err
}
const getFileStat = `-- name: GetFileStat :one
SELECT id, num, size_b FROM file_stats where id = ?
`
func (q *Queries) GetFileStat(ctx context.Context, id int64) (FileStat, error) {
row := q.db.QueryRowContext(ctx, getFileStat, id)
var i FileStat
err := row.Scan(&i.ID, &i.Num, &i.SizeB)
return i, err
}
const getNamespaceByName = `-- name: GetNamespaceByName :one
SELECT
ns.id,
ns.name,
ns.lastseen,
ns.allowance_time,
ns.quota_kb,
ns.quota_usage_kb,
d.num as d_num,
d.size_b as d_size_b,
ul.num as ul_num,
ul.size_b as ul_size_b
FROM namespace as ns
JOIN file_stats as d
ON ns.download_id = d.id
JOIN file_stats as ul
ON ns.upload_id = ul.id
WHERE ns.name = ?
`
type GetNamespaceByNameRow struct {
ID int64
Name string
Lastseen int64
AllowanceTime sql.NullInt64
QuotaKb sql.NullInt64
QuotaUsageKb sql.NullInt64
DNum int64
DSizeB int64
UlNum int64
UlSizeB int64
}
func (q *Queries) GetNamespaceByName(ctx context.Context, name string) (GetNamespaceByNameRow, error) {
row := q.db.QueryRowContext(ctx, getNamespaceByName, name)
var i GetNamespaceByNameRow
err := row.Scan(
&i.ID,
&i.Name,
&i.Lastseen,
&i.AllowanceTime,
&i.QuotaKb,
&i.QuotaUsageKb,
&i.DNum,
&i.DSizeB,
&i.UlNum,
&i.UlSizeB,
)
return i, err
}
const updateFileStat = `-- name: UpdateFileStat :exec
UPDATE file_stats SET num = ?, size_b = ? where id = ?
`
type UpdateFileStatParams struct {
Num int64
SizeB int64
ID int64
}
func (q *Queries) UpdateFileStat(ctx context.Context, arg UpdateFileStatParams) error {
_, err := q.db.ExecContext(ctx, updateFileStat, arg.Num, arg.SizeB, arg.ID)
return err
}
const updateNamespace = `-- name: UpdateNamespace :one
UPDATE namespace SET
name = ?,
lastseen = ?,
allowance_time = ?,
quota_kb = ?,
quota_usage_kb = ?
WHERE id = ?
RETURNING download_id, upload_id
`
type UpdateNamespaceParams struct {
Name string
Lastseen int64
AllowanceTime sql.NullInt64
QuotaKb sql.NullInt64
QuotaUsageKb sql.NullInt64
ID int64
}
type UpdateNamespaceRow struct {
DownloadID int64
UploadID int64
}
func (q *Queries) UpdateNamespace(ctx context.Context, arg UpdateNamespaceParams) (UpdateNamespaceRow, error) {
row := q.db.QueryRowContext(ctx, updateNamespace,
arg.Name,
arg.Lastseen,
arg.AllowanceTime,
arg.QuotaKb,
arg.QuotaUsageKb,
arg.ID,
)
var i UpdateNamespaceRow
err := row.Scan(&i.DownloadID, &i.UploadID)
return i, err
}

@ -0,0 +1,45 @@
package namespace
import (
"caj-larsson/bog/dataswamp/namespace"
"errors"
"time"
)
type NamespaceRecord struct {
ID int64
Name string
LastSeen string
AllowanceSeconds int64
QuotaKB int64
QuotaUsedKB int64
}
var ErrUnparseableRecord = errors.New("record could not be mapped to entity")
func (r *NamespaceRecord) toEntity() (*namespace.Namespace, error) {
lastseen, err := time.Parse(time.RFC3339, r.LastSeen)
if err != nil {
return nil, ErrUnparseableRecord
}
var ns = new(namespace.Namespace)
ns.ID = r.ID
ns.Name = r.Name
ns.LastSeen = lastseen
ns.AllowanceDuration = time.Duration(r.AllowanceSeconds * int64(time.Second))
ns.FileQuota = namespace.FileSizeQuota{r.QuotaKB, r.QuotaUsedKB}
return ns, err
}
func fromEntity(ns namespace.Namespace) (*NamespaceRecord, error) {
var record = new(NamespaceRecord)
record.ID = ns.ID
record.Name = ns.Name
record.LastSeen = ns.LastSeen.Format(time.RFC3339)
record.AllowanceSeconds = int64(ns.AllowanceDuration.Seconds())
record.QuotaKB = ns.FileQuota.AllowanceKB
record.QuotaUsedKB = ns.FileQuota.CurrentUsage
return record, nil
}

@ -0,0 +1,189 @@
package namespace
import (
"caj-larsson/bog/dataswamp/namespace"
"context"
"database/sql"
"github.com/mattn/go-sqlite3"
"time"
)
var _ = sqlite3.ErrError
type Repository struct {
db *sql.DB
}
func (r *Repository) migrate() error {
query := `
CREATE TABLE IF NOT EXISTS file_stats(
id INTEGER PRIMARY KEY,
num BIGINT NOT NULL,
size_b BIGINT NOT NULL
);`
_, err := r.db.Exec(query)
if err != nil {
return err
}
query = `
CREATE TABLE IF NOT EXISTS namespace(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
lastseen TEXT,
allowance_time BIGINT,
quota_kb BIGINT,
quota_usage_kb BIGINT,
download_id BIGINT NOT NULL REFERENCES file_stats(Id),
upload_id BIGINT NOT NULL REFERENCES file_stats(Id)
);`
_, err = r.db.Exec(query)
return err
}
func NewRepository(filename string) namespace.Repository {
db, err := sql.Open("sqlite3", filename)
if err != nil {
panic(err)
}
repo := Repository{
db: db,
}
repo.migrate()
return &repo
}
func (q *Queries) createFileStats(ctx context.Context, fstat namespace.FileStat) (int64, error) {
return q.CreateFileStats(ctx, CreateFileStatsParams{fstat.Num, fstat.SizeB})
}
func (r *Repository) Create(ns namespace.Namespace) (*namespace.Namespace, error) {
ctx := context.Background()
q := New(r.db)
dl_id, err := q.createFileStats(ctx, ns.Download)
if err != nil {
return nil, err
}
ul_id, err := q.createFileStats(ctx, ns.Upload)
if err != nil {
return nil, err
}
ns.LastSeen = ns.LastSeen.Round(time.Microsecond)
p := CreateNamespaceParams{
Name: ns.Name,
Lastseen: ns.LastSeen.UnixMicro(),
AllowanceTime: sql.NullInt64{int64(ns.AllowanceDuration.Seconds()), true},
QuotaKb: sql.NullInt64{int64(ns.FileQuota.AllowanceKB), true},
QuotaUsageKb: sql.NullInt64{int64(ns.FileQuota.CurrentUsage), true},
DownloadID: dl_id,
UploadID: ul_id,
}
id, err := q.CreateNamespace(ctx, p)
if err != nil {
return nil, err
}
ns.ID = id
return &ns, nil
}
func (r *Repository) All() ([]namespace.Namespace, error) {
ctx := context.Background()
q := New(r.db)
rows, err := q.AllNamespaces(ctx)
if err != nil {
return nil, err
}
var all []namespace.Namespace
for _, row := range rows {
ns := namespace.Namespace{
row.ID,
row.Name,
time.UnixMicro(row.Lastseen),
time.Duration(row.AllowanceTime.Int64 * int64(time.Second)),
namespace.FileSizeQuota{row.QuotaKb.Int64, row.QuotaUsageKb.Int64},
namespace.FileStat{row.DNum, row.DSizeB},
namespace.FileStat{row.UlNum, row.UlSizeB},
}
all = append(all, ns)
}
return all, nil
}
func (r *Repository) GetByName(name string) (*namespace.Namespace, error) {
ctx := context.Background()
q := New(r.db)
row, err := q.GetNamespaceByName(ctx, name)
if err != nil {
return nil, namespace.ErrNotExists
}
ns := namespace.Namespace{
row.ID,
row.Name,
time.UnixMicro(row.Lastseen),
time.Duration(row.AllowanceTime.Int64 * int64(time.Second)),
namespace.FileSizeQuota{row.QuotaKb.Int64, row.QuotaUsageKb.Int64},
namespace.FileStat{row.DNum, row.DSizeB},
namespace.FileStat{row.UlNum, row.UlSizeB},
}
return &ns, nil
}
func (q *Queries) updateFileStat(ctx context.Context, id int64, fstat namespace.FileStat) error {
return q.UpdateFileStat(ctx, UpdateFileStatParams{fstat.Num, fstat.SizeB, id})
}
func (r *Repository) Update(id int64, ns namespace.Namespace) (*namespace.Namespace, error) {
ctx := context.Background()
q := New(r.db)
ids, err := q.UpdateNamespace(ctx, UpdateNamespaceParams{
ns.Name,
ns.LastSeen.Round(time.Microsecond).UnixMicro(),
sql.NullInt64{int64(ns.AllowanceDuration.Seconds()), true},
sql.NullInt64{int64(ns.FileQuota.AllowanceKB), true},
sql.NullInt64{int64(ns.FileQuota.CurrentUsage), true},
ns.ID,
})
err = q.updateFileStat(ctx, ids.DownloadID, ns.Download)
if err != nil {
return nil, err
}
err = q.updateFileStat(ctx, ids.UploadID, ns.Upload)
if err != nil {
return nil, err
}
return &ns, nil
}
func (r *Repository) Delete(id int64) error {
ctx := context.Background()
q := New(r.db)
err := q.DeleteNameSpace(ctx, id)
if err != nil {
return namespace.ErrDeleteFailed
}
return nil
}

@ -0,0 +1,10 @@
package namespace
import (
"caj-larsson/bog/dataswamp/namespace"
"testing"
)
func TestFileRepo(t *testing.T) {
namespace.RepositoryContract(func() namespace.Repository { return NewRepository(":memory:") }, t)
}

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS file_stats(
id BIGINT PRIMARY KEY,
num BIGINT NOT NULL,
size_b BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS namespace(
id BIGINT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
lastseen BIGINT NOT NULL,
allowance_time BIGINT,
quota_kb BIGINT,
quota_usage_kb BIGINT,
download_id BIGINT NOT NULL REFERENCES file_stats(Id) ON DELETE CASCADE,
upload_id BIGINT NOT NULL REFERENCES file_stats(Id) ON DELETE CASCADE
);

@ -0,0 +1,11 @@
package system_time
import (
"time"
)
type Clock struct{}
func (c Clock) Now() time.Time {
return time.Now()
}

@ -1,94 +0,0 @@
package integration
import (
"time"
"os"
"path"
"caj-larsson/bog/domain"
)
type FileSystemBogFileData struct {
path string
size int64
mod_time time.Time
file *os.File
}
func (f FileSystemBogFileData) Read(p []byte) (int, error) {
return f.file.Read(p)
}
func (f FileSystemBogFileData) Write(p []byte) (int, error) {
return f.file.Write(p)
}
func (f FileSystemBogFileData) Close() error {
return f.file.Close()
}
func (f FileSystemBogFileData) Seek(offset int64, whence int) (int64, error) {
return f.file.Seek(offset, whence)
}
func (f FileSystemBogFileData) Path() string {
return f.path
}
func (f FileSystemBogFileData) Size() int64 {
return f.size
}
func (f FileSystemBogFileData) Modified() time.Time{
return time.Now()
}
type FileSystemBogRepository struct {
Root string
}
func (f FileSystemBogRepository) absPath(filename string, user_agent_label string) string {
return path.Join(f.Root, user_agent_label, filename)
}
func (f FileSystemBogRepository) Create(filename string, user_agent_label string) (domain.BogInFile, error) {
abs_path := f.absPath(filename, user_agent_label)
dir := path.Dir(abs_path)
os.MkdirAll(dir, 0750)
file, err := os.OpenFile(abs_path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
stat_info, err := file.Stat()
if err != nil {
panic(err)
}
bfd := FileSystemBogFileData {filename, stat_info.Size(), stat_info.ModTime(), file}
return bfd, nil
}
func (f FileSystemBogRepository) Open(filename string, user_agent_label string) (domain.BogOutFile, error) {
abs_path := f.absPath(filename, user_agent_label)
dir := path.Dir(abs_path)
os.MkdirAll(dir, 0750)
file, err := os.OpenFile(abs_path, os.O_RDONLY, 0644)
if err != nil {
return nil, domain.ErrNotExists
}
bfd := FileSystemBogFileData {filename, 0, time.Now(), file}
return bfd, nil
}
func (f FileSystemBogRepository) Delete(filename string, user_agent_label string) {
abs_path := f.absPath(filename, user_agent_label)
os.Remove(abs_path)
}

@ -1,45 +0,0 @@
package integration
import (
"errors"
"time"
"caj-larsson/bog/domain"
)
type UserAgentDBRecord struct {
ID int64
Name string
LastSeen string
AllowanceSeconds int64
QuotaKB int64
QuotaUsedKB int64
}
var ErrUnparseableRecord = errors.New("record could not be mapped to entity")
func (r *UserAgentDBRecord) toEntity() (*domain.UserAgent, error) {
lastseen, err := time.Parse(time.RFC3339, r.LastSeen)
if err != nil {
return nil, ErrUnparseableRecord
}
var useragent = new(domain.UserAgent)
useragent.ID = r.ID
useragent.Name = r.Name
useragent.LastSeen = lastseen
useragent.AllowanceDuration = time.Duration(r.AllowanceSeconds * int64(time.Second))
useragent.FileQuota = domain.FileSizeQuota { r.QuotaKB, r.QuotaUsedKB }
return useragent, err
}
func fromEntity(useragent domain.UserAgent) (*UserAgentDBRecord, error) {
var record = new(UserAgentDBRecord)
record.ID = useragent.ID
record.Name = useragent.Name
record.LastSeen = useragent.LastSeen.Format(time.RFC3339)
record.AllowanceSeconds = int64(useragent.AllowanceDuration.Seconds())
record.QuotaKB = useragent.FileQuota.AllowanceKB
record.QuotaUsedKB = useragent.FileQuota.CurrentUsage
return record, nil
}

@ -1,154 +0,0 @@
package integration
import (
"caj-larsson/bog/domain"
"database/sql"
"errors"
"github.com/mattn/go-sqlite3"
)
type SQLiteUserAgentRepository struct {
db *sql.DB
}
func NewSQLiteUserAgentRepository(filename string) *SQLiteUserAgentRepository {
db, err := sql.Open("sqlite3", filename)
if err != nil {
panic(err)
}
repo := SQLiteUserAgentRepository{
db: db,
}
repo.migrate()
return &repo
}
func (r SQLiteUserAgentRepository) migrate() error {
query := `
CREATE TABLE IF NOT EXISTS useragent(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
lastseen text,
allowance_time bigint,
quota_kb bigint,
quota_usage_kb bigint
);
`
_, err := r.db.Exec(query)
return err
}
func (r *SQLiteUserAgentRepository) Create(useragent domain.UserAgent) (*domain.UserAgent, error) {
var record, err = fromEntity(useragent)
if err != nil {
}
res, err := r.db.Exec(
"INSERT INTO useragent(name, lastseen, allowance_time, quota_kb, quota_usage_kb) values(?,?,?,?,?)",
record.Name, record.LastSeen, record.AllowanceSeconds, record.QuotaKB, record.QuotaUsedKB,
)
if err != nil {
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) {
if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintUnique) {
return nil, domain.ErrDuplicate
}
}
return nil, err
}
id, err := res.LastInsertId()
if err != nil {
return nil, err
}
useragent.ID = id
return &useragent, nil
}
func (r SQLiteUserAgentRepository) All() ([]domain.UserAgent, error) {
rows, err := r.db.Query("SELECT * FROM useragent")
if err != nil {
return nil, err
}
defer rows.Close()
var all []domain.UserAgent
for rows.Next() {
var record UserAgentDBRecord
if err := rows.Scan(&record.ID, &record.Name, &record.LastSeen, &record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB); err != nil {
return nil, err
}
var useragent, err = record.toEntity()
if err != nil {
return nil, err
}
all = append(all, *useragent)
}
return all, nil
}
func (r SQLiteUserAgentRepository) GetByName(name string) (*domain.UserAgent, error) {
row := r.db.QueryRow("SELECT id, name, lastseen, allowance_time, quota_kb, quota_usage_kb FROM useragent WHERE name = ?", name)
var record UserAgentDBRecord
if err := row.Scan(&record.ID, &record.Name, &record.LastSeen, &record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrNotExists
}
return nil, err
}
var useragent, err = record.toEntity()
if err != nil {
return nil, err
}
return useragent, nil
}
func (r SQLiteUserAgentRepository) Update(id int64, updated domain.UserAgent) (*domain.UserAgent, error) {
if id == 0 {
return nil, errors.New("invalid updated ID")
}
var record, err = fromEntity(updated)
res, err := r.db.Exec("UPDATE useragent SET name = ?, lastseen = ?, allowance_time = ?, quota_kb = ?, quota_usage_kb = ? WHERE id = ?",
record.Name, record.LastSeen, record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB, id)
if err != nil {
return nil, err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, domain.ErrUpdateFailed
}
return &updated, nil
}
func (r SQLiteUserAgentRepository) Delete(id int64) error {
res, err := r.db.Exec("DELETE FROM useragent WHERE id = ?", id)
if err != nil {
return err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return domain.ErrDeleteFailed
}
return err
}

@ -1,9 +1,8 @@
package main
import (
"caj-larsson/bog/application"
"caj-larsson/bog/server"
"io/ioutil"
"fmt"
)
func main() {
@ -13,13 +12,10 @@ func main() {
panic(err)
}
config, err := application.ConfigFromToml(string(content))
config, err := server.ConfigFromToml(string(content))
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", config)
bog := application.New(config)
bog := server.New(config)
bog.Run()
}

@ -0,0 +1,163 @@
package server
import (
"caj-larsson/bog/dataswamp"
"caj-larsson/bog/dataswamp/namespace"
"caj-larsson/bog/dataswamp/swampfile"
fs_swampfile "caj-larsson/bog/infrastructure/fs/swampfile"
sql_namespace "caj-larsson/bog/infrastructure/sqlite/namespace"
"caj-larsson/bog/infrastructure/system_time"
"caj-larsson/bog/util"
"net/http"
"strconv"
"text/template"
"time"
)
type Router interface {
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
ServeHTTP(http.ResponseWriter, *http.Request)
}
type Bog struct {
router Router
adminRouter Router
file_service dataswamp.DataSwampService
address string
adminAddress string
logger util.Logger
}
func buildFileDataRepository(config FileConfig) swampfile.Repository {
r, err := fs_swampfile.NewRepository(config.Path)
if err != nil {
panic(err)
}
return r
}
func buildNamespaceRepository(config DatabaseConfig) namespace.Repository {
if config.Backend != "sqlite" {
panic("Can only handle sqlite")
}
return sql_namespace.NewRepository(config.Connection)
}
func (b *Bog) fileHandler(w http.ResponseWriter, r *http.Request) {
ref := swampfile.FileReference{r.URL.Path, r.Header["User-Agent"][0]}
if r.URL.Path == "/" {
http.NotFound(w, r)
return
}
switch r.Method {
case "GET":
swamp_file, err := b.file_service.OpenOutFile(ref)
if err == swampfile.ErrNotExists {
templ, err := template.ParseFiles("server/views/upload.html")
if err != nil {
panic(err)
}
w.WriteHeader(404)
err = templ.Execute(w, nil)
if err != nil {
panic(err)
}
return
}
if err != nil {
panic(err)
}
b.logger.Info("Serving file '%s' of size %d from '%s'", ref.Path, swamp_file.Size(), ref.UserAgent)
w.Header().Set("Content-Disposition", "attachment")
http.ServeContent(w, r, swamp_file.Path(), swamp_file.Modified(), swamp_file)
case "POST":
fallthrough
case "PUT":
size_str := r.Header["Content-Length"][0]
size, err := strconv.ParseInt(size_str, 10, 64)
if err != nil {
w.WriteHeader(422)
return
}
b.logger.Info("Recieving file '%s' of size %d from '%s'", ref.Path, size, ref.UserAgent)
err = b.file_service.SaveFile(ref, r.Body, size)
if err != nil {
panic(err)
}
}
return
}
func (b *Bog) dashboardHandler(w http.ResponseWriter, r *http.Request) {
templ, err := template.ParseFiles("server/views/dashboard.html")
if err != nil {
panic(err)
}
stats := b.file_service.NamespaceStats()
err = templ.Execute(w, stats)
if err != nil {
panic(err)
}
}
func (b *Bog) routes() {
b.router.HandleFunc("/", b.fileHandler)
// b.adminRouter.HandleFunc("/", b.dashboardHandler)
}
func (b *Bog) cleanNamespaces() {
for true {
b.file_service.CleanUpExpiredFiles()
time.Sleep(time.Minute * 10)
}
}
func New(config *Configuration) *Bog {
b := new(Bog)
b.address = config.Server.bindAddress()
b.adminAddress = config.Admin.bindAddress()
fsSwampRepo := buildFileDataRepository(config.File)
nsRepo := buildNamespaceRepository(config.Database)
logger := ServerLogger{Debug}
ns_svc := namespace.NewNamespaceService(
nsRepo,
logger,
system_time.Clock{},
config.Quota.ParsedDuration(),
config.Quota.ParsedSizeBytes(),
)
b.file_service = *dataswamp.NewDataSwampService(
*ns_svc,
fsSwampRepo,
logger,
)
b.logger = logger
b.router = new(http.ServeMux)
b.adminRouter = new(http.ServeMux)
b.routes()
return b
}
func (b *Bog) Run() {
b.logger.Info("Starting bog on address: %s", b.address)
go b.cleanNamespaces()
go func() { http.ListenAndServe(b.adminAddress, b.adminRouter) }()
http.ListenAndServe(b.address, b.router)
}

@ -0,0 +1,57 @@
package server
import (
"testing"
// "fmt"
"caj-larsson/bog/dataswamp"
"caj-larsson/bog/dataswamp/namespace"
ns "caj-larsson/bog/infrastructure/memory/namespace"
"caj-larsson/bog/infrastructure/memory/swampfile"
"caj-larsson/bog/infrastructure/system_time"
"github.com/matryer/is"
"net/http"
"net/http/httptest"
"strings"
"time"
)
type TestLogger struct{}
func (t TestLogger) Debug(format string, a ...interface{}) {}
func (t TestLogger) Info(format string, a ...interface{}) {}
func (t TestLogger) Warn(format string, a ...interface{}) {}
func TestApplication(t *testing.T) {
is := is.New(t)
logger := TestLogger{}
nsRepo := ns.NewRepository()
ns_svc := namespace.NewNamespaceService(
nsRepo,
logger,
system_time.Clock{},
time.Hour,
1000,
)
file_service := dataswamp.NewDataSwampService(
*ns_svc,
swampfile.NewRepository(),
logger,
)
bog := Bog{
router: new(http.ServeMux),
file_service: *file_service,
address: "fake",
logger: logger,
}
bog.routes()
req := httptest.NewRequest("POST", "/apath", strings.NewReader("testdata"))
req.Header.Add("User-Agent", "testingclient")
req.Header.Add("Content-Length", "8")
w := httptest.NewRecorder()
bog.router.ServeHTTP(w, req)
is.Equal(w.Code, 200)
}

@ -1,14 +1,12 @@
package application
package server
import (
"fmt"
"time"
"github.com/BurntSushi/toml"
"github.com/c2h5oh/datasize"
"time"
)
func (qc QuotaConfig) ParsedSizeBytes() int64 {
var v datasize.ByteSize
@ -38,40 +36,41 @@ func (qc QuotaConfig) ParsedDuration() time.Duration {
}
type QuotaConfig struct {
DefaultSize string `toml:"default_size"`
DefaultSize string `toml:"default_size"`
DefaultDuration string `toml:"default_duration"`
}
type ServerConfig struct {
Port int64
Host string
}
type FileConfig struct {
Path string
}
type DatabaseConfig struct {
Backend string
Backend string
Connection string
}
type LoggingConfig struct {
Level string
}
type Configuration struct {
Server ServerConfig
File FileConfig
Server ServerConfig
Admin ServerConfig
File FileConfig
Database DatabaseConfig
Quota QuotaConfig
Quota QuotaConfig
Logging LoggingConfig
}
func (c *Configuration) bindAddress() string {
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
func (c ServerConfig) bindAddress() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
func ConfigFromToml(toml_data string) (*Configuration, error) {
var config Configuration

@ -0,0 +1,36 @@
package server
import (
"github.com/matryer/is"
"testing"
"time"
)
func TestConfiguration(t *testing.T) {
is := is.New(t)
c, _ := ConfigFromToml(`
[server]
port = 8002
host = "127.0.0.1"
[admin]
port = 8001
host = "127.0.0.1"
[file]
path = "/tmp/datta2"
[database]
backend = "sqlite"
connection = "sql.db"
[quota]
default_size = "1MB"
default_duration = "72h"`,
)
is.Equal(c.Server.Port, int64(8002))
is.Equal(c.Server.Host, "127.0.0.1")
is.Equal(c.Quota.ParsedSizeBytes(), int64(1024*1024))
is.Equal(c.Quota.ParsedDuration(), time.Duration(time.Hour*72))
}

@ -0,0 +1,40 @@
package server
import (
"fmt"
"time"
)
const (
Debug int = 0
Info = 1
Warn = 2
None = 3
)
type ServerLogger struct {
level int
}
func logf(level string, format string, a ...interface{}) {
head := fmt.Sprintf("%s - [%s]: ", time.Now().Format(time.RFC3339), level)
fmt.Printf(head+format+"\n", a...)
}
func (t ServerLogger) Debug(format string, a ...interface{}) {
if t.level <= Debug {
logf("DEBUG", format, a...)
}
}
func (t ServerLogger) Info(format string, a ...interface{}) {
if t.level <= Info {
logf("INFO", format, a...)
}
}
func (t ServerLogger) Warn(format string, a ...interface{}) {
if t.level <= Warn {
logf("WARN", format, a...)
}
}

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>bog</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>
<body>
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Last Seen</th>
<th scope="col">Quota</th>
<th scope="col">Usage</th>
<th scope="col">Download</th>
<th scope="col">Upload</th>
</tr>
</thead>
<tbody>
{{ range . }}
<tr>
<td>{{ .Name }}</td>
<td>{{ .LastSeen }} - {{ .AllowanceDuration }}</td>
<td>{{ .FileQuota.CurrentUsage }}/{{ .FileQuota.AllowanceKB}} B</td>
<td>{{ .Usage.SizeB }}B - {{ .Usage.Num }} </td>
<td>{{ .Download.SizeB }}B - {{ .Download.Num }} </td>
<td>{{ .Upload.SizeB }}B - {{ .Upload.Num }} </td>
</tr>
{{ end }}
</tbody>
</table>
</body>
</html>

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>bog</title>
</head>
<body>
<script>
function upload() {
var file = document.getElementById('file').files[0];
var ajax = new XMLHttpRequest;
const reader = new FileReader();
ajax.open('POST', '', true);
ajax.overrideMimeType('text/plain; charset=x-user-defined-binary');
reader.onload = function(evt) {
ajax.send(evt.target.result);
};
reader.readAsBinaryString(file);
}
</script>
<input id="file" type="file" />
<input type="button" onClick="upload()" value="Upload">
</body>
</html>

@ -0,0 +1,8 @@
version: "1"
packages:
- path: infrastructure/sqlite/namespace
name: namespace
engine: postgresql
schema: infrastructure/sqlite/namespace/schema.sql
queries: infrastructure/sqlite/namespace/queries.sql

@ -1,94 +0,0 @@
package test
import (
"time"
"bytes"
"testing"
"caj-larsson/bog/domain"
"caj-larsson/bog/test/mock"
)
var file_ref1 = domain.FileReference { "path1", "ua1" }
var file_ref2 = domain.FileReference { "path1", "ua2" }
var file_ref3 = domain.FileReference { "path2", "ua1" }
func NewTestBogFileService() domain.BogFileService {
file_repo := mock.NewMockFileRepository()
ua_repo := mock.NewMockUserAgentRepository()
return domain.NewBogFileService(ua_repo, file_repo, 1024, time.Hour)
}
func TestFileDontExist(t *testing.T) {
s := NewTestBogFileService()
outfile, err := s.OpenOutFile(file_ref1)
if outfile != nil && err != domain.ErrNotExists {
t.Errorf("File shall not exist by default")
}
}
func TestFileIsStored(t *testing.T) {
s := NewTestBogFileService()
fakefile := bytes.NewBufferString("My bog data")
err := s.SaveFile(file_ref1, fakefile, int64(fakefile.Len()))
if err != nil {
t.Errorf("A small file should be writable %s", err)
}
largefakefile := bytes.NewBufferString("")
for largefakefile.Len() < 64000 {
_, err = largefakefile.WriteString("A very repetitive file")
}
err = s.SaveFile(file_ref3, largefakefile, int64(largefakefile.Len()))
if err != domain.ErrExceedQuota {
t.Errorf("too large files should not be excepted")
}
}
func TestFileIsReadBack(t *testing.T) {
s := NewTestBogFileService()
infile := bytes.NewBufferString("My bog data")
_ = s.SaveFile(file_ref1, infile, int64(infile.Len()))
outbogfile, _ := s.OpenOutFile(file_ref1)
outfile := bytes.NewBufferString("")
_, _ = outfile.ReadFrom(outbogfile)
if outfile.String() != "My bog data" {
t.Errorf("file corrupted")
}
}
func TestUAIsolation(t *testing.T) {
file_repo := mock.NewMockFileRepository()
ua_repo := mock.NewMockUserAgentRepository()
s := domain.NewBogFileService(ua_repo, file_repo, 1024, time.Hour)
ua1_file := bytes.NewBufferString("My bog data ua1")
ua2_file := bytes.NewBufferString("My bog data ua2")
_ = s.SaveFile(file_ref1, ua1_file, int64(ua1_file.Len()))
_ = s.SaveFile(file_ref2, ua2_file, int64(ua2_file.Len()))
outbogfile, _ := s.OpenOutFile(file_ref1)
outfile := bytes.NewBufferString("")
_, _ = outfile.ReadFrom(outbogfile)
if outfile.String() != "My bog data ua1" {
t.Errorf("file corrupted")
}
}

@ -1,58 +0,0 @@
package test
import (
"testing"
"caj-larsson/bog/domain"
)
func TestQuota(t *testing.T) {
quota := domain.FileSizeQuota { 1000, 0 }
if !quota.Allows(1000) {
t.Errorf("It should allow filling completely")
}
if quota.Allows(1001) {
t.Errorf("It should not allow filling completely")
}
}
func TestQuotaManipulation(t *testing.T) {
quota := domain.FileSizeQuota { 1000, 0 }
if quota.Add(500) != nil {
t.Errorf("It should allow adding")
}
if quota.CurrentUsage != 500 {
t.Errorf("It should add the usage correctly")
}
if quota.Add(500) != nil {
t.Errorf("It should allow adding up to the limit")
}
if quota.Add(1) != domain.ErrExceedQuota {
t.Errorf("It should not allow adding beyond limit")
}
if quota.CurrentUsage != 1000 {
t.Errorf("It should not overtaxed after failure to add")
}
if quota.Remove(1001) != domain.ErrQuotaInvalid {
t.Errorf("It should not allow reducing further than 0")
}
if quota.CurrentUsage != 1000 {
t.Errorf("It should not overtaxed after failure to remove")
}
if quota.Remove(1000) != nil {
t.Errorf("It should allow reducing to 0")
}
if quota.CurrentUsage != 0 {
t.Errorf("It should reduce accurately")
}
}

@ -1,21 +0,0 @@
package test
import (
"path"
"testing"
"caj-larsson/bog/domain"
"caj-larsson/bog/integration"
"caj-larsson/bog/test/mock"
)
func TestFsFileRepo(t *testing.T) {
var fac = func()domain.FileDataRepository {
r := t.TempDir()
d := path.Join(r, "fs")
repo := integration.FileSystemBogRepository { d }
return &repo
}
mock.BogFileRepositoryContract(fac, t)
}

@ -1,90 +0,0 @@
package mock
import (
"time"
"path"
"os"
// "io"
"github.com/spf13/afero"
"caj-larsson/bog/domain"
)
type MockBogFile struct {
filename string
file afero.File
}
func (f MockBogFile) Path() string {
return f.filename
}
func (f MockBogFile) Size() int64 {
stat, _ := f.file.Stat()
return int64(stat.Size())
}
func (f MockBogFile) Read(p []byte) (int, error) {
return f.file.Read(p)
}
func (f MockBogFile) Write(p []byte) (int, error) {
return f.file.Write(p)
}
func (f MockBogFile) Close() error {
return f.file.Close()
}
func (f MockBogFile) Seek(offset int64, whence int) (int64, error) {
return f.file.Seek(offset, whence)
}
func (f MockBogFile) Modified() time.Time {
stat, _ := f.file.Stat()
return stat.ModTime()
}
// The actual repository
type MockFileRepository struct {
fs afero.Fs
}
func NewMockFileRepository() domain.FileDataRepository {
return MockFileRepository { afero.NewMemMapFs() }
}
func (r MockFileRepository) Create(filename string, user_agent_label string) (domain.BogInFile, error) {
abs_path := path.Join( filename, user_agent_label)
dir := path.Dir(abs_path)
r.fs.MkdirAll(dir, 0750)
file, err := r.fs.OpenFile(abs_path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
bf := MockBogFile {filename, file}
return bf, nil
}
func (r MockFileRepository) Open(filename string, user_agent_label string) (domain.BogOutFile, error) {
abs_path := path.Join(filename, user_agent_label)
dir := path.Dir(abs_path)
r.fs.MkdirAll(dir, 0750)
file, err := r.fs.OpenFile(abs_path, os.O_RDONLY, 0644)
if err != nil {
return nil, domain.ErrNotExists
}
bf := MockBogFile {filename, file}
return bf, nil
}
func (r MockFileRepository) Delete(filename string, user_agent_label string) {
abs_path := path.Join(filename, user_agent_label)
r.fs.Remove(abs_path)
}

@ -1,57 +0,0 @@
package mock
import (
"testing"
"caj-larsson/bog/domain"
)
func BogFileRepositoryContract(fac func() domain.FileDataRepository, t *testing.T) {
basicFileOperationContract(fac, t)
}
func basicFileOperationContract(fac func() domain.FileDataRepository, t *testing.T) {
repo := fac()
not_file, err := repo.Open("doesnot", "exist")
if err != domain.ErrNotExists || not_file != nil{
t.Errorf("Must raise not exists and file must not open")
}
new_file, err := repo.Create("newfile.new", "ua1")
if err != nil || new_file == nil {
t.Errorf("Create ")
}
var testdata = "testingdata"
new_file.Write([]byte(testdata))
new_file.Close()
reopened_file, err := repo.Open("newfile.new", "ua1")
if err != nil || reopened_file == nil{
t.Errorf("Must open existing files")
}
readback := make([]byte, 128)
size, err := reopened_file.Read(readback)
if string(readback[0:size]) != testdata {
t.Errorf("Must contain previously stored data '%s' '%s'", string(readback), testdata)
}
reopened_file.Close()
repo.Delete("newfile.new", "ua1")
deleted_file, err := repo.Open("newfile.new", "ua1")
if err != domain.ErrNotExists || deleted_file != nil{
t.Errorf("Musn't open deleted files")
}
}

@ -1,11 +0,0 @@
package mock
import (
"testing"
//"caj-larsson/bog/domain"
)
func TestMockFileRepo(t *testing.T) {
BogFileRepositoryContract(NewMockFileRepository, t)
}

@ -1,66 +0,0 @@
package mock
import (
// "time"
"caj-larsson/bog/domain"
)
type MockUserAgentRepository struct {
IdIdx map[int64]*domain.UserAgent
NameIdx map[string]*domain.UserAgent
NextId int64
}
func NewMockUserAgentRepository() *MockUserAgentRepository {
r := new(MockUserAgentRepository)
r.NextId = 0
r.IdIdx = make(map[int64]*domain.UserAgent)
r.NameIdx = make(map[string]*domain.UserAgent)
return r
}
func (r *MockUserAgentRepository) Create(useragent domain.UserAgent) (*domain.UserAgent, error) {
r.NextId += 1
useragent.ID = r.NextId
r.IdIdx[useragent.ID] = &useragent
r.NameIdx[useragent.Name] = &useragent
return &useragent, nil
}
func (r *MockUserAgentRepository) All() ([]domain.UserAgent, error) {
v := make([]domain.UserAgent, 0, len(r.IdIdx))
for _, value := range r.IdIdx {
v = append(v, *value)
}
return v, nil
}
func (r *MockUserAgentRepository) GetByName(name string) (*domain.UserAgent, error) {
useragent, exists := r.NameIdx[name]
if exists {
return useragent, nil
}
return nil, domain.ErrNotExists
}
func (r *MockUserAgentRepository) Update(id int64, useragent domain.UserAgent) (*domain.UserAgent, error) {
original := *r.IdIdx[id]
useragent.ID = id
r.IdIdx[id] = &useragent
r.NameIdx[original.Name] = &useragent
return &useragent, nil
}
func (r *MockUserAgentRepository) Delete(id int64) error {
original := *r.IdIdx[id]
delete(r.NameIdx, original.Name)
delete(r.IdIdx, original.ID)
return nil
}

@ -1,52 +0,0 @@
package mock
import (
"testing"
"time"
"caj-larsson/bog/domain"
)
func TestMockUserAgentRepo(t *testing.T) {
r := NewMockUserAgentRepository()
all, err := r.All()
if len(all) != 0 && err != nil {
t.Errorf("New repo should be empty")
}
ua := domain.UserAgent {23, "n1", time.Now(), time.Duration(time.Hour * 3), domain.FileSizeQuota {1000, 0} }
ua1, _ := r.Create(ua)
ua.Name = "n2"
ua2, _ := r.Create(ua)
if ua1 == ua2 {
t.Errorf("Must create unique items")
}
all, err = r.All()
if len(all) != 2 && err != nil {
t.Errorf("After adding there should be two Useragent")
}
if ua.ID != 23 {
t.Errorf("It does not change the original UserAgent")
}
ua3, _ := r.GetByName("n2")
if ua3 != ua2 {
t.Errorf("It the correct ua is acquired")
}
if r.Delete(ua2.ID) != nil {
t.Errorf("Must delete without error")
}
all, err = r.All()
if len(all) != 1 && err != nil {
t.Errorf("After deleting one there should be one UA ")
}
}

@ -0,0 +1,41 @@
package util
type Event struct {
eventName string
payload interface{}
}
func NewEvent(eventName string, payload interface{}) *Event {
return &Event{
eventName,
payload,
}
}
func (e *Event) EventName() string {
return e.eventName
}
func (e *Event) Payload() interface{} {
return e.payload
}
type EventHandler func(payload interface{})
type EventBus struct {
handlers map[string][]EventHandler
}
func NewEventBus() *EventBus {
return &EventBus{make(map[string][]EventHandler)}
}
func (eb *EventBus) Register(eventName string, handler EventHandler) {
eb.handlers[eventName] = append(eb.handlers[eventName], handler)
}
func (eb *EventBus) Handle(e Event) {
for _, handler := range eb.handlers[e.EventName()] {
handler(e.Payload())
}
}

@ -0,0 +1,23 @@
package util
import (
"github.com/matryer/is"
"testing"
)
func (eb *EventBus) Handled(e Event) bool {
// TODO: figure out how to verify the event signature here.
handlers, exists := eb.handlers[e.EventName()]
if !exists {
return false
}
return len(handlers) > 0
}
func AcceptsMessage(t *testing.T, eb *EventBus, es []Event) {
is := is.New(t)
for _, e := range es {
is.True(eb.Handled(e))
}
}

@ -0,0 +1,13 @@
package util
type Logger interface {
Debug(format string, a ...interface{})
Info(format string, a ...interface{})
Warn(format string, a ...interface{})
}
type TestLogger struct{}
func (t TestLogger) Debug(format string, a ...interface{}) {}
func (t TestLogger) Info(format string, a ...interface{}) {}
func (t TestLogger) Warn(format string, a ...interface{}) {}
Loading…
Cancel
Save