Compare commits

..

26 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

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

1
.gitignore vendored

@ -1,2 +1,3 @@
sql.db sql.db
bog 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?

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

@ -0,0 +1 @@
package dataswamp

@ -11,6 +11,8 @@ type Namespace struct {
LastSeen time.Time LastSeen time.Time
AllowanceDuration time.Duration AllowanceDuration time.Duration
FileQuota FileSizeQuota FileQuota FileSizeQuota
Download FileStat
Upload FileStat
} }
var ( var (

@ -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)
}

@ -5,30 +5,36 @@ type FileSizeQuota struct {
CurrentUsage int64 CurrentUsage int64
} }
func (f *FileSizeQuota) Allows(size int64) bool { func (f FileSizeQuota) Allows(size int64) bool {
return f.Remaining() >= size return f.Remaining() >= size
} }
func (f *FileSizeQuota) Remaining() int64 { func (f FileSizeQuota) Remaining() int64 {
return f.AllowanceKB - f.CurrentUsage return f.AllowanceKB - f.CurrentUsage
} }
func (f *FileSizeQuota) Add(size int64) error { func (f FileSizeQuota) Add(size int64) FileSizeQuota {
if !f.Allows(size) { return FileSizeQuota{
return ErrExceedQuota f.AllowanceKB,
f.CurrentUsage + size,
} }
f.CurrentUsage += size
return nil
} }
func (f *FileSizeQuota) Remove(size int64) error { func (f FileSizeQuota) Remove(size int64) FileSizeQuota {
if size > f.CurrentUsage { return FileSizeQuota{
return ErrQuotaInvalid f.AllowanceKB,
f.CurrentUsage - size,
} }
}
f.CurrentUsage -= size type FileStat struct {
Num int64
SizeB int64
}
return nil func (s FileStat) Add(size int64) FileStat {
return FileStat{
s.Num + 1,
s.SizeB + size,
}
} }

@ -16,22 +16,8 @@ func TestQuota(t *testing.T) {
func TestQuotaManipulation(t *testing.T) { func TestQuotaManipulation(t *testing.T) {
is := is.New(t) is := is.New(t)
quota := FileSizeQuota{1000, 0} quota := FileSizeQuota{1000, 0}
quota = quota.Add(500)
is.NoErr(quota.Add(500))
is.Equal(quota.CurrentUsage, int64(500)) is.Equal(quota.CurrentUsage, int64(500))
quota = quota.Remove(1000)
is.NoErr(quota.Add(500)) is.Equal(quota.CurrentUsage, int64(-500))
is.Equal(quota.Add(1), ErrExceedQuota)
is.Equal(quota.CurrentUsage, int64(1000))
is.Equal(quota.Remove(1001), ErrQuotaInvalid)
is.Equal(quota.CurrentUsage, int64(1000))
is.NoErr(quota.Remove(1000))
is.Equal(quota.CurrentUsage, int64(0))
} }

@ -1,138 +0,0 @@
package dataswamp
import (
"caj-larsson/bog/dataswamp/namespace"
"caj-larsson/bog/dataswamp/swampfile"
"io"
"strconv"
"time"
// "errors"
// "fmt"
)
type SwampFileService struct {
namespace_repo namespace.Repository
swamp_file_repo swampfile.Repository
default_allowance_bytes int64
default_allowance_duration time.Duration
}
func NewSwampFileService(
namespace_repo namespace.Repository,
swamp_file_repo swampfile.Repository,
da_bytes int64,
da_duration time.Duration,
) SwampFileService {
return SwampFileService{namespace_repo, swamp_file_repo, da_bytes, da_duration}
}
func (s SwampFileService) getOrCreateNs(namespace_in string) *namespace.Namespace {
ns, err := s.namespace_repo.GetByName(namespace_in)
if err == namespace.ErrNotExists {
new_ns := namespace.Namespace{
0,
namespace_in,
time.Now(),
s.default_allowance_duration,
namespace.FileSizeQuota{s.default_allowance_bytes, 0},
}
created_ns, err := s.namespace_repo.Create(new_ns)
if err != nil {
panic(err)
}
return created_ns
}
if err != nil {
panic(err)
}
return ns
}
func (s SwampFileService) SaveFile(ref swampfile.FileReference, src io.Reader, size int64) error {
ns := s.getOrCreateNs(ref.UserAgent)
err := ref.Clean(true)
if err != nil {
return err
}
if !ns.FileQuota.Allows(size) {
return namespace.ErrExceedQuota
}
f, err := s.swamp_file_repo.Create(ref.Path, strconv.FormatInt(ns.ID, 10))
if err != nil {
return err
}
written, err := io.CopyN(f, src, size)
if written < size {
s.swamp_file_repo.Delete(ref.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(ref.Path, strconv.FormatInt(ns.ID, 10))
return swampfile.ErrContentSizeExceeded
}
f.Close()
ns.FileQuota.Add(size)
s.namespace_repo.Update(ns.ID, *ns)
return nil
}
func (s SwampFileService) OpenOutFile(ref swampfile.FileReference) (swampfile.SwampOutFile, error) {
ns := s.getOrCreateNs(ref.UserAgent)
err := ref.Clean(true)
if err != nil {
return nil, err
}
f, err := s.swamp_file_repo.Open(ref.Path, strconv.FormatInt(ns.ID, 10))
if err != nil {
return nil, err
}
return f, nil
}
func (s SwampFileService) CleanUpExpiredFiles() error {
nss, err := s.namespace_repo.All()
if err != nil {
return err
}
for _, ns := range nss {
expiry := time.Now().Add(-ns.AllowanceDuration)
dfs, err := s.swamp_file_repo.DeleteOlderThan(strconv.FormatInt(ns.ID, 10), expiry)
for _, df := range dfs {
ns.FileQuota.Remove(df.Size)
}
if err != nil {
panic(err)
}
s.namespace_repo.Update(ns.ID, ns)
}
return nil
}

@ -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
}

@ -2,29 +2,47 @@ package dataswamp
import ( import (
"bytes" "bytes"
"caj-larsson/bog/dataswamp/namespace"
"caj-larsson/bog/dataswamp/swampfile" "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/matryer/is"
"github.com/spf13/afero" "github.com/spf13/afero"
"testing" "testing"
"time" "time"
// "caj-larsson/bog/dataswamp/namespace"
m_namespace "caj-larsson/bog/infrastructure/memory/namespace"
m_swampfile "caj-larsson/bog/infrastructure/memory/swampfile"
) )
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_ref1 = swampfile.FileReference{"/path1", "ns1"}
var file_ref2 = swampfile.FileReference{"/path1", "ns2"} var file_ref2 = swampfile.FileReference{"/path1", "ns2"}
var file_ref3 = swampfile.FileReference{"/path2", "ns1"} var file_ref3 = swampfile.FileReference{"/path2", "ns1"}
func NewTestSwampFileService() SwampFileService { func NewTestDataSwampService() DataSwampService {
file_repo := m_swampfile.NewRepository() file_repo := m_swampfile.NewRepository()
ns_repo := m_namespace.NewRepository() ns_repo := m_namespace.NewRepository()
return NewSwampFileService(ns_repo, file_repo, 1024, time.Hour)
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) { func TestFileDontExist(t *testing.T) {
is := is.New(t) is := is.New(t)
s := NewTestSwampFileService() s := NewTestDataSwampService()
outfile, err := s.OpenOutFile(file_ref1) outfile, err := s.OpenOutFile(file_ref1)
is.True(err == swampfile.ErrNotExists) is.True(err == swampfile.ErrNotExists)
@ -33,7 +51,7 @@ func TestFileDontExist(t *testing.T) {
func TestFileIsStored(t *testing.T) { func TestFileIsStored(t *testing.T) {
is := is.New(t) is := is.New(t)
s := NewTestSwampFileService() s := NewTestDataSwampService()
fakefile := bytes.NewBufferString("My bog data") fakefile := bytes.NewBufferString("My bog data")
@ -54,7 +72,7 @@ func TestFileIsStored(t *testing.T) {
func TestFileIsReadBack(t *testing.T) { func TestFileIsReadBack(t *testing.T) {
is := is.New(t) is := is.New(t)
s := NewTestSwampFileService() s := NewTestDataSwampService()
infile := bytes.NewBufferString("My bog data") infile := bytes.NewBufferString("My bog data")
@ -70,7 +88,7 @@ func TestFileIsReadBack(t *testing.T) {
func TestNSIsolation(t *testing.T) { func TestNSIsolation(t *testing.T) {
is := is.New(t) is := is.New(t)
s := NewTestSwampFileService() s := NewTestDataSwampService()
ns1_file := bytes.NewBufferString("My bog data ns1") ns1_file := bytes.NewBufferString("My bog data ns1")
ns2_file := bytes.NewBufferString("My bog data ns2") ns2_file := bytes.NewBufferString("My bog data ns2")
@ -88,7 +106,7 @@ func TestNSIsolation(t *testing.T) {
func TestPathStrictMode(t *testing.T) { func TestPathStrictMode(t *testing.T) {
is := is.New(t) is := is.New(t)
s := NewTestSwampFileService() s := NewTestDataSwampService()
ns_file := bytes.NewBufferString("My bog data ns1") ns_file := bytes.NewBufferString("My bog data ns1")
@ -108,7 +126,7 @@ func TestPathStrictMode(t *testing.T) {
func TestQuotaWithContenSizeLieOver(t *testing.T) { func TestQuotaWithContenSizeLieOver(t *testing.T) {
is := is.New(t) is := is.New(t)
s := NewTestSwampFileService() s := NewTestDataSwampService()
largefakefile := bytes.NewBufferString("") largefakefile := bytes.NewBufferString("")
@ -123,7 +141,7 @@ func TestQuotaWithContenSizeLieOver(t *testing.T) {
func TestQuotaWithContenSizeLieUnder(t *testing.T) { func TestQuotaWithContenSizeLieUnder(t *testing.T) {
is := is.New(t) is := is.New(t)
s := NewTestSwampFileService() s := NewTestDataSwampService()
largefakefile := bytes.NewBufferString("small") largefakefile := bytes.NewBufferString("small")
@ -138,22 +156,32 @@ func TestCleanUpExpired(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
file_repo := m_swampfile.Repository{fs} file_repo := m_swampfile.Repository{fs}
ns_repo := m_namespace.NewRepository() ns_repo := m_namespace.NewRepository()
s := NewSwampFileService(ns_repo, file_repo, 1024, time.Hour) logger := TestLogger{}
ns_svc := namespace.NewNamespaceService(
fakefile := bytes.NewBufferString("My bog data") 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())) err := s.SaveFile(file_ref1, fakefile, int64(fakefile.Len()))
is.NoErr(err) is.NoErr(err)
fakefile = bytes.NewBufferString("My bog data") fakefile = bytes.NewBufferString("My bog")
err = s.SaveFile(file_ref3, fakefile, int64(fakefile.Len())) err = s.SaveFile(file_ref3, fakefile, int64(fakefile.Len()))
is.NoErr(err) is.NoErr(err)
err = fs.Chtimes("1/path1", time.Now(), time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) err = fs.Chtimes("1/path1", time.Now(), time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))
is.NoErr(err)
is.NoErr(s.CleanUpExpiredFiles()) is.NoErr(s.CleanUpExpiredFiles())
ns, err := ns_repo.GetByName("ns1") ns, err := ns_repo.GetByName("ns1")
fmt.Printf("file final usage %v\n", ns.FileQuota)
is.NoErr(err) is.NoErr(err)
fmt.Printf("file\n")
is.Equal(ns.FileQuota.CurrentUsage, int64(len("My bog data"))) is.Equal(ns.FileQuota.CurrentUsage, int64(len("My bog")))
} }

@ -33,6 +33,7 @@ func basicFileOperationContract(fac func() Repository, t *testing.T) {
is.NoErr(err) is.NoErr(err)
is.True(reopened_file != nil) is.True(reopened_file != nil)
is.Equal(reopened_file.Size(), int64(len(testdata)))
readback := make([]byte, 128) readback := make([]byte, 128)

@ -36,14 +36,17 @@ type DeletedSwampFile struct {
Size int64 Size int64
} }
func (fr *FileReference) Clean(strict bool) error { func (fr *FileReference) Clean(strict bool) (*FileReference, error) {
c := filepath.FromSlash(path.Clean("/" + strings.Trim(fr.Path, "/"))) c := filepath.FromSlash(path.Clean("/" + strings.Trim(fr.Path, "/")))
if c != fr.Path && strict { if c != fr.Path && strict {
return ErrUnacceptablePath return nil, ErrUnacceptablePath
} }
fr.Path = c n := FileReference{
c,
fr.UserAgent,
}
return nil return &n, nil
} }

@ -25,9 +25,9 @@ func TestSwampPathNotStrict(t *testing.T) {
"ns", "ns",
} }
err := rf.Clean(false) rc, err := rf.Clean(false)
is.NoErr(err) is.NoErr(err)
is.Equal(rf.Path, tc.clean) is.Equal(rc.Path, tc.clean)
} }
} }
@ -39,6 +39,6 @@ func TestSwampPathStrict(t *testing.T) {
"ns", "ns",
} }
err := rf.Clean(true) _, err := rf.Clean(true)
is.Equal(err, ErrUnacceptablePath) 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

@ -48,6 +48,13 @@ func (f FileSystemSwampFileData) Modified() time.Time {
return stat.ModTime() return stat.ModTime()
} }
func (f FileSystemSwampFileData) Truncate() {
err := f.file.Truncate(0)
if err != nil {
panic(err)
}
}
type Repository struct { type Repository struct {
Root string Root string
} }
@ -98,6 +105,7 @@ func (f Repository) Create(filename string, namespace_ns string) (swampfile.Swam
if err != nil { if err != nil {
panic(err) panic(err)
} }
file.Truncate(0)
bfd := FileSystemSwampFileData{filename, stat_info.Size(), stat_info.ModTime(), file} bfd := FileSystemSwampFileData{filename, stat_info.Size(), stat_info.ModTime(), file}
@ -115,7 +123,13 @@ func (f Repository) Open(filename string, namespace_ns string) (swampfile.SwampO
return nil, swampfile.ErrNotExists return nil, swampfile.ErrNotExists
} }
bfd := FileSystemSwampFileData{filename, 0, time.Now(), file} stat, err := file.Stat()
if err != nil {
return nil, err
}
bfd := FileSystemSwampFileData{filename, stat.Size(), time.Now(), file}
return bfd, nil return bfd, nil
} }

@ -11,7 +11,7 @@ type Repository struct {
NextId int64 NextId int64
} }
func NewRepository() *Repository { func NewRepository() namespace.Repository {
r := new(Repository) r := new(Repository)
r.NextId = 0 r.NextId = 0
r.IdIdx = make(map[int64]*namespace.Namespace) r.IdIdx = make(map[int64]*namespace.Namespace)

@ -2,42 +2,9 @@ package namespace
import ( import (
"caj-larsson/bog/dataswamp/namespace" "caj-larsson/bog/dataswamp/namespace"
"github.com/matryer/is"
"testing" "testing"
"time"
) )
func TestUserAgentRepo(t *testing.T) { func TestFileRepo(t *testing.T) {
is := is.New(t) namespace.RepositoryContract(NewRepository, t)
r := NewRepository()
all, err := r.All()
is.NoErr(err)
is.Equal(len(all), 0)
ns := namespace.Namespace{23, "n1", time.Now(), time.Duration(time.Hour * 3), namespace.FileSizeQuota{1000, 0}}
ns1, _ := r.Create(ns)
ns.Name = "n2"
ns2, _ := r.Create(ns)
is.True(ns1 != ns2)
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)
} }

@ -44,6 +44,13 @@ func (f SwampFile) Modified() time.Time {
return stat.ModTime() return stat.ModTime()
} }
func (f SwampFile) Truncate() {
err := f.file.Truncate(0)
if err != nil {
panic(err)
}
}
// The actual repository // The actual repository
type Repository struct { type Repository struct {
Fs afero.Fs Fs afero.Fs

@ -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
}

@ -2,153 +2,188 @@ package namespace
import ( import (
"caj-larsson/bog/dataswamp/namespace" "caj-larsson/bog/dataswamp/namespace"
"context"
"database/sql" "database/sql"
"errors"
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
"time"
) )
var _ = sqlite3.ErrError
type Repository struct { type Repository struct {
db *sql.DB db *sql.DB
} }
func NewRepository(filename string) *Repository { 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) db, err := sql.Open("sqlite3", filename)
if err != nil { if err != nil {
panic(err) panic(err)
} }
repo := Repository{ repo := Repository{
db: db, db: db,
} }
repo.migrate() repo.migrate()
return &repo return &repo
} }
func (r Repository) migrate() error { func (q *Queries) createFileStats(ctx context.Context, fstat namespace.FileStat) (int64, error) {
query := ` return q.CreateFileStats(ctx, CreateFileStatsParams{fstat.Num, fstat.SizeB})
CREATE TABLE IF NOT EXISTS namespace(
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 *Repository) Create(ns namespace.Namespace) (*namespace.Namespace, error) { func (r *Repository) Create(ns namespace.Namespace) (*namespace.Namespace, error) {
var record, err = fromEntity(ns) ctx := context.Background()
if err != nil { q := New(r.db)
dl_id, err := q.createFileStats(ctx, ns.Download)
if err != nil {
return nil, err
} }
res, err := r.db.Exec( ul_id, err := q.createFileStats(ctx, ns.Upload)
"INSERT INTO namespace(name, lastseen, allowance_time, quota_kb, quota_usage_kb) values(?,?,?,?,?)",
record.Name, record.LastSeen, record.AllowanceSeconds, record.QuotaKB, record.QuotaUsedKB,
)
if err != nil { if err != nil {
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) {
if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintUnique) {
return nil, namespace.ErrDuplicate
}
}
return nil, err return nil, err
} }
id, err := res.LastInsertId() 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 { if err != nil {
return nil, err return nil, err
} }
ns.ID = id
ns.ID = id
return &ns, nil return &ns, nil
} }
func (r Repository) All() ([]namespace.Namespace, error) { func (r *Repository) All() ([]namespace.Namespace, error) {
rows, err := r.db.Query("SELECT * FROM namespace") ctx := context.Background()
q := New(r.db)
rows, err := q.AllNamespaces(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close()
var all []namespace.Namespace var all []namespace.Namespace
for rows.Next() { for _, row := range rows {
var record NamespaceRecord ns := namespace.Namespace{
if err := rows.Scan(&record.ID, &record.Name, &record.LastSeen, &record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB); err != nil { row.ID,
return nil, err 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},
} }
var ns, err = record.toEntity()
if err != nil { all = append(all, ns)
return nil, err
}
all = append(all, *ns)
} }
return all, nil return all, nil
} }
func (r Repository) GetByName(name string) (*namespace.Namespace, error) { func (r *Repository) GetByName(name string) (*namespace.Namespace, error) {
row := r.db.QueryRow("SELECT id, name, lastseen, allowance_time, quota_kb, quota_usage_kb FROM namespace WHERE name = ?", name) ctx := context.Background()
q := New(r.db)
row, err := q.GetNamespaceByName(ctx, name)
var record NamespaceRecord if err != nil {
if err := row.Scan(&record.ID, &record.Name, &record.LastSeen, &record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB); err != nil { return nil, namespace.ErrNotExists
if errors.Is(err, sql.ErrNoRows) {
return nil, namespace.ErrNotExists
}
return nil, err
} }
var ns, err = record.toEntity() ns := namespace.Namespace{
row.ID,
if err != nil { row.Name,
return nil, err 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
return &ns, nil
} }
func (r Repository) Update(id int64, updated namespace.Namespace) (*namespace.Namespace, error) { func (q *Queries) updateFileStat(ctx context.Context, id int64, fstat namespace.FileStat) error {
if id == 0 { return q.UpdateFileStat(ctx, UpdateFileStatParams{fstat.Num, fstat.SizeB, id})
return nil, errors.New("invalid updated ID") }
}
var record, err = fromEntity(updated) func (r *Repository) Update(id int64, ns namespace.Namespace) (*namespace.Namespace, error) {
ctx := context.Background()
q := New(r.db)
res, err := r.db.Exec("UPDATE namespace SET name = ?, lastseen = ?, allowance_time = ?, quota_kb = ?, quota_usage_kb = ? WHERE id = ?", ids, err := q.UpdateNamespace(ctx, UpdateNamespaceParams{
record.Name, record.LastSeen, record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB, id) 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 { if err != nil {
return nil, err return nil, err
} }
rowsAffected, err := res.RowsAffected() err = q.updateFileStat(ctx, ids.UploadID, ns.Upload)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rowsAffected == 0 { return &ns, nil
return nil, namespace.ErrUpdateFailed
}
return &updated, nil
} }
func (r Repository) Delete(id int64) error { func (r *Repository) Delete(id int64) error {
res, err := r.db.Exec("DELETE FROM namespace WHERE id = ?", id) ctx := context.Background()
if err != nil { q := New(r.db)
return err
}
rowsAffected, err := res.RowsAffected() err := q.DeleteNameSpace(ctx, id)
if err != nil { if err != nil {
return err
}
if rowsAffected == 0 {
return namespace.ErrDeleteFailed return namespace.ErrDeleteFailed
} }
return err 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()
}

@ -2,7 +2,6 @@ package main
import ( import (
"caj-larsson/bog/server" "caj-larsson/bog/server"
"fmt"
"io/ioutil" "io/ioutil"
) )
@ -17,9 +16,6 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Printf("Dataswamp running")
bog := server.New(config) bog := server.New(config)
bog.Run() bog.Run()
} }

@ -1,15 +1,17 @@
package server package server
import ( import (
"fmt"
"net/http"
"strconv"
"time"
"caj-larsson/bog/dataswamp" "caj-larsson/bog/dataswamp"
"caj-larsson/bog/dataswamp/namespace" "caj-larsson/bog/dataswamp/namespace"
"caj-larsson/bog/dataswamp/swampfile" "caj-larsson/bog/dataswamp/swampfile"
fs_swampfile "caj-larsson/bog/infrastructure/fs/swampfile" fs_swampfile "caj-larsson/bog/infrastructure/fs/swampfile"
sql_namespace "caj-larsson/bog/infrastructure/sqlite/namespace" 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 { type Router interface {
@ -19,8 +21,11 @@ type Router interface {
type Bog struct { type Bog struct {
router Router router Router
file_service dataswamp.SwampFileService adminRouter Router
file_service dataswamp.DataSwampService
address string address string
adminAddress string
logger util.Logger
} }
func buildFileDataRepository(config FileConfig) swampfile.Repository { func buildFileDataRepository(config FileConfig) swampfile.Repository {
@ -39,26 +44,39 @@ func buildNamespaceRepository(config DatabaseConfig) namespace.Repository {
} }
func (b *Bog) fileHandler(w http.ResponseWriter, r *http.Request) { 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 == "/" { if r.URL.Path == "/" {
fmt.Fprintf(w, "Hi") http.NotFound(w, r)
return return
} }
ref := swampfile.FileReference{r.URL.Path, r.Header["User-Agent"][0]}
switch r.Method { switch r.Method {
case "GET": case "GET":
swamp_file, err := b.file_service.OpenOutFile(ref) swamp_file, err := b.file_service.OpenOutFile(ref)
if err == swampfile.ErrNotExists { if err == swampfile.ErrNotExists {
http.NotFound(w, r) 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 return
} }
if err != nil { if err != nil {
panic(err) 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) http.ServeContent(w, r, swamp_file.Path(), swamp_file.Modified(), swamp_file)
case "POST": case "POST":
fallthrough fallthrough
@ -71,6 +89,7 @@ func (b *Bog) fileHandler(w http.ResponseWriter, r *http.Request) {
return 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) err = b.file_service.SaveFile(ref, r.Body, size)
if err != nil { if err != nil {
@ -80,11 +99,27 @@ func (b *Bog) fileHandler(w http.ResponseWriter, r *http.Request) {
return 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() { func (b *Bog) routes() {
b.router.HandleFunc("/", b.fileHandler) b.router.HandleFunc("/", b.fileHandler)
// b.adminRouter.HandleFunc("/", b.dashboardHandler)
} }
func (b *Bog) cleanNamespaces(){ func (b *Bog) cleanNamespaces() {
for true { for true {
b.file_service.CleanUpExpiredFiles() b.file_service.CleanUpExpiredFiles()
time.Sleep(time.Minute * 10) time.Sleep(time.Minute * 10)
@ -93,21 +128,36 @@ func (b *Bog) cleanNamespaces(){
func New(config *Configuration) *Bog { func New(config *Configuration) *Bog {
b := new(Bog) b := new(Bog)
b.address = config.bindAddress() b.address = config.Server.bindAddress()
b.adminAddress = config.Admin.bindAddress()
fsSwampRepo := buildFileDataRepository(config.File) fsSwampRepo := buildFileDataRepository(config.File)
nsRepo := buildNamespaceRepository(config.Database) nsRepo := buildNamespaceRepository(config.Database)
b.file_service = dataswamp.NewSwampFileService( logger := ServerLogger{Debug}
nsRepo, fsSwampRepo, config.Quota.ParsedSizeBytes(), config.Quota.ParsedDuration(), 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.router = new(http.ServeMux)
b.adminRouter = new(http.ServeMux)
b.routes() b.routes()
return b return b
} }
func (b *Bog) Run() { func (b *Bog) Run() {
b.logger.Info("Starting bog on address: %s", b.address)
go b.cleanNamespaces() go b.cleanNamespaces()
go func() { http.ListenAndServe(b.adminAddress, b.adminRouter) }()
http.ListenAndServe(b.address, b.router) http.ListenAndServe(b.address, b.router)
} }

@ -4,8 +4,10 @@ import (
"testing" "testing"
// "fmt" // "fmt"
"caj-larsson/bog/dataswamp" "caj-larsson/bog/dataswamp"
"caj-larsson/bog/infrastructure/memory/namespace" "caj-larsson/bog/dataswamp/namespace"
ns "caj-larsson/bog/infrastructure/memory/namespace"
"caj-larsson/bog/infrastructure/memory/swampfile" "caj-larsson/bog/infrastructure/memory/swampfile"
"caj-larsson/bog/infrastructure/system_time"
"github.com/matryer/is" "github.com/matryer/is"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -13,19 +15,36 @@ import (
"time" "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) { func TestApplication(t *testing.T) {
is := is.New(t) is := is.New(t)
file_service := dataswamp.NewSwampFileService( logger := TestLogger{}
namespace.NewRepository(), nsRepo := ns.NewRepository()
swampfile.NewRepository(),
1000, ns_svc := namespace.NewNamespaceService(
nsRepo,
logger,
system_time.Clock{},
time.Hour, time.Hour,
1000,
)
file_service := dataswamp.NewDataSwampService(
*ns_svc,
swampfile.NewRepository(),
logger,
) )
bog := Bog{ bog := Bog{
router: new(http.ServeMux), router: new(http.ServeMux),
file_service: file_service, file_service: *file_service,
address: "fake", address: "fake",
logger: logger,
} }
bog.routes() bog.routes()
req := httptest.NewRequest("POST", "/apath", strings.NewReader("testdata")) req := httptest.NewRequest("POST", "/apath", strings.NewReader("testdata"))

@ -54,15 +54,21 @@ type DatabaseConfig struct {
Connection string Connection string
} }
type LoggingConfig struct {
Level string
}
type Configuration struct { type Configuration struct {
Server ServerConfig Server ServerConfig
Admin ServerConfig
File FileConfig 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) {

@ -8,11 +8,15 @@ import (
func TestConfiguration(t *testing.T) { func TestConfiguration(t *testing.T) {
is := is.New(t) is := is.New(t)
c, _ := ConfigFromToml( c, _ := ConfigFromToml(`
`[server] [server]
port = 8002 port = 8002
host = "127.0.0.1" host = "127.0.0.1"
[admin]
port = 8001
host = "127.0.0.1"
[file] [file]
path = "/tmp/datta2" path = "/tmp/datta2"

@ -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

@ -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