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 # 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 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 you know your lake is already full with a bunch of stuff that should
have been deleted long ago? 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 without authentication the old school way: encryption. Pass a password
when you create your data and if you pass the same when you retrieve when you create your data and if you pass the same when you retrieve
it, you get the same bits back. 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 ( import (
"time"
"errors" "errors"
"time"
) )
type UserAgent struct { type Namespace struct {
ID int64 ID int64
Name string Name string
LastSeen time.Time LastSeen time.Time
AllowanceDuration time.Duration AllowanceDuration time.Duration
FileQuota FileSizeQuota FileQuota FileSizeQuota
} Download FileStat
Upload FileStat
type BogFile struct {
UserAgentId int64
Path string
Size int64
CreatedAt time.Time
} }
var ( 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 port = 8002
host = "127.0.0.1" host = "127.0.0.1"
[admin]
port = 8003
host = "127.0.0.1"
[file] [file]
path = "/tmp/datta2" 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 ( require (
github.com/BurntSushi/toml v1.1.0 // indirect github.com/BurntSushi/toml v1.1.0 // indirect
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 // 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/mattn/go-sqlite3 v1.14.12 // indirect
github.com/spf13/afero v1.8.2 // indirect github.com/spf13/afero v1.8.2 // indirect
golang.org/x/text v0.3.4 // 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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.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 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 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= 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 package main
import ( import (
"caj-larsson/bog/application" "caj-larsson/bog/server"
"io/ioutil" "io/ioutil"
"fmt"
) )
func main() { func main() {
@ -13,13 +12,10 @@ func main() {
panic(err) panic(err)
} }
config, err := application.ConfigFromToml(string(content)) config, err := server.ConfigFromToml(string(content))
if err != nil { if err != nil {
panic(err) panic(err)
} }
bog := server.New(config)
fmt.Printf("%+v\n", config)
bog := application.New(config)
bog.Run() 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 ( import (
"fmt" "fmt"
"time"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/c2h5oh/datasize" "github.com/c2h5oh/datasize"
"time"
) )
func (qc QuotaConfig) ParsedSizeBytes() int64 { func (qc QuotaConfig) ParsedSizeBytes() int64 {
var v datasize.ByteSize var v datasize.ByteSize
@ -38,40 +36,41 @@ func (qc QuotaConfig) ParsedDuration() time.Duration {
} }
type QuotaConfig struct { type QuotaConfig struct {
DefaultSize string `toml:"default_size"` DefaultSize string `toml:"default_size"`
DefaultDuration string `toml:"default_duration"` DefaultDuration string `toml:"default_duration"`
} }
type ServerConfig struct { type ServerConfig struct {
Port int64 Port int64
Host string Host string
} }
type FileConfig struct { type FileConfig struct {
Path string Path string
} }
type DatabaseConfig struct { type DatabaseConfig struct {
Backend string Backend string
Connection string Connection string
} }
type LoggingConfig struct {
Level string
}
type Configuration struct { type Configuration struct {
Server ServerConfig Server ServerConfig
File FileConfig Admin ServerConfig
File FileConfig
Database DatabaseConfig Database DatabaseConfig
Quota QuotaConfig Quota QuotaConfig
Logging LoggingConfig
} }
func (c *Configuration) bindAddress() string { func (c ServerConfig) bindAddress() string {
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port) return fmt.Sprintf("%s:%d", c.Host, c.Port)
} }
func ConfigFromToml(toml_data string) (*Configuration, error) { func ConfigFromToml(toml_data string) (*Configuration, error) {
var config Configuration 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