Compare commits
No commits in common. 'master' and 'v0.1.0' have entirely different histories.
@ -1,2 +0,0 @@
|
||||
Dockerfile
|
||||
test
|
@ -1,3 +0,0 @@
|
||||
sql.db
|
||||
bog
|
||||
test
|
@ -1,7 +0,0 @@
|
||||
pipeline:
|
||||
build:
|
||||
image: golang
|
||||
commands:
|
||||
- go test ./...
|
||||
environment:
|
||||
- GOPRIVATE=git.sg.caj.me/caj
|
@ -1,41 +0,0 @@
|
||||
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
|
@ -0,0 +1,101 @@
|
||||
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)
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
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")
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
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")
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package dataswamp
|
||||
|
||||
import (
|
||||
// "caj-larsson/bog/dataswamp/namespace"
|
||||
)
|
||||
|
||||
type AdminService struct{}
|
@ -1 +0,0 @@
|
||||
package dataswamp
|
@ -1,15 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
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))
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
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")))
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
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")
|
||||
)
|
@ -1,67 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
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)
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
@ -1,18 +1,23 @@
|
||||
package namespace
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Namespace struct {
|
||||
type UserAgent struct {
|
||||
ID int64
|
||||
Name string
|
||||
LastSeen time.Time
|
||||
AllowanceDuration time.Duration
|
||||
FileQuota FileSizeQuota
|
||||
Download FileStat
|
||||
Upload FileStat
|
||||
}
|
||||
|
||||
type BogFile struct {
|
||||
UserAgentId int64
|
||||
Path string
|
||||
Size int64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
var (
|
@ -0,0 +1,21 @@
|
||||
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)
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
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
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
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
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package namespace
|
||||
|
||||
import (
|
||||
"caj-larsson/bog/dataswamp/namespace"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileRepo(t *testing.T) {
|
||||
namespace.RepositoryContract(NewRepository, t)
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package swampfile
|
||||
|
||||
import (
|
||||
"caj-larsson/bog/dataswamp/swampfile"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileRepo(t *testing.T) {
|
||||
swampfile.RepositoryContract(NewRepository, t)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
// 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,
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
// 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
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
-- 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 = ?;
|
@ -1,260 +0,0 @@
|
||||
// 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
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package namespace
|
||||
|
||||
import (
|
||||
"caj-larsson/bog/dataswamp/namespace"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileRepo(t *testing.T) {
|
||||
namespace.RepositoryContract(func() namespace.Repository { return NewRepository(":memory:") }, t)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
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
|
||||
);
|
@ -1,11 +0,0 @@
|
||||
package system_time
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Clock struct{}
|
||||
|
||||
func (c Clock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
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)
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
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
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
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,163 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
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,36 +0,0 @@
|
||||
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))
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
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...)
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<!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>
|
@ -1,25 +0,0 @@
|
||||
<!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>
|
@ -1,8 +0,0 @@
|
||||
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,94 @@
|
||||
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")
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
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")
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
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)
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
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)
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
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")
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"testing"
|
||||
//"caj-larsson/bog/domain"
|
||||
)
|
||||
|
||||
|
||||
func TestMockFileRepo(t *testing.T) {
|
||||
BogFileRepositoryContract(NewMockFileRepository, t)
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
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
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
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 ")
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
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())
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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…
Reference in New Issue