Compare commits
34 Commits
@ -0,0 +1,2 @@
|
|||||||
|
Dockerfile
|
||||||
|
test
|
@ -0,0 +1,3 @@
|
|||||||
|
sql.db
|
||||||
|
bog
|
||||||
|
test
|
@ -0,0 +1,7 @@
|
|||||||
|
pipeline:
|
||||||
|
build:
|
||||||
|
image: golang
|
||||||
|
commands:
|
||||||
|
- go test ./...
|
||||||
|
environment:
|
||||||
|
- GOPRIVATE=git.sg.caj.me/caj
|
@ -0,0 +1,41 @@
|
|||||||
|
FROM golang:alpine as builder
|
||||||
|
|
||||||
|
ENV GO111MODULE=on
|
||||||
|
|
||||||
|
ENV USER=appuser
|
||||||
|
ENV UID=1000
|
||||||
|
RUN adduser \
|
||||||
|
--disabled-password \
|
||||||
|
--gecos "" \
|
||||||
|
--home "/nonexistent" \
|
||||||
|
--shell "/sbin/nologin" \
|
||||||
|
--no-create-home \
|
||||||
|
--uid "${UID}" \
|
||||||
|
"${USER}"
|
||||||
|
|
||||||
|
RUN apk update && apk add --no-cache git ca-certificates tzdata sqlite build-base
|
||||||
|
RUN mkdir /build
|
||||||
|
COPY . /build/
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY ./ ./
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o bog .
|
||||||
|
|
||||||
|
FROM scratch AS final
|
||||||
|
LABEL author="Cajually <me@caj.me>"
|
||||||
|
|
||||||
|
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
COPY --from=builder /etc/passwd /etc/passwd
|
||||||
|
COPY --from=builder /etc/group /etc/group
|
||||||
|
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=builder /build/bog /
|
||||||
|
COPY --from=builder /build/server/views /server/views
|
||||||
|
COPY --from=builder /build/default.toml /
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
USER appuser:appuser
|
||||||
|
ENTRYPOINT ["/bog"]
|
||||||
|
|
||||||
|
EXPOSE 8002
|
@ -1,101 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
// "io"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
"caj-larsson/bog/integration"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Router interface {
|
|
||||||
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
|
|
||||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Bog struct {
|
|
||||||
router Router
|
|
||||||
file_service domain.BogFileService
|
|
||||||
address string
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildFileDataRepository(config FileConfig) domain.FileDataRepository{
|
|
||||||
fsBogRepo := new(integration.FileSystemBogRepository)
|
|
||||||
fsBogRepo.Root = config.Path
|
|
||||||
return fsBogRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildUserAgentRepository(config DatabaseConfig) *integration.SQLiteUserAgentRepository{
|
|
||||||
if config.Backend != "sqlite" {
|
|
||||||
panic("Can only handle sqlite")
|
|
||||||
}
|
|
||||||
return integration.NewSQLiteUserAgentRepository(config.Connection)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bog) fileHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path == "/" {
|
|
||||||
fmt.Fprintf(w, "Hi")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ref := domain.FileReference {r.URL.Path, r.Header["User-Agent"][0]}
|
|
||||||
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
bog_file, err := b.file_service.OpenOutFile(ref)
|
|
||||||
|
|
||||||
if err == domain.ErrNotExists {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.ServeContent(w, r, bog_file.Path(), bog_file.Modified(), bog_file)
|
|
||||||
case "POST":
|
|
||||||
fallthrough
|
|
||||||
case "PUT":
|
|
||||||
size_str := r.Header["Content-Length"][0]
|
|
||||||
|
|
||||||
size, err := strconv.ParseInt(size_str, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(422)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = b.file_service.SaveFile(ref, r.Body, size)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bog) routes() {
|
|
||||||
b.router.HandleFunc("/", b.fileHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func New(config *Configuration) *Bog {
|
|
||||||
b := new(Bog)
|
|
||||||
b.address = config.bindAddress()
|
|
||||||
|
|
||||||
fsBogRepo := buildFileDataRepository(config.File)
|
|
||||||
uaRepo := buildUserAgentRepository(config.Database)
|
|
||||||
|
|
||||||
b.file_service = domain.NewBogFileService(
|
|
||||||
uaRepo, fsBogRepo, config.Quota.ParsedSizeBytes(), config.Quota.ParsedDuration(),
|
|
||||||
)
|
|
||||||
|
|
||||||
b.router = new(http.ServeMux)
|
|
||||||
b.routes()
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bog) Run() {
|
|
||||||
http.ListenAndServe(b.address, b.router)
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
"caj-larsson/bog/test/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestApplication(t *testing.T) {
|
|
||||||
|
|
||||||
file_service := domain.NewBogFileService(
|
|
||||||
mock.NewMockUserAgentRepository(),
|
|
||||||
mock.NewMockFileRepository(),
|
|
||||||
1000,
|
|
||||||
time.Hour,
|
|
||||||
)
|
|
||||||
|
|
||||||
bog := Bog {
|
|
||||||
router: new(http.ServeMux),
|
|
||||||
file_service: file_service,
|
|
||||||
address: "fake",
|
|
||||||
}
|
|
||||||
bog.routes()
|
|
||||||
req := httptest.NewRequest("POST", "/apath", strings.NewReader("testdata"))
|
|
||||||
req.Header.Add("User-Agent", "testingclient")
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
bog.router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if (w.Code != 200){
|
|
||||||
fmt.Printf("%v", w)
|
|
||||||
t.Error("not ok")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func TestConfiguration(t *testing.T) {
|
|
||||||
c, _ := ConfigFromToml(
|
|
||||||
`[server]
|
|
||||||
port = 8002
|
|
||||||
host = "127.0.0.1"
|
|
||||||
|
|
||||||
[file]
|
|
||||||
path = "/tmp/datta2"
|
|
||||||
|
|
||||||
[database]
|
|
||||||
backend = "sqlite"
|
|
||||||
connection = "sql.db"
|
|
||||||
|
|
||||||
[quota]
|
|
||||||
default_size = "1MB"
|
|
||||||
default_duration = "72h"`,
|
|
||||||
)
|
|
||||||
|
|
||||||
if c.Server.Port != 8002 {
|
|
||||||
t.Errorf("port parsing failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Server.Host != "127.0.0.1" {
|
|
||||||
t.Errorf("host parsing failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Quota.ParsedSizeBytes() != 1024 * 1024 {
|
|
||||||
t.Errorf("quota size parsing failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Quota.ParsedDuration() != time.Duration(time.Hour * 72) {
|
|
||||||
t.Errorf("quota size parsing failed")
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,7 @@
|
|||||||
|
package dataswamp
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "caj-larsson/bog/dataswamp/namespace"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminService struct{}
|
@ -0,0 +1 @@
|
|||||||
|
package dataswamp
|
@ -1,23 +1,18 @@
|
|||||||
package domain
|
package namespace
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserAgent struct {
|
type Namespace struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
LastSeen time.Time
|
LastSeen time.Time
|
||||||
AllowanceDuration time.Duration
|
AllowanceDuration time.Duration
|
||||||
FileQuota FileSizeQuota
|
FileQuota FileSizeQuota
|
||||||
}
|
Download FileStat
|
||||||
|
Upload FileStat
|
||||||
type BogFile struct {
|
|
||||||
UserAgentId int64
|
|
||||||
Path string
|
|
||||||
Size int64
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
@ -0,0 +1,15 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoNamespace = errors.New("that namespace does not exist")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
Create(namespace Namespace) (*Namespace, error)
|
||||||
|
All() ([]Namespace, error)
|
||||||
|
GetByName(name string) (*Namespace, error)
|
||||||
|
Update(id int64, namespace Namespace) (*Namespace, error)
|
||||||
|
Delete(id int64) error
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RepositoryContract(fac func() Repository, t *testing.T) {
|
||||||
|
basicNamespaceContract(fac, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func basicNamespaceContract(fac func() Repository, t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
r := fac()
|
||||||
|
|
||||||
|
all, err := r.All()
|
||||||
|
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(len(all), 0)
|
||||||
|
|
||||||
|
ns := Namespace{
|
||||||
|
23,
|
||||||
|
"n1",
|
||||||
|
time.Now(),
|
||||||
|
time.Duration(time.Hour * 3),
|
||||||
|
FileSizeQuota{1000, 0},
|
||||||
|
FileStat{1, 2},
|
||||||
|
FileStat{3, 4},
|
||||||
|
}
|
||||||
|
|
||||||
|
ns1, err := r.Create(ns)
|
||||||
|
is.NoErr(err)
|
||||||
|
ns.Name = "n2"
|
||||||
|
ns2, err := r.Create(ns)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
is.True(ns1.ID != ns2.ID)
|
||||||
|
|
||||||
|
all, err = r.All()
|
||||||
|
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(len(all), 2)
|
||||||
|
|
||||||
|
is.Equal(ns.ID, int64(23))
|
||||||
|
|
||||||
|
ns3, _ := r.GetByName("n2")
|
||||||
|
|
||||||
|
is.Equal(*ns3, *ns2)
|
||||||
|
|
||||||
|
is.NoErr(r.Delete(ns2.ID))
|
||||||
|
|
||||||
|
all, err = r.All()
|
||||||
|
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(len(all), 1)
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/util"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Clock interface {
|
||||||
|
Now() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type NamespaceService struct {
|
||||||
|
repo Repository
|
||||||
|
outboxes []func(util.Event)
|
||||||
|
logger util.Logger
|
||||||
|
clock Clock
|
||||||
|
default_ttl time.Duration
|
||||||
|
default_quota_bytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNamespaceService(repo Repository, logger util.Logger, clock Clock, default_ttl time.Duration, default_quota_bytes int64) *NamespaceService {
|
||||||
|
return &NamespaceService{
|
||||||
|
repo,
|
||||||
|
nil,
|
||||||
|
logger,
|
||||||
|
clock,
|
||||||
|
default_ttl,
|
||||||
|
default_quota_bytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NamespaceService) GetOrCreateNs(name string) *Namespace {
|
||||||
|
ns, err := s.repo.GetByName(name)
|
||||||
|
|
||||||
|
if err == ErrNotExists {
|
||||||
|
new_ns := Namespace{
|
||||||
|
0,
|
||||||
|
name,
|
||||||
|
s.clock.Now(),
|
||||||
|
s.default_ttl,
|
||||||
|
FileSizeQuota{s.default_quota_bytes, 0},
|
||||||
|
FileStat{0, 0},
|
||||||
|
FileStat{0, 0},
|
||||||
|
}
|
||||||
|
created_ns, err := s.repo.Create(new_ns)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return created_ns
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return ns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NamespaceService) Wire(reg func(string, util.EventHandler), outbox func(ev util.Event)) {
|
||||||
|
reg("FileUsed", s.handleFileUsed)
|
||||||
|
s.outboxes = append(s.outboxes, outbox)
|
||||||
|
|
||||||
|
reg("FileUsed", s.handleFileUsed)
|
||||||
|
reg("FileDeleted", s.handleFileDeleted)
|
||||||
|
reg("FileRecieved", s.handleFileRecieved)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NamespaceService) All() []Namespace {
|
||||||
|
nss, err := s.repo.All()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return nss
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NamespaceService) handleFileUsed(payload interface{}) {
|
||||||
|
var payload_s = payload.(struct {
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
})
|
||||||
|
fmt.Printf("file used %v\n", payload_s)
|
||||||
|
ns := s.GetOrCreateNs(payload_s.Name)
|
||||||
|
ns.FileQuota = ns.FileQuota.Add(payload_s.Size)
|
||||||
|
s.repo.Update(ns.ID, *ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NamespaceService) handleFileDeleted(payload interface{}) {
|
||||||
|
var payload_s = payload.(struct {
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
})
|
||||||
|
fmt.Printf("file deleted %v\n", payload_s)
|
||||||
|
ns := s.GetOrCreateNs(payload_s.Name)
|
||||||
|
ns.FileQuota = ns.FileQuota.Add(-payload_s.Size)
|
||||||
|
fmt.Printf("file usage %v\n", ns.FileQuota)
|
||||||
|
s.repo.Update(ns.ID, *ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NamespaceService) handleFileRecieved(payload interface{}) {
|
||||||
|
var payload_s = payload.(struct {
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
})
|
||||||
|
fmt.Printf("file recieved %v\n", payload_s)
|
||||||
|
ns := s.GetOrCreateNs(payload_s.Name)
|
||||||
|
ns.FileQuota = ns.FileQuota.Add(payload_s.Size)
|
||||||
|
fmt.Printf("file usage %v\n", ns.FileQuota)
|
||||||
|
s.repo.Update(ns.ID, *ns)
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/util"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEventTest(t *testing.T) {
|
||||||
|
eb := util.NewEventBus()
|
||||||
|
svc := NamespaceService{}
|
||||||
|
|
||||||
|
svc.Wire(eb.Register, eb.Handle)
|
||||||
|
|
||||||
|
events := []util.Event{
|
||||||
|
*util.NewEvent("FileUsed", struct {
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
}{
|
||||||
|
"asd",
|
||||||
|
int64(12),
|
||||||
|
}),
|
||||||
|
*util.NewEvent("FileDeleted", struct {
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
}{
|
||||||
|
"asd",
|
||||||
|
int64(12),
|
||||||
|
}),
|
||||||
|
*util.NewEvent("FileRecieved", struct {
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
}{
|
||||||
|
"asd",
|
||||||
|
int64(12),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
util.AcceptsMessage(t, eb, events)
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
type FileSizeQuota struct {
|
||||||
|
AllowanceKB int64
|
||||||
|
CurrentUsage int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSizeQuota) Allows(size int64) bool {
|
||||||
|
return f.Remaining() >= size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSizeQuota) Remaining() int64 {
|
||||||
|
return f.AllowanceKB - f.CurrentUsage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSizeQuota) Add(size int64) FileSizeQuota {
|
||||||
|
return FileSizeQuota{
|
||||||
|
f.AllowanceKB,
|
||||||
|
f.CurrentUsage + size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSizeQuota) Remove(size int64) FileSizeQuota {
|
||||||
|
return FileSizeQuota{
|
||||||
|
f.AllowanceKB,
|
||||||
|
f.CurrentUsage - size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileStat struct {
|
||||||
|
Num int64
|
||||||
|
SizeB int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FileStat) Add(size int64) FileStat {
|
||||||
|
return FileStat{
|
||||||
|
s.Num + 1,
|
||||||
|
s.SizeB + size,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQuota(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
quota := FileSizeQuota{1000, 0}
|
||||||
|
|
||||||
|
is.True(quota.Allows(1000))
|
||||||
|
is.True(!quota.Allows(1001))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaManipulation(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
quota := FileSizeQuota{1000, 0}
|
||||||
|
quota = quota.Add(500)
|
||||||
|
is.Equal(quota.CurrentUsage, int64(500))
|
||||||
|
quota = quota.Remove(1000)
|
||||||
|
is.Equal(quota.CurrentUsage, int64(-500))
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
package dataswamp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/dataswamp/namespace"
|
||||||
|
"caj-larsson/bog/dataswamp/swampfile"
|
||||||
|
"caj-larsson/bog/util"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
// "errors"
|
||||||
|
// "fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataSwampService struct {
|
||||||
|
ns_svc namespace.NamespaceService
|
||||||
|
swamp_file_repo swampfile.Repository
|
||||||
|
logger util.Logger
|
||||||
|
eventBus util.EventBus
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDataSwampService(
|
||||||
|
ns_svc namespace.NamespaceService,
|
||||||
|
swamp_file_repo swampfile.Repository,
|
||||||
|
logger util.Logger,
|
||||||
|
) *DataSwampService {
|
||||||
|
s := DataSwampService{ns_svc, swamp_file_repo, logger, *util.NewEventBus()}
|
||||||
|
ns_svc.Wire(s.eventBus.Register, s.eventBus.Handle)
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DataSwampService) NamespaceStats() []namespace.Namespace {
|
||||||
|
return s.ns_svc.All()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DataSwampService) SaveFile(ref swampfile.FileReference, src io.Reader, size int64) error {
|
||||||
|
ns := s.ns_svc.GetOrCreateNs(ref.UserAgent)
|
||||||
|
|
||||||
|
r, err := ref.Clean(true)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ns.FileQuota.Allows(size) {
|
||||||
|
return namespace.ErrExceedQuota
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := s.swamp_file_repo.Create(r.Path, strconv.FormatInt(ns.ID, 10))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// TODO: convert this into a different error.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.eventBus.Handle(*util.NewEvent("FileUsed", struct {
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
}{
|
||||||
|
ns.Name,
|
||||||
|
f.Size(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// TODO: rewrite this into an interruptable loop that emits downloaded events
|
||||||
|
written, err := io.CopyN(f, src, size)
|
||||||
|
|
||||||
|
if written < size {
|
||||||
|
s.swamp_file_repo.Delete(r.Path, strconv.FormatInt(ns.ID, 10)) //
|
||||||
|
return swampfile.ErrContentSizeExaggerated
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf = make([]byte, 1)
|
||||||
|
|
||||||
|
overread, err := src.Read(buf)
|
||||||
|
|
||||||
|
if overread > 0 || err != io.EOF {
|
||||||
|
s.swamp_file_repo.Delete(r.Path, strconv.FormatInt(ns.ID, 10))
|
||||||
|
return swampfile.ErrContentSizeExceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.eventBus.Handle(*util.NewEvent("FileRecieved", struct {
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
}{
|
||||||
|
ns.Name,
|
||||||
|
written,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DataSwampService) OpenOutFile(ref swampfile.FileReference) (swampfile.SwampOutFile, error) {
|
||||||
|
ns := s.ns_svc.GetOrCreateNs(ref.UserAgent)
|
||||||
|
|
||||||
|
r, err := ref.Clean(true)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := s.swamp_file_repo.Open(r.Path, strconv.FormatInt(ns.ID, 10))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.eventBus.Handle(*util.NewEvent("FileSent", f.Size()))
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DataSwampService) CleanUpExpiredFiles() error {
|
||||||
|
s.logger.Info("Cleaning up expired files")
|
||||||
|
|
||||||
|
for _, ns := range s.ns_svc.All() {
|
||||||
|
expiry := time.Now().Add(-ns.AllowanceDuration)
|
||||||
|
dfs, err := s.swamp_file_repo.DeleteOlderThan(strconv.FormatInt(ns.ID, 10), expiry)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, df := range dfs {
|
||||||
|
s.eventBus.Handle(*util.NewEvent("FileDeleted", struct {
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
}{
|
||||||
|
ns.Name,
|
||||||
|
df.Size,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,187 @@
|
|||||||
|
package dataswamp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"caj-larsson/bog/dataswamp/namespace"
|
||||||
|
"caj-larsson/bog/dataswamp/swampfile"
|
||||||
|
m_namespace "caj-larsson/bog/infrastructure/memory/namespace"
|
||||||
|
m_swampfile "caj-larsson/bog/infrastructure/memory/swampfile"
|
||||||
|
"caj-larsson/bog/infrastructure/system_time"
|
||||||
|
"fmt"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestLogger struct{}
|
||||||
|
|
||||||
|
func (t TestLogger) Debug(format string, a ...interface{}) {}
|
||||||
|
func (t TestLogger) Info(format string, a ...interface{}) {}
|
||||||
|
func (t TestLogger) Warn(format string, a ...interface{}) {}
|
||||||
|
|
||||||
|
var file_ref1 = swampfile.FileReference{"/path1", "ns1"}
|
||||||
|
var file_ref2 = swampfile.FileReference{"/path1", "ns2"}
|
||||||
|
var file_ref3 = swampfile.FileReference{"/path2", "ns1"}
|
||||||
|
|
||||||
|
func NewTestDataSwampService() DataSwampService {
|
||||||
|
file_repo := m_swampfile.NewRepository()
|
||||||
|
ns_repo := m_namespace.NewRepository()
|
||||||
|
|
||||||
|
logger := TestLogger{}
|
||||||
|
ns_svc := namespace.NewNamespaceService(
|
||||||
|
ns_repo,
|
||||||
|
logger,
|
||||||
|
system_time.Clock{},
|
||||||
|
time.Hour,
|
||||||
|
1024,
|
||||||
|
)
|
||||||
|
|
||||||
|
return *NewDataSwampService(*ns_svc, file_repo, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileDontExist(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
s := NewTestDataSwampService()
|
||||||
|
outfile, err := s.OpenOutFile(file_ref1)
|
||||||
|
|
||||||
|
is.True(err == swampfile.ErrNotExists)
|
||||||
|
is.True(outfile == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileIsStored(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
s := NewTestDataSwampService()
|
||||||
|
|
||||||
|
fakefile := bytes.NewBufferString("My bog data")
|
||||||
|
|
||||||
|
err := s.SaveFile(file_ref1, fakefile, int64(fakefile.Len()))
|
||||||
|
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
largefakefile := bytes.NewBufferString("")
|
||||||
|
|
||||||
|
for largefakefile.Len() < 64000 {
|
||||||
|
_, err = largefakefile.WriteString("A very repetitive file")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.SaveFile(file_ref3, largefakefile, int64(largefakefile.Len()))
|
||||||
|
|
||||||
|
is.Equal(err, swampfile.ErrExceedQuota)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileIsReadBack(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
s := NewTestDataSwampService()
|
||||||
|
|
||||||
|
infile := bytes.NewBufferString("My bog data")
|
||||||
|
|
||||||
|
_ = s.SaveFile(file_ref1, infile, int64(infile.Len()))
|
||||||
|
|
||||||
|
outswampfile, _ := s.OpenOutFile(file_ref1)
|
||||||
|
|
||||||
|
outfile := bytes.NewBufferString("")
|
||||||
|
_, _ = outfile.ReadFrom(outswampfile)
|
||||||
|
|
||||||
|
is.Equal(outfile.String(), "My bog data")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNSIsolation(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
s := NewTestDataSwampService()
|
||||||
|
|
||||||
|
ns1_file := bytes.NewBufferString("My bog data ns1")
|
||||||
|
ns2_file := bytes.NewBufferString("My bog data ns2")
|
||||||
|
|
||||||
|
_ = s.SaveFile(file_ref1, ns1_file, int64(ns1_file.Len()))
|
||||||
|
_ = s.SaveFile(file_ref2, ns2_file, int64(ns2_file.Len()))
|
||||||
|
|
||||||
|
outswampfile, _ := s.OpenOutFile(file_ref1)
|
||||||
|
|
||||||
|
outfile := bytes.NewBufferString("")
|
||||||
|
_, _ = outfile.ReadFrom(outswampfile)
|
||||||
|
|
||||||
|
is.Equal(outfile.String(), "My bog data ns1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathStrictMode(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
s := NewTestDataSwampService()
|
||||||
|
|
||||||
|
ns_file := bytes.NewBufferString("My bog data ns1")
|
||||||
|
|
||||||
|
ref := swampfile.FileReference{
|
||||||
|
"/path/../with/../backrefs",
|
||||||
|
"ns1",
|
||||||
|
}
|
||||||
|
|
||||||
|
outfile, err := s.OpenOutFile(ref)
|
||||||
|
|
||||||
|
is.Equal(err, swampfile.ErrUnacceptablePath)
|
||||||
|
is.Equal(outfile, nil)
|
||||||
|
|
||||||
|
err = s.SaveFile(ref, ns_file, int64(ns_file.Len()))
|
||||||
|
is.Equal(err, swampfile.ErrUnacceptablePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaWithContenSizeLieOver(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
s := NewTestDataSwampService()
|
||||||
|
|
||||||
|
largefakefile := bytes.NewBufferString("")
|
||||||
|
|
||||||
|
for largefakefile.Len() < 64000 {
|
||||||
|
_, _ = largefakefile.WriteString("A very repetitive file")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.SaveFile(file_ref3, largefakefile, int64(10))
|
||||||
|
|
||||||
|
is.Equal(err, swampfile.ErrContentSizeExceeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaWithContenSizeLieUnder(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
s := NewTestDataSwampService()
|
||||||
|
|
||||||
|
largefakefile := bytes.NewBufferString("small")
|
||||||
|
|
||||||
|
err := s.SaveFile(file_ref3, largefakefile, int64(1024))
|
||||||
|
|
||||||
|
is.Equal(err, swampfile.ErrContentSizeExaggerated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanUpExpired(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file_repo := m_swampfile.Repository{fs}
|
||||||
|
ns_repo := m_namespace.NewRepository()
|
||||||
|
logger := TestLogger{}
|
||||||
|
ns_svc := namespace.NewNamespaceService(
|
||||||
|
ns_repo,
|
||||||
|
logger,
|
||||||
|
system_time.Clock{},
|
||||||
|
time.Hour,
|
||||||
|
1024,
|
||||||
|
)
|
||||||
|
|
||||||
|
s := NewDataSwampService(*ns_svc, file_repo, logger)
|
||||||
|
|
||||||
|
fakefile := bytes.NewBufferString("My bog")
|
||||||
|
err := s.SaveFile(file_ref1, fakefile, int64(fakefile.Len()))
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
fakefile = bytes.NewBufferString("My bog")
|
||||||
|
err = s.SaveFile(file_ref3, fakefile, int64(fakefile.Len()))
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
err = fs.Chtimes("1/path1", time.Now(), time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
is.NoErr(s.CleanUpExpiredFiles())
|
||||||
|
|
||||||
|
ns, err := ns_repo.GetByName("ns1")
|
||||||
|
fmt.Printf("file final usage %v\n", ns.FileQuota)
|
||||||
|
is.NoErr(err)
|
||||||
|
fmt.Printf("file\n")
|
||||||
|
is.Equal(ns.FileQuota.CurrentUsage, int64(len("My bog")))
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package swampfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SwampFile struct {
|
||||||
|
UserAgentId int64
|
||||||
|
Path string
|
||||||
|
Size int64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDuplicate = errors.New("record already exists")
|
||||||
|
ErrExceedQuota = errors.New("file too large")
|
||||||
|
ErrQuotaInvalid = errors.New("quota invalid")
|
||||||
|
ErrContentSizeExceeded = errors.New("content size exceeded")
|
||||||
|
ErrContentSizeExaggerated = errors.New("content size exaggerated")
|
||||||
|
ErrNotExists = errors.New("row not exists")
|
||||||
|
ErrUpdateFailed = errors.New("update failed")
|
||||||
|
ErrDeleteFailed = errors.New("delete failed")
|
||||||
|
ErrUnacceptablePath = errors.New("unacceptable path")
|
||||||
|
ErrCorrupted = errors.New("repository contains unexpected data")
|
||||||
|
)
|
@ -0,0 +1,67 @@
|
|||||||
|
package swampfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RepositoryContract(fac func() Repository, t *testing.T) {
|
||||||
|
basicFileOperationContract(fac, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func basicFileOperationContract(fac func() Repository, t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
repo := fac()
|
||||||
|
|
||||||
|
not_file, err := repo.Open("doesnot", "exist")
|
||||||
|
|
||||||
|
is.Equal(err, ErrNotExists)
|
||||||
|
is.Equal(not_file, nil)
|
||||||
|
|
||||||
|
new_file, err := repo.Create("newfile.new", "ua1")
|
||||||
|
|
||||||
|
is.NoErr(err)
|
||||||
|
is.True(new_file != nil)
|
||||||
|
|
||||||
|
var testdata = "testingdata"
|
||||||
|
|
||||||
|
new_file.Write([]byte(testdata))
|
||||||
|
new_file.Close()
|
||||||
|
|
||||||
|
reopened_file, err := repo.Open("newfile.new", "ua1")
|
||||||
|
|
||||||
|
is.NoErr(err)
|
||||||
|
is.True(reopened_file != nil)
|
||||||
|
is.Equal(reopened_file.Size(), int64(len(testdata)))
|
||||||
|
|
||||||
|
readback := make([]byte, 128)
|
||||||
|
|
||||||
|
size, err := reopened_file.Read(readback)
|
||||||
|
|
||||||
|
is.Equal(string(readback[0:size]), testdata)
|
||||||
|
|
||||||
|
reopened_file.Close()
|
||||||
|
|
||||||
|
repo.Delete("newfile.new", "ua1")
|
||||||
|
|
||||||
|
deleted_file, err := repo.Open("newfile.new", "ua1")
|
||||||
|
|
||||||
|
is.Equal(err, ErrNotExists)
|
||||||
|
is.True(deleted_file == nil)
|
||||||
|
|
||||||
|
// DeleteOlderThan
|
||||||
|
|
||||||
|
expiring_file, err := repo.Create("deep/dir/expiring.new", "ua1")
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
expiring_file.Write([]byte(testdata))
|
||||||
|
expiring_file.Close()
|
||||||
|
|
||||||
|
_, err = repo.DeleteOlderThan("ua1", time.Now().Add(time.Hour))
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
expired_file, err := repo.Open("deep/dir/expiring.new", "ua1")
|
||||||
|
is.Equal(err, ErrNotExists)
|
||||||
|
is.True(expired_file == nil)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package swampfile
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
Create(filename string, namespace_stub string) (SwampInFile, error)
|
||||||
|
Open(filename string, namespace_stub string) (SwampOutFile, error)
|
||||||
|
Delete(filename string, namespace_stub string)
|
||||||
|
DeleteOlderThan(namespace_stub string, ts time.Time) ([]DeletedSwampFile, error)
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package swampfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SwampOutFile interface {
|
||||||
|
Path() string
|
||||||
|
Size() int64
|
||||||
|
Modified() time.Time
|
||||||
|
io.Reader
|
||||||
|
io.Seeker
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
type SwampInFile interface {
|
||||||
|
Path() string
|
||||||
|
Size() int64
|
||||||
|
Modified() time.Time
|
||||||
|
io.Writer
|
||||||
|
io.Seeker
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileReference struct {
|
||||||
|
Path string
|
||||||
|
UserAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeletedSwampFile struct {
|
||||||
|
Path string
|
||||||
|
Size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fr *FileReference) Clean(strict bool) (*FileReference, error) {
|
||||||
|
c := filepath.FromSlash(path.Clean("/" + strings.Trim(fr.Path, "/")))
|
||||||
|
|
||||||
|
if c != fr.Path && strict {
|
||||||
|
return nil, ErrUnacceptablePath
|
||||||
|
}
|
||||||
|
|
||||||
|
n := FileReference{
|
||||||
|
c,
|
||||||
|
fr.UserAgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &n, nil
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package swampfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSwampPathNotStrict(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
dirty string
|
||||||
|
clean string
|
||||||
|
}{
|
||||||
|
{"/", "/"},
|
||||||
|
{"..", "/"},
|
||||||
|
{"/../a", "/a"},
|
||||||
|
{"/../a/../a", "/a"},
|
||||||
|
{"../b", "/b"},
|
||||||
|
}
|
||||||
|
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
rf := FileReference{
|
||||||
|
tc.dirty,
|
||||||
|
"ns",
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err := rf.Clean(false)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(rc.Path, tc.clean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwampPathStrict(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
rf := FileReference{
|
||||||
|
"/a/../b",
|
||||||
|
"ns",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := rf.Clean(true)
|
||||||
|
is.Equal(err, ErrUnacceptablePath)
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -1,21 +0,0 @@
|
|||||||
package domain
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNoUserAgent = errors.New("that useragent does not exist")
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserAgentRepository interface{
|
|
||||||
Create(useragent UserAgent) (*UserAgent, error)
|
|
||||||
All() ([]UserAgent, error)
|
|
||||||
GetByName(name string) (*UserAgent, error)
|
|
||||||
Update(id int64, useragent UserAgent) (*UserAgent, error)
|
|
||||||
Delete(id int64) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileDataRepository interface {
|
|
||||||
Create(filename string, user_agent_label string) (BogInFile, error)
|
|
||||||
Open(filename string, user_agent_label string) (BogOutFile, error)
|
|
||||||
Delete(filename string, user_agent_label string)
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
package domain
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
type BogFileService struct {
|
|
||||||
user_agent_repo UserAgentRepository
|
|
||||||
file_data_repo FileDataRepository
|
|
||||||
default_allowance_bytes int64
|
|
||||||
default_allowance_duration time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBogFileService(
|
|
||||||
user_agent_repo UserAgentRepository,
|
|
||||||
file_data_repo FileDataRepository,
|
|
||||||
da_bytes int64,
|
|
||||||
da_duration time.Duration,
|
|
||||||
) BogFileService {
|
|
||||||
return BogFileService {user_agent_repo, file_data_repo, da_bytes, da_duration}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b BogFileService) getOrCreateUA(useragent_in string) *UserAgent{
|
|
||||||
ua, err := b.user_agent_repo.GetByName(useragent_in)
|
|
||||||
|
|
||||||
if err == ErrNotExists {
|
|
||||||
new_ua := UserAgent {
|
|
||||||
0, useragent_in, time.Now(), b.default_allowance_duration, FileSizeQuota { b.default_allowance_bytes, 0 },
|
|
||||||
}
|
|
||||||
created_ua, err := b.user_agent_repo.Create(new_ua)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return created_ua
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ua
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b BogFileService) SaveFile(ref FileReference, src io.Reader, size int64) error {
|
|
||||||
user_agent := b.getOrCreateUA(ref.UserAgent)
|
|
||||||
|
|
||||||
if !user_agent.FileQuota.Allows(size) {
|
|
||||||
return ErrExceedQuota
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := b.file_data_repo.Create(ref.Path, strconv.FormatInt(user_agent.ID, 10))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
io.Copy(f, src)
|
|
||||||
f.Close()
|
|
||||||
user_agent.FileQuota.Add(size)
|
|
||||||
b.user_agent_repo.Update(user_agent.ID, *user_agent)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b BogFileService) OpenOutFile(ref FileReference) (BogOutFile, error) {
|
|
||||||
user_agent, err := b.user_agent_repo.GetByName(ref.UserAgent)
|
|
||||||
if err == ErrNotExists {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := b.file_data_repo.Open(ref.Path, strconv.FormatInt(user_agent.ID, 10))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f, nil
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package domain
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FileSizeQuota struct {
|
|
||||||
AllowanceKB int64
|
|
||||||
CurrentUsage int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FileSizeQuota) Allows(size int64) bool {
|
|
||||||
return f.CurrentUsage + size <= f.AllowanceKB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FileSizeQuota) Add(size int64) error {
|
|
||||||
if !f.Allows(size) {
|
|
||||||
return ErrExceedQuota
|
|
||||||
}
|
|
||||||
|
|
||||||
f.CurrentUsage += size
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FileSizeQuota) Remove(size int64) error {
|
|
||||||
if size > f.CurrentUsage {
|
|
||||||
return ErrQuotaInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
f.CurrentUsage -= size
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type BogOutFile interface {
|
|
||||||
Path() string
|
|
||||||
Size() int64
|
|
||||||
Modified() time.Time
|
|
||||||
io.Reader
|
|
||||||
io.Seeker
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type BogInFile interface {
|
|
||||||
Path() string
|
|
||||||
Size() int64
|
|
||||||
Modified() time.Time
|
|
||||||
io.Writer
|
|
||||||
io.Seeker
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileReference struct {
|
|
||||||
Path string
|
|
||||||
UserAgent string
|
|
||||||
}
|
|
@ -0,0 +1,170 @@
|
|||||||
|
package swampfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/dataswamp/swampfile"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrDirtyRepo = errors.New("Dirty repository without flag")
|
||||||
|
|
||||||
|
type FileSystemSwampFileData struct {
|
||||||
|
path string
|
||||||
|
size int64
|
||||||
|
mod_time time.Time
|
||||||
|
file *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSystemSwampFileData) Read(p []byte) (int, error) {
|
||||||
|
return f.file.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSystemSwampFileData) Write(p []byte) (int, error) {
|
||||||
|
return f.file.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSystemSwampFileData) Close() error {
|
||||||
|
return f.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSystemSwampFileData) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
return f.file.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSystemSwampFileData) Path() string {
|
||||||
|
return f.path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSystemSwampFileData) Size() int64 {
|
||||||
|
return f.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSystemSwampFileData) Modified() time.Time {
|
||||||
|
stat, _ := f.file.Stat()
|
||||||
|
return stat.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileSystemSwampFileData) Truncate() {
|
||||||
|
err := f.file.Truncate(0)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
Root string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(root string) (*Repository, error) {
|
||||||
|
fi, err := os.ReadDir(root)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
flagpath := path.Join(root, ".bogrepo")
|
||||||
|
|
||||||
|
if len(fi) == 0 {
|
||||||
|
// New Repository, write flagfile
|
||||||
|
f, err := os.OpenFile(flagpath, os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
flagfile, err := os.OpenFile(flagpath, os.O_RDONLY, 0)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrDirtyRepo
|
||||||
|
}
|
||||||
|
flagfile.Close()
|
||||||
|
return &Repository{root}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Repository) absPath(filename string, namespace_ns string) string {
|
||||||
|
return path.Join(f.Root, namespace_ns, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Repository) Create(filename string, namespace_ns string) (swampfile.SwampInFile, error) {
|
||||||
|
abs_path := f.absPath(filename, namespace_ns)
|
||||||
|
|
||||||
|
dir := path.Dir(abs_path)
|
||||||
|
os.MkdirAll(dir, 0750)
|
||||||
|
file, err := os.OpenFile(abs_path, os.O_RDWR|os.O_CREATE, 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stat_info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
file.Truncate(0)
|
||||||
|
|
||||||
|
bfd := FileSystemSwampFileData{filename, stat_info.Size(), stat_info.ModTime(), file}
|
||||||
|
|
||||||
|
return bfd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Repository) Open(filename string, namespace_ns string) (swampfile.SwampOutFile, error) {
|
||||||
|
abs_path := f.absPath(filename, namespace_ns)
|
||||||
|
|
||||||
|
dir := path.Dir(abs_path)
|
||||||
|
os.MkdirAll(dir, 0750)
|
||||||
|
file, err := os.OpenFile(abs_path, os.O_RDONLY, 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, swampfile.ErrNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, err := file.Stat()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bfd := FileSystemSwampFileData{filename, stat.Size(), time.Now(), file}
|
||||||
|
|
||||||
|
return bfd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Repository) Delete(filename string, namespace_ns string) {
|
||||||
|
abs_path := f.absPath(filename, namespace_ns)
|
||||||
|
os.Remove(abs_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Repository) DeleteOlderThan(namespace_stub string, ts time.Time) ([]swampfile.DeletedSwampFile, error) {
|
||||||
|
df := []swampfile.DeletedSwampFile{}
|
||||||
|
|
||||||
|
dr := path.Join(r.Root, namespace_stub)
|
||||||
|
err := filepath.Walk(dr, func(path string, info fs.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mode := info.Mode()
|
||||||
|
|
||||||
|
if mode.IsRegular() {
|
||||||
|
if info.ModTime().Before(ts) {
|
||||||
|
df = append(df, swampfile.DeletedSwampFile{path, info.Size()})
|
||||||
|
err = os.Remove(path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mode.IsDir() {
|
||||||
|
return swampfile.ErrCorrupted
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return df, err
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package swampfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/dataswamp/swampfile"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newRepoDir(t *testing.T) string {
|
||||||
|
r := t.TempDir()
|
||||||
|
d := path.Join(r, "fs")
|
||||||
|
|
||||||
|
err := os.Mkdir(d, 0755)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFsFileRepo(t *testing.T) {
|
||||||
|
|
||||||
|
var fac = func() swampfile.Repository {
|
||||||
|
repo, err := NewRepository(newRepoDir(t))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
|
||||||
|
swampfile.RepositoryContract(fac, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyDir(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
dir_path := newRepoDir(t)
|
||||||
|
_, err := NewRepository(dir_path)
|
||||||
|
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
_, err = os.OpenFile(path.Join(dir_path, ".bogrepo"), os.O_RDONLY, 0)
|
||||||
|
is.NoErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirtyDir(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
dir_path := newRepoDir(t)
|
||||||
|
|
||||||
|
_, err := os.OpenFile(path.Join(dir_path, "randomfile"), os.O_CREATE, 0644)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
_, err = NewRepository(dir_path)
|
||||||
|
|
||||||
|
is.Equal(err, ErrDirtyRepo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirtyWithFlag(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
dir_path := newRepoDir(t)
|
||||||
|
|
||||||
|
_, err := os.OpenFile(path.Join(dir_path, "randomfile"), os.O_CREATE, 0644)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
_, err = os.OpenFile(path.Join(dir_path, ".bogrepo"), os.O_CREATE, 0644)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
_, err = NewRepository(dir_path)
|
||||||
|
is.NoErr(err)
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "time"
|
||||||
|
"caj-larsson/bog/dataswamp/namespace"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
IdIdx map[int64]*namespace.Namespace
|
||||||
|
NameIdx map[string]*namespace.Namespace
|
||||||
|
NextId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository() namespace.Repository {
|
||||||
|
r := new(Repository)
|
||||||
|
r.NextId = 0
|
||||||
|
r.IdIdx = make(map[int64]*namespace.Namespace)
|
||||||
|
r.NameIdx = make(map[string]*namespace.Namespace)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Create(ns namespace.Namespace) (*namespace.Namespace, error) {
|
||||||
|
r.NextId += 1
|
||||||
|
ns.ID = r.NextId
|
||||||
|
|
||||||
|
r.IdIdx[ns.ID] = &ns
|
||||||
|
r.NameIdx[ns.Name] = &ns
|
||||||
|
return &ns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) All() ([]namespace.Namespace, error) {
|
||||||
|
ns := make([]namespace.Namespace, 0, len(r.IdIdx))
|
||||||
|
|
||||||
|
for _, value := range r.IdIdx {
|
||||||
|
ns = append(ns, *value)
|
||||||
|
}
|
||||||
|
return ns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetByName(name string) (*namespace.Namespace, error) {
|
||||||
|
ns, exists := r.NameIdx[name]
|
||||||
|
if exists {
|
||||||
|
return ns, nil
|
||||||
|
}
|
||||||
|
return nil, namespace.ErrNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Update(id int64, ns namespace.Namespace) (*namespace.Namespace, error) {
|
||||||
|
original := *r.IdIdx[id]
|
||||||
|
ns.ID = id
|
||||||
|
r.IdIdx[id] = &ns
|
||||||
|
r.NameIdx[original.Name] = &ns
|
||||||
|
return &ns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Delete(id int64) error {
|
||||||
|
original := *r.IdIdx[id]
|
||||||
|
delete(r.NameIdx, original.Name)
|
||||||
|
delete(r.IdIdx, original.ID)
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/dataswamp/namespace"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileRepo(t *testing.T) {
|
||||||
|
namespace.RepositoryContract(NewRepository, t)
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package swampfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/dataswamp/swampfile"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SwampFile struct {
|
||||||
|
filename string
|
||||||
|
file afero.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f SwampFile) Path() string {
|
||||||
|
return f.filename
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f SwampFile) Size() int64 {
|
||||||
|
stat, _ := f.file.Stat()
|
||||||
|
return int64(stat.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f SwampFile) Read(p []byte) (int, error) {
|
||||||
|
return f.file.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f SwampFile) Write(p []byte) (int, error) {
|
||||||
|
return f.file.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f SwampFile) Close() error {
|
||||||
|
return f.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f SwampFile) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
return f.file.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f SwampFile) Modified() time.Time {
|
||||||
|
stat, _ := f.file.Stat()
|
||||||
|
return stat.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f SwampFile) Truncate() {
|
||||||
|
err := f.file.Truncate(0)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The actual repository
|
||||||
|
type Repository struct {
|
||||||
|
Fs afero.Fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository() swampfile.Repository {
|
||||||
|
return Repository{afero.NewMemMapFs()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Repository) Create(filename string, namespace_stub string) (swampfile.SwampInFile, error) {
|
||||||
|
abs_path := path.Join(namespace_stub, filename)
|
||||||
|
dir := path.Dir(abs_path)
|
||||||
|
r.Fs.MkdirAll(dir, 0750)
|
||||||
|
file, err := r.Fs.OpenFile(abs_path, os.O_RDWR|os.O_CREATE, 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bf := SwampFile{filename, file}
|
||||||
|
|
||||||
|
return bf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Repository) Open(filename string, namespace_stub string) (swampfile.SwampOutFile, error) {
|
||||||
|
abs_path := path.Join(namespace_stub, filename)
|
||||||
|
|
||||||
|
dir := path.Dir(abs_path)
|
||||||
|
r.Fs.MkdirAll(dir, 0750)
|
||||||
|
file, err := r.Fs.OpenFile(abs_path, os.O_RDONLY, 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, swampfile.ErrNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
bf := SwampFile{filename, file}
|
||||||
|
return bf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Repository) Delete(filename string, namespace_stub string) {
|
||||||
|
abs_path := path.Join(namespace_stub, filename)
|
||||||
|
r.Fs.Remove(abs_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Repository) DeleteOlderThan(namespace_stub string, ts time.Time) ([]swampfile.DeletedSwampFile, error) {
|
||||||
|
df := []swampfile.DeletedSwampFile{}
|
||||||
|
|
||||||
|
err := afero.Walk(r.Fs, namespace_stub, func(path string, info fs.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Mode().IsRegular() {
|
||||||
|
if info.ModTime().Before(ts) {
|
||||||
|
df = append(df, swampfile.DeletedSwampFile{path, info.Size()})
|
||||||
|
r.Fs.Remove(path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.Mode().IsDir() {
|
||||||
|
return swampfile.ErrCorrupted
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return df, err
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package swampfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/dataswamp/swampfile"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileRepo(t *testing.T) {
|
||||||
|
swampfile.RepositoryContract(NewRepository, t)
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
|
|
||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBTX interface {
|
||||||
|
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||||
|
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||||
|
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||||
|
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db DBTX) *Queries {
|
||||||
|
return &Queries{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queries struct {
|
||||||
|
db DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||||
|
return &Queries{
|
||||||
|
db: tx,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
|
|
||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileStat struct {
|
||||||
|
ID int64
|
||||||
|
Num int64
|
||||||
|
SizeB int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Namespace struct {
|
||||||
|
ID int64
|
||||||
|
Name string
|
||||||
|
Lastseen int64
|
||||||
|
AllowanceTime sql.NullInt64
|
||||||
|
QuotaKb sql.NullInt64
|
||||||
|
QuotaUsageKb sql.NullInt64
|
||||||
|
DownloadID int64
|
||||||
|
UploadID int64
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
-- name: CreateNamespace :one
|
||||||
|
INSERT INTO
|
||||||
|
namespace(
|
||||||
|
name,
|
||||||
|
lastseen,
|
||||||
|
allowance_time,
|
||||||
|
quota_kb,
|
||||||
|
quota_usage_kb,
|
||||||
|
download_id,
|
||||||
|
upload_id
|
||||||
|
)
|
||||||
|
values(?, ?, ?, ?, ?, ?, ?)
|
||||||
|
returning id;
|
||||||
|
|
||||||
|
-- name: CreateFileStats :one
|
||||||
|
INSERT INTO file_stats(num, size_b)
|
||||||
|
values(?, ?)
|
||||||
|
returning id;
|
||||||
|
|
||||||
|
-- name: AllNamespaces :many
|
||||||
|
SELECT
|
||||||
|
ns.id,
|
||||||
|
ns.name,
|
||||||
|
ns.lastseen,
|
||||||
|
ns.allowance_time,
|
||||||
|
ns.quota_kb,
|
||||||
|
ns.quota_usage_kb,
|
||||||
|
d.num as d_num,
|
||||||
|
d.size_b as d_size_b,
|
||||||
|
ul.num as ul_num,
|
||||||
|
ul.size_b as ul_size_b
|
||||||
|
FROM namespace as ns
|
||||||
|
JOIN file_stats as d
|
||||||
|
ON ns.download_id = d.id
|
||||||
|
JOIN file_stats as ul
|
||||||
|
ON ns.upload_id = ul.id;
|
||||||
|
|
||||||
|
-- name: GetNamespaceByName :one
|
||||||
|
SELECT
|
||||||
|
ns.id,
|
||||||
|
ns.name,
|
||||||
|
ns.lastseen,
|
||||||
|
ns.allowance_time,
|
||||||
|
ns.quota_kb,
|
||||||
|
ns.quota_usage_kb,
|
||||||
|
d.num as d_num,
|
||||||
|
d.size_b as d_size_b,
|
||||||
|
ul.num as ul_num,
|
||||||
|
ul.size_b as ul_size_b
|
||||||
|
FROM namespace as ns
|
||||||
|
JOIN file_stats as d
|
||||||
|
ON ns.download_id = d.id
|
||||||
|
JOIN file_stats as ul
|
||||||
|
ON ns.upload_id = ul.id
|
||||||
|
WHERE ns.name = ?;
|
||||||
|
|
||||||
|
|
||||||
|
-- name: GetFileStat :one
|
||||||
|
SELECT * FROM file_stats where id = ?;
|
||||||
|
|
||||||
|
-- name: UpdateFileStat :exec
|
||||||
|
UPDATE file_stats SET num = ?, size_b = ? where id = ?;
|
||||||
|
|
||||||
|
-- name: UpdateNamespace :one
|
||||||
|
UPDATE namespace SET
|
||||||
|
name = ?,
|
||||||
|
lastseen = ?,
|
||||||
|
allowance_time = ?,
|
||||||
|
quota_kb = ?,
|
||||||
|
quota_usage_kb = ?
|
||||||
|
WHERE id = ?
|
||||||
|
RETURNING download_id, upload_id;
|
||||||
|
|
||||||
|
-- name: DeleteNameSpace :exec
|
||||||
|
DELETE FROM namespace where id = ?;
|
@ -0,0 +1,260 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.13.0
|
||||||
|
// source: queries.sql
|
||||||
|
|
||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
const allNamespaces = `-- name: AllNamespaces :many
|
||||||
|
SELECT
|
||||||
|
ns.id,
|
||||||
|
ns.name,
|
||||||
|
ns.lastseen,
|
||||||
|
ns.allowance_time,
|
||||||
|
ns.quota_kb,
|
||||||
|
ns.quota_usage_kb,
|
||||||
|
d.num as d_num,
|
||||||
|
d.size_b as d_size_b,
|
||||||
|
ul.num as ul_num,
|
||||||
|
ul.size_b as ul_size_b
|
||||||
|
FROM namespace as ns
|
||||||
|
JOIN file_stats as d
|
||||||
|
ON ns.download_id = d.id
|
||||||
|
JOIN file_stats as ul
|
||||||
|
ON ns.upload_id = ul.id
|
||||||
|
`
|
||||||
|
|
||||||
|
type AllNamespacesRow struct {
|
||||||
|
ID int64
|
||||||
|
Name string
|
||||||
|
Lastseen int64
|
||||||
|
AllowanceTime sql.NullInt64
|
||||||
|
QuotaKb sql.NullInt64
|
||||||
|
QuotaUsageKb sql.NullInt64
|
||||||
|
DNum int64
|
||||||
|
DSizeB int64
|
||||||
|
UlNum int64
|
||||||
|
UlSizeB int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) AllNamespaces(ctx context.Context) ([]AllNamespacesRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, allNamespaces)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AllNamespacesRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i AllNamespacesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Lastseen,
|
||||||
|
&i.AllowanceTime,
|
||||||
|
&i.QuotaKb,
|
||||||
|
&i.QuotaUsageKb,
|
||||||
|
&i.DNum,
|
||||||
|
&i.DSizeB,
|
||||||
|
&i.UlNum,
|
||||||
|
&i.UlSizeB,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFileStats = `-- name: CreateFileStats :one
|
||||||
|
INSERT INTO file_stats(num, size_b)
|
||||||
|
values(?, ?)
|
||||||
|
returning id
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateFileStatsParams struct {
|
||||||
|
Num int64
|
||||||
|
SizeB int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateFileStats(ctx context.Context, arg CreateFileStatsParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createFileStats, arg.Num, arg.SizeB)
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNamespace = `-- name: CreateNamespace :one
|
||||||
|
INSERT INTO
|
||||||
|
namespace(
|
||||||
|
name,
|
||||||
|
lastseen,
|
||||||
|
allowance_time,
|
||||||
|
quota_kb,
|
||||||
|
quota_usage_kb,
|
||||||
|
download_id,
|
||||||
|
upload_id
|
||||||
|
)
|
||||||
|
values(?, ?, ?, ?, ?, ?, ?)
|
||||||
|
returning id
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateNamespaceParams struct {
|
||||||
|
Name string
|
||||||
|
Lastseen int64
|
||||||
|
AllowanceTime sql.NullInt64
|
||||||
|
QuotaKb sql.NullInt64
|
||||||
|
QuotaUsageKb sql.NullInt64
|
||||||
|
DownloadID int64
|
||||||
|
UploadID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateNamespace(ctx context.Context, arg CreateNamespaceParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createNamespace,
|
||||||
|
arg.Name,
|
||||||
|
arg.Lastseen,
|
||||||
|
arg.AllowanceTime,
|
||||||
|
arg.QuotaKb,
|
||||||
|
arg.QuotaUsageKb,
|
||||||
|
arg.DownloadID,
|
||||||
|
arg.UploadID,
|
||||||
|
)
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteNameSpace = `-- name: DeleteNameSpace :exec
|
||||||
|
DELETE FROM namespace where id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteNameSpace(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteNameSpace, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileStat = `-- name: GetFileStat :one
|
||||||
|
SELECT id, num, size_b FROM file_stats where id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetFileStat(ctx context.Context, id int64) (FileStat, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getFileStat, id)
|
||||||
|
var i FileStat
|
||||||
|
err := row.Scan(&i.ID, &i.Num, &i.SizeB)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNamespaceByName = `-- name: GetNamespaceByName :one
|
||||||
|
SELECT
|
||||||
|
ns.id,
|
||||||
|
ns.name,
|
||||||
|
ns.lastseen,
|
||||||
|
ns.allowance_time,
|
||||||
|
ns.quota_kb,
|
||||||
|
ns.quota_usage_kb,
|
||||||
|
d.num as d_num,
|
||||||
|
d.size_b as d_size_b,
|
||||||
|
ul.num as ul_num,
|
||||||
|
ul.size_b as ul_size_b
|
||||||
|
FROM namespace as ns
|
||||||
|
JOIN file_stats as d
|
||||||
|
ON ns.download_id = d.id
|
||||||
|
JOIN file_stats as ul
|
||||||
|
ON ns.upload_id = ul.id
|
||||||
|
WHERE ns.name = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetNamespaceByNameRow struct {
|
||||||
|
ID int64
|
||||||
|
Name string
|
||||||
|
Lastseen int64
|
||||||
|
AllowanceTime sql.NullInt64
|
||||||
|
QuotaKb sql.NullInt64
|
||||||
|
QuotaUsageKb sql.NullInt64
|
||||||
|
DNum int64
|
||||||
|
DSizeB int64
|
||||||
|
UlNum int64
|
||||||
|
UlSizeB int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetNamespaceByName(ctx context.Context, name string) (GetNamespaceByNameRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getNamespaceByName, name)
|
||||||
|
var i GetNamespaceByNameRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Lastseen,
|
||||||
|
&i.AllowanceTime,
|
||||||
|
&i.QuotaKb,
|
||||||
|
&i.QuotaUsageKb,
|
||||||
|
&i.DNum,
|
||||||
|
&i.DSizeB,
|
||||||
|
&i.UlNum,
|
||||||
|
&i.UlSizeB,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateFileStat = `-- name: UpdateFileStat :exec
|
||||||
|
UPDATE file_stats SET num = ?, size_b = ? where id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateFileStatParams struct {
|
||||||
|
Num int64
|
||||||
|
SizeB int64
|
||||||
|
ID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateFileStat(ctx context.Context, arg UpdateFileStatParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateFileStat, arg.Num, arg.SizeB, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateNamespace = `-- name: UpdateNamespace :one
|
||||||
|
UPDATE namespace SET
|
||||||
|
name = ?,
|
||||||
|
lastseen = ?,
|
||||||
|
allowance_time = ?,
|
||||||
|
quota_kb = ?,
|
||||||
|
quota_usage_kb = ?
|
||||||
|
WHERE id = ?
|
||||||
|
RETURNING download_id, upload_id
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateNamespaceParams struct {
|
||||||
|
Name string
|
||||||
|
Lastseen int64
|
||||||
|
AllowanceTime sql.NullInt64
|
||||||
|
QuotaKb sql.NullInt64
|
||||||
|
QuotaUsageKb sql.NullInt64
|
||||||
|
ID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateNamespaceRow struct {
|
||||||
|
DownloadID int64
|
||||||
|
UploadID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateNamespace(ctx context.Context, arg UpdateNamespaceParams) (UpdateNamespaceRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateNamespace,
|
||||||
|
arg.Name,
|
||||||
|
arg.Lastseen,
|
||||||
|
arg.AllowanceTime,
|
||||||
|
arg.QuotaKb,
|
||||||
|
arg.QuotaUsageKb,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i UpdateNamespaceRow
|
||||||
|
err := row.Scan(&i.DownloadID, &i.UploadID)
|
||||||
|
return i, err
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/dataswamp/namespace"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NamespaceRecord struct {
|
||||||
|
ID int64
|
||||||
|
Name string
|
||||||
|
LastSeen string
|
||||||
|
AllowanceSeconds int64
|
||||||
|
QuotaKB int64
|
||||||
|
QuotaUsedKB int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrUnparseableRecord = errors.New("record could not be mapped to entity")
|
||||||
|
|
||||||
|
func (r *NamespaceRecord) toEntity() (*namespace.Namespace, error) {
|
||||||
|
lastseen, err := time.Parse(time.RFC3339, r.LastSeen)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrUnparseableRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
var ns = new(namespace.Namespace)
|
||||||
|
ns.ID = r.ID
|
||||||
|
ns.Name = r.Name
|
||||||
|
ns.LastSeen = lastseen
|
||||||
|
ns.AllowanceDuration = time.Duration(r.AllowanceSeconds * int64(time.Second))
|
||||||
|
ns.FileQuota = namespace.FileSizeQuota{r.QuotaKB, r.QuotaUsedKB}
|
||||||
|
return ns, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromEntity(ns namespace.Namespace) (*NamespaceRecord, error) {
|
||||||
|
var record = new(NamespaceRecord)
|
||||||
|
record.ID = ns.ID
|
||||||
|
record.Name = ns.Name
|
||||||
|
record.LastSeen = ns.LastSeen.Format(time.RFC3339)
|
||||||
|
record.AllowanceSeconds = int64(ns.AllowanceDuration.Seconds())
|
||||||
|
record.QuotaKB = ns.FileQuota.AllowanceKB
|
||||||
|
record.QuotaUsedKB = ns.FileQuota.CurrentUsage
|
||||||
|
return record, nil
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/dataswamp/namespace"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = sqlite3.ErrError
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) migrate() error {
|
||||||
|
query := `
|
||||||
|
CREATE TABLE IF NOT EXISTS file_stats(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
num BIGINT NOT NULL,
|
||||||
|
size_b BIGINT NOT NULL
|
||||||
|
);`
|
||||||
|
_, err := r.db.Exec(query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query = `
|
||||||
|
CREATE TABLE IF NOT EXISTS namespace(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
lastseen TEXT,
|
||||||
|
allowance_time BIGINT,
|
||||||
|
quota_kb BIGINT,
|
||||||
|
quota_usage_kb BIGINT,
|
||||||
|
download_id BIGINT NOT NULL REFERENCES file_stats(Id),
|
||||||
|
upload_id BIGINT NOT NULL REFERENCES file_stats(Id)
|
||||||
|
);`
|
||||||
|
_, err = r.db.Exec(query)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(filename string) namespace.Repository {
|
||||||
|
db, err := sql.Open("sqlite3", filename)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := Repository{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.migrate()
|
||||||
|
return &repo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) createFileStats(ctx context.Context, fstat namespace.FileStat) (int64, error) {
|
||||||
|
return q.CreateFileStats(ctx, CreateFileStatsParams{fstat.Num, fstat.SizeB})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Create(ns namespace.Namespace) (*namespace.Namespace, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
q := New(r.db)
|
||||||
|
dl_id, err := q.createFileStats(ctx, ns.Download)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ul_id, err := q.createFileStats(ctx, ns.Upload)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ns.LastSeen = ns.LastSeen.Round(time.Microsecond)
|
||||||
|
|
||||||
|
p := CreateNamespaceParams{
|
||||||
|
Name: ns.Name,
|
||||||
|
Lastseen: ns.LastSeen.UnixMicro(),
|
||||||
|
AllowanceTime: sql.NullInt64{int64(ns.AllowanceDuration.Seconds()), true},
|
||||||
|
QuotaKb: sql.NullInt64{int64(ns.FileQuota.AllowanceKB), true},
|
||||||
|
QuotaUsageKb: sql.NullInt64{int64(ns.FileQuota.CurrentUsage), true},
|
||||||
|
DownloadID: dl_id,
|
||||||
|
UploadID: ul_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := q.CreateNamespace(ctx, p)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ns.ID = id
|
||||||
|
return &ns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) All() ([]namespace.Namespace, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
q := New(r.db)
|
||||||
|
rows, err := q.AllNamespaces(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var all []namespace.Namespace
|
||||||
|
for _, row := range rows {
|
||||||
|
ns := namespace.Namespace{
|
||||||
|
row.ID,
|
||||||
|
row.Name,
|
||||||
|
time.UnixMicro(row.Lastseen),
|
||||||
|
time.Duration(row.AllowanceTime.Int64 * int64(time.Second)),
|
||||||
|
namespace.FileSizeQuota{row.QuotaKb.Int64, row.QuotaUsageKb.Int64},
|
||||||
|
namespace.FileStat{row.DNum, row.DSizeB},
|
||||||
|
namespace.FileStat{row.UlNum, row.UlSizeB},
|
||||||
|
}
|
||||||
|
|
||||||
|
all = append(all, ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
return all, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetByName(name string) (*namespace.Namespace, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
q := New(r.db)
|
||||||
|
row, err := q.GetNamespaceByName(ctx, name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, namespace.ErrNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := namespace.Namespace{
|
||||||
|
row.ID,
|
||||||
|
row.Name,
|
||||||
|
time.UnixMicro(row.Lastseen),
|
||||||
|
time.Duration(row.AllowanceTime.Int64 * int64(time.Second)),
|
||||||
|
namespace.FileSizeQuota{row.QuotaKb.Int64, row.QuotaUsageKb.Int64},
|
||||||
|
namespace.FileStat{row.DNum, row.DSizeB},
|
||||||
|
namespace.FileStat{row.UlNum, row.UlSizeB},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) updateFileStat(ctx context.Context, id int64, fstat namespace.FileStat) error {
|
||||||
|
return q.UpdateFileStat(ctx, UpdateFileStatParams{fstat.Num, fstat.SizeB, id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Update(id int64, ns namespace.Namespace) (*namespace.Namespace, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
q := New(r.db)
|
||||||
|
|
||||||
|
ids, err := q.UpdateNamespace(ctx, UpdateNamespaceParams{
|
||||||
|
ns.Name,
|
||||||
|
ns.LastSeen.Round(time.Microsecond).UnixMicro(),
|
||||||
|
sql.NullInt64{int64(ns.AllowanceDuration.Seconds()), true},
|
||||||
|
sql.NullInt64{int64(ns.FileQuota.AllowanceKB), true},
|
||||||
|
sql.NullInt64{int64(ns.FileQuota.CurrentUsage), true},
|
||||||
|
ns.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
err = q.updateFileStat(ctx, ids.DownloadID, ns.Download)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = q.updateFileStat(ctx, ids.UploadID, ns.Upload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Delete(id int64) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
q := New(r.db)
|
||||||
|
|
||||||
|
err := q.DeleteNameSpace(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return namespace.ErrDeleteFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/dataswamp/namespace"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileRepo(t *testing.T) {
|
||||||
|
namespace.RepositoryContract(func() namespace.Repository { return NewRepository(":memory:") }, t)
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS file_stats(
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
num BIGINT NOT NULL,
|
||||||
|
size_b BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS namespace(
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
lastseen BIGINT NOT NULL,
|
||||||
|
allowance_time BIGINT,
|
||||||
|
quota_kb BIGINT,
|
||||||
|
quota_usage_kb BIGINT,
|
||||||
|
download_id BIGINT NOT NULL REFERENCES file_stats(Id) ON DELETE CASCADE,
|
||||||
|
upload_id BIGINT NOT NULL REFERENCES file_stats(Id) ON DELETE CASCADE
|
||||||
|
);
|
@ -0,0 +1,11 @@
|
|||||||
|
package system_time
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Clock struct{}
|
||||||
|
|
||||||
|
func (c Clock) Now() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
@ -1,94 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FileSystemBogFileData struct {
|
|
||||||
path string
|
|
||||||
size int64
|
|
||||||
mod_time time.Time
|
|
||||||
file *os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (f FileSystemBogFileData) Read(p []byte) (int, error) {
|
|
||||||
return f.file.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileSystemBogFileData) Write(p []byte) (int, error) {
|
|
||||||
return f.file.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileSystemBogFileData) Close() error {
|
|
||||||
return f.file.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileSystemBogFileData) Seek(offset int64, whence int) (int64, error) {
|
|
||||||
return f.file.Seek(offset, whence)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileSystemBogFileData) Path() string {
|
|
||||||
return f.path
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileSystemBogFileData) Size() int64 {
|
|
||||||
return f.size
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileSystemBogFileData) Modified() time.Time{
|
|
||||||
return time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileSystemBogRepository struct {
|
|
||||||
Root string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileSystemBogRepository) absPath(filename string, user_agent_label string) string {
|
|
||||||
return path.Join(f.Root, user_agent_label, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileSystemBogRepository) Create(filename string, user_agent_label string) (domain.BogInFile, error) {
|
|
||||||
abs_path := f.absPath(filename, user_agent_label)
|
|
||||||
|
|
||||||
dir := path.Dir(abs_path)
|
|
||||||
os.MkdirAll(dir, 0750)
|
|
||||||
file, err := os.OpenFile(abs_path, os.O_RDWR|os.O_CREATE, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stat_info, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bfd := FileSystemBogFileData {filename, stat_info.Size(), stat_info.ModTime(), file}
|
|
||||||
|
|
||||||
return bfd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileSystemBogRepository) Open(filename string, user_agent_label string) (domain.BogOutFile, error) {
|
|
||||||
abs_path := f.absPath(filename, user_agent_label)
|
|
||||||
|
|
||||||
dir := path.Dir(abs_path)
|
|
||||||
os.MkdirAll(dir, 0750)
|
|
||||||
file, err := os.OpenFile(abs_path, os.O_RDONLY, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, domain.ErrNotExists
|
|
||||||
}
|
|
||||||
|
|
||||||
bfd := FileSystemBogFileData {filename, 0, time.Now(), file}
|
|
||||||
|
|
||||||
return bfd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileSystemBogRepository) Delete(filename string, user_agent_label string) {
|
|
||||||
abs_path := f.absPath(filename, user_agent_label)
|
|
||||||
os.Remove(abs_path)
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserAgentDBRecord struct {
|
|
||||||
ID int64
|
|
||||||
Name string
|
|
||||||
LastSeen string
|
|
||||||
AllowanceSeconds int64
|
|
||||||
QuotaKB int64
|
|
||||||
QuotaUsedKB int64
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrUnparseableRecord = errors.New("record could not be mapped to entity")
|
|
||||||
|
|
||||||
func (r *UserAgentDBRecord) toEntity() (*domain.UserAgent, error) {
|
|
||||||
lastseen, err := time.Parse(time.RFC3339, r.LastSeen)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrUnparseableRecord
|
|
||||||
}
|
|
||||||
|
|
||||||
var useragent = new(domain.UserAgent)
|
|
||||||
useragent.ID = r.ID
|
|
||||||
useragent.Name = r.Name
|
|
||||||
useragent.LastSeen = lastseen
|
|
||||||
useragent.AllowanceDuration = time.Duration(r.AllowanceSeconds * int64(time.Second))
|
|
||||||
useragent.FileQuota = domain.FileSizeQuota { r.QuotaKB, r.QuotaUsedKB }
|
|
||||||
return useragent, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func fromEntity(useragent domain.UserAgent) (*UserAgentDBRecord, error) {
|
|
||||||
var record = new(UserAgentDBRecord)
|
|
||||||
record.ID = useragent.ID
|
|
||||||
record.Name = useragent.Name
|
|
||||||
record.LastSeen = useragent.LastSeen.Format(time.RFC3339)
|
|
||||||
record.AllowanceSeconds = int64(useragent.AllowanceDuration.Seconds())
|
|
||||||
record.QuotaKB = useragent.FileQuota.AllowanceKB
|
|
||||||
record.QuotaUsedKB = useragent.FileQuota.CurrentUsage
|
|
||||||
return record, nil
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SQLiteUserAgentRepository struct {
|
|
||||||
db *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSQLiteUserAgentRepository(filename string) *SQLiteUserAgentRepository {
|
|
||||||
db, err := sql.Open("sqlite3", filename)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
repo := SQLiteUserAgentRepository{
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
repo.migrate()
|
|
||||||
return &repo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r SQLiteUserAgentRepository) migrate() error {
|
|
||||||
query := `
|
|
||||||
CREATE TABLE IF NOT EXISTS useragent(
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL UNIQUE,
|
|
||||||
lastseen text,
|
|
||||||
allowance_time bigint,
|
|
||||||
quota_kb bigint,
|
|
||||||
quota_usage_kb bigint
|
|
||||||
);
|
|
||||||
`
|
|
||||||
_, err := r.db.Exec(query)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SQLiteUserAgentRepository) Create(useragent domain.UserAgent) (*domain.UserAgent, error) {
|
|
||||||
var record, err = fromEntity(useragent)
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := r.db.Exec(
|
|
||||||
"INSERT INTO useragent(name, lastseen, allowance_time, quota_kb, quota_usage_kb) values(?,?,?,?,?)",
|
|
||||||
record.Name, record.LastSeen, record.AllowanceSeconds, record.QuotaKB, record.QuotaUsedKB,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
var sqliteErr sqlite3.Error
|
|
||||||
if errors.As(err, &sqliteErr) {
|
|
||||||
if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintUnique) {
|
|
||||||
return nil, domain.ErrDuplicate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := res.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
useragent.ID = id
|
|
||||||
|
|
||||||
return &useragent, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r SQLiteUserAgentRepository) All() ([]domain.UserAgent, error) {
|
|
||||||
rows, err := r.db.Query("SELECT * FROM useragent")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var all []domain.UserAgent
|
|
||||||
for rows.Next() {
|
|
||||||
var record UserAgentDBRecord
|
|
||||||
if err := rows.Scan(&record.ID, &record.Name, &record.LastSeen, &record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var useragent, err = record.toEntity()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
all = append(all, *useragent)
|
|
||||||
}
|
|
||||||
return all, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r SQLiteUserAgentRepository) GetByName(name string) (*domain.UserAgent, error) {
|
|
||||||
row := r.db.QueryRow("SELECT id, name, lastseen, allowance_time, quota_kb, quota_usage_kb FROM useragent WHERE name = ?", name)
|
|
||||||
|
|
||||||
var record UserAgentDBRecord
|
|
||||||
if err := row.Scan(&record.ID, &record.Name, &record.LastSeen, &record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB); err != nil {
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
return nil, domain.ErrNotExists
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var useragent, err = record.toEntity()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return useragent, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r SQLiteUserAgentRepository) Update(id int64, updated domain.UserAgent) (*domain.UserAgent, error) {
|
|
||||||
if id == 0 {
|
|
||||||
return nil, errors.New("invalid updated ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
var record, err = fromEntity(updated)
|
|
||||||
|
|
||||||
res, err := r.db.Exec("UPDATE useragent SET name = ?, lastseen = ?, allowance_time = ?, quota_kb = ?, quota_usage_kb = ? WHERE id = ?",
|
|
||||||
record.Name, record.LastSeen, record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB, id)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rowsAffected, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if rowsAffected == 0 {
|
|
||||||
return nil, domain.ErrUpdateFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
return &updated, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r SQLiteUserAgentRepository) Delete(id int64) error {
|
|
||||||
res, err := r.db.Exec("DELETE FROM useragent WHERE id = ?", id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rowsAffected, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if rowsAffected == 0 {
|
|
||||||
return domain.ErrDeleteFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
@ -0,0 +1,163 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"caj-larsson/bog/dataswamp"
|
||||||
|
"caj-larsson/bog/dataswamp/namespace"
|
||||||
|
"caj-larsson/bog/dataswamp/swampfile"
|
||||||
|
fs_swampfile "caj-larsson/bog/infrastructure/fs/swampfile"
|
||||||
|
sql_namespace "caj-larsson/bog/infrastructure/sqlite/namespace"
|
||||||
|
"caj-larsson/bog/infrastructure/system_time"
|
||||||
|
"caj-larsson/bog/util"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Router interface {
|
||||||
|
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
|
||||||
|
ServeHTTP(http.ResponseWriter, *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bog struct {
|
||||||
|
router Router
|
||||||
|
adminRouter Router
|
||||||
|
file_service dataswamp.DataSwampService
|
||||||
|
address string
|
||||||
|
adminAddress string
|
||||||
|
logger util.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFileDataRepository(config FileConfig) swampfile.Repository {
|
||||||
|
r, err := fs_swampfile.NewRepository(config.Path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildNamespaceRepository(config DatabaseConfig) namespace.Repository {
|
||||||
|
if config.Backend != "sqlite" {
|
||||||
|
panic("Can only handle sqlite")
|
||||||
|
}
|
||||||
|
return sql_namespace.NewRepository(config.Connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bog) fileHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ref := swampfile.FileReference{r.URL.Path, r.Header["User-Agent"][0]}
|
||||||
|
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
swamp_file, err := b.file_service.OpenOutFile(ref)
|
||||||
|
|
||||||
|
if err == swampfile.ErrNotExists {
|
||||||
|
templ, err := template.ParseFiles("server/views/upload.html")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(404)
|
||||||
|
err = templ.Execute(w, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
b.logger.Info("Serving file '%s' of size %d from '%s'", ref.Path, swamp_file.Size(), ref.UserAgent)
|
||||||
|
w.Header().Set("Content-Disposition", "attachment")
|
||||||
|
http.ServeContent(w, r, swamp_file.Path(), swamp_file.Modified(), swamp_file)
|
||||||
|
case "POST":
|
||||||
|
fallthrough
|
||||||
|
case "PUT":
|
||||||
|
size_str := r.Header["Content-Length"][0]
|
||||||
|
|
||||||
|
size, err := strconv.ParseInt(size_str, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(422)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger.Info("Recieving file '%s' of size %d from '%s'", ref.Path, size, ref.UserAgent)
|
||||||
|
err = b.file_service.SaveFile(ref, r.Body, size)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bog) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
templ, err := template.ParseFiles("server/views/dashboard.html")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := b.file_service.NamespaceStats()
|
||||||
|
err = templ.Execute(w, stats)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bog) routes() {
|
||||||
|
b.router.HandleFunc("/", b.fileHandler)
|
||||||
|
// b.adminRouter.HandleFunc("/", b.dashboardHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bog) cleanNamespaces() {
|
||||||
|
for true {
|
||||||
|
b.file_service.CleanUpExpiredFiles()
|
||||||
|
time.Sleep(time.Minute * 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config *Configuration) *Bog {
|
||||||
|
b := new(Bog)
|
||||||
|
b.address = config.Server.bindAddress()
|
||||||
|
b.adminAddress = config.Admin.bindAddress()
|
||||||
|
|
||||||
|
fsSwampRepo := buildFileDataRepository(config.File)
|
||||||
|
nsRepo := buildNamespaceRepository(config.Database)
|
||||||
|
|
||||||
|
logger := ServerLogger{Debug}
|
||||||
|
ns_svc := namespace.NewNamespaceService(
|
||||||
|
nsRepo,
|
||||||
|
logger,
|
||||||
|
system_time.Clock{},
|
||||||
|
config.Quota.ParsedDuration(),
|
||||||
|
config.Quota.ParsedSizeBytes(),
|
||||||
|
)
|
||||||
|
|
||||||
|
b.file_service = *dataswamp.NewDataSwampService(
|
||||||
|
*ns_svc,
|
||||||
|
fsSwampRepo,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
b.logger = logger
|
||||||
|
b.router = new(http.ServeMux)
|
||||||
|
b.adminRouter = new(http.ServeMux)
|
||||||
|
b.routes()
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bog) Run() {
|
||||||
|
b.logger.Info("Starting bog on address: %s", b.address)
|
||||||
|
go b.cleanNamespaces()
|
||||||
|
go func() { http.ListenAndServe(b.adminAddress, b.adminRouter) }()
|
||||||
|
http.ListenAndServe(b.address, b.router)
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
// "fmt"
|
||||||
|
"caj-larsson/bog/dataswamp"
|
||||||
|
"caj-larsson/bog/dataswamp/namespace"
|
||||||
|
ns "caj-larsson/bog/infrastructure/memory/namespace"
|
||||||
|
"caj-larsson/bog/infrastructure/memory/swampfile"
|
||||||
|
"caj-larsson/bog/infrastructure/system_time"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestLogger struct{}
|
||||||
|
|
||||||
|
func (t TestLogger) Debug(format string, a ...interface{}) {}
|
||||||
|
func (t TestLogger) Info(format string, a ...interface{}) {}
|
||||||
|
func (t TestLogger) Warn(format string, a ...interface{}) {}
|
||||||
|
|
||||||
|
func TestApplication(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
logger := TestLogger{}
|
||||||
|
nsRepo := ns.NewRepository()
|
||||||
|
|
||||||
|
ns_svc := namespace.NewNamespaceService(
|
||||||
|
nsRepo,
|
||||||
|
logger,
|
||||||
|
system_time.Clock{},
|
||||||
|
time.Hour,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
file_service := dataswamp.NewDataSwampService(
|
||||||
|
*ns_svc,
|
||||||
|
swampfile.NewRepository(),
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
bog := Bog{
|
||||||
|
router: new(http.ServeMux),
|
||||||
|
file_service: *file_service,
|
||||||
|
address: "fake",
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
bog.routes()
|
||||||
|
req := httptest.NewRequest("POST", "/apath", strings.NewReader("testdata"))
|
||||||
|
req.Header.Add("User-Agent", "testingclient")
|
||||||
|
req.Header.Add("Content-Length", "8")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
bog.router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
is.Equal(w.Code, 200)
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfiguration(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
c, _ := ConfigFromToml(`
|
||||||
|
[server]
|
||||||
|
port = 8002
|
||||||
|
host = "127.0.0.1"
|
||||||
|
|
||||||
|
[admin]
|
||||||
|
port = 8001
|
||||||
|
host = "127.0.0.1"
|
||||||
|
|
||||||
|
[file]
|
||||||
|
path = "/tmp/datta2"
|
||||||
|
|
||||||
|
[database]
|
||||||
|
backend = "sqlite"
|
||||||
|
connection = "sql.db"
|
||||||
|
|
||||||
|
[quota]
|
||||||
|
default_size = "1MB"
|
||||||
|
default_duration = "72h"`,
|
||||||
|
)
|
||||||
|
|
||||||
|
is.Equal(c.Server.Port, int64(8002))
|
||||||
|
is.Equal(c.Server.Host, "127.0.0.1")
|
||||||
|
is.Equal(c.Quota.ParsedSizeBytes(), int64(1024*1024))
|
||||||
|
is.Equal(c.Quota.ParsedDuration(), time.Duration(time.Hour*72))
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Debug int = 0
|
||||||
|
Info = 1
|
||||||
|
Warn = 2
|
||||||
|
None = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerLogger struct {
|
||||||
|
level int
|
||||||
|
}
|
||||||
|
|
||||||
|
func logf(level string, format string, a ...interface{}) {
|
||||||
|
head := fmt.Sprintf("%s - [%s]: ", time.Now().Format(time.RFC3339), level)
|
||||||
|
fmt.Printf(head+format+"\n", a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t ServerLogger) Debug(format string, a ...interface{}) {
|
||||||
|
if t.level <= Debug {
|
||||||
|
logf("DEBUG", format, a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t ServerLogger) Info(format string, a ...interface{}) {
|
||||||
|
if t.level <= Info {
|
||||||
|
logf("INFO", format, a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t ServerLogger) Warn(format string, a ...interface{}) {
|
||||||
|
if t.level <= Warn {
|
||||||
|
logf("WARN", format, a...)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>bog</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Last Seen</th>
|
||||||
|
<th scope="col">Quota</th>
|
||||||
|
<th scope="col">Usage</th>
|
||||||
|
<th scope="col">Download</th>
|
||||||
|
<th scope="col">Upload</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range . }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ .Name }}</td>
|
||||||
|
<td>{{ .LastSeen }} - {{ .AllowanceDuration }}</td>
|
||||||
|
<td>{{ .FileQuota.CurrentUsage }}/{{ .FileQuota.AllowanceKB}} B</td>
|
||||||
|
<td>{{ .Usage.SizeB }}B - {{ .Usage.Num }} </td>
|
||||||
|
<td>{{ .Download.SizeB }}B - {{ .Download.Num }} </td>
|
||||||
|
<td>{{ .Upload.SizeB }}B - {{ .Upload.Num }} </td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>bog</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
function upload() {
|
||||||
|
var file = document.getElementById('file').files[0];
|
||||||
|
var ajax = new XMLHttpRequest;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
ajax.open('POST', '', true);
|
||||||
|
|
||||||
|
ajax.overrideMimeType('text/plain; charset=x-user-defined-binary');
|
||||||
|
reader.onload = function(evt) {
|
||||||
|
ajax.send(evt.target.result);
|
||||||
|
};
|
||||||
|
reader.readAsBinaryString(file);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<input id="file" type="file" />
|
||||||
|
<input type="button" onClick="upload()" value="Upload">
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,8 @@
|
|||||||
|
version: "1"
|
||||||
|
|
||||||
|
packages:
|
||||||
|
- path: infrastructure/sqlite/namespace
|
||||||
|
name: namespace
|
||||||
|
engine: postgresql
|
||||||
|
schema: infrastructure/sqlite/namespace/schema.sql
|
||||||
|
queries: infrastructure/sqlite/namespace/queries.sql
|
@ -1,94 +0,0 @@
|
|||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
"caj-larsson/bog/test/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
var file_ref1 = domain.FileReference { "path1", "ua1" }
|
|
||||||
var file_ref2 = domain.FileReference { "path1", "ua2" }
|
|
||||||
var file_ref3 = domain.FileReference { "path2", "ua1" }
|
|
||||||
|
|
||||||
|
|
||||||
func NewTestBogFileService() domain.BogFileService {
|
|
||||||
file_repo := mock.NewMockFileRepository()
|
|
||||||
ua_repo := mock.NewMockUserAgentRepository()
|
|
||||||
return domain.NewBogFileService(ua_repo, file_repo, 1024, time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileDontExist(t *testing.T) {
|
|
||||||
s := NewTestBogFileService()
|
|
||||||
outfile, err := s.OpenOutFile(file_ref1)
|
|
||||||
|
|
||||||
if outfile != nil && err != domain.ErrNotExists {
|
|
||||||
t.Errorf("File shall not exist by default")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileIsStored(t *testing.T) {
|
|
||||||
s := NewTestBogFileService()
|
|
||||||
|
|
||||||
fakefile := bytes.NewBufferString("My bog data")
|
|
||||||
|
|
||||||
err := s.SaveFile(file_ref1, fakefile, int64(fakefile.Len()))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("A small file should be writable %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
largefakefile := bytes.NewBufferString("")
|
|
||||||
|
|
||||||
for largefakefile.Len() < 64000 {
|
|
||||||
_, err = largefakefile.WriteString("A very repetitive file")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.SaveFile(file_ref3, largefakefile, int64(largefakefile.Len()))
|
|
||||||
|
|
||||||
if err != domain.ErrExceedQuota {
|
|
||||||
t.Errorf("too large files should not be excepted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func TestFileIsReadBack(t *testing.T) {
|
|
||||||
s := NewTestBogFileService()
|
|
||||||
|
|
||||||
infile := bytes.NewBufferString("My bog data")
|
|
||||||
|
|
||||||
_ = s.SaveFile(file_ref1, infile, int64(infile.Len()))
|
|
||||||
|
|
||||||
outbogfile, _ := s.OpenOutFile(file_ref1)
|
|
||||||
|
|
||||||
outfile := bytes.NewBufferString("")
|
|
||||||
_, _ = outfile.ReadFrom(outbogfile)
|
|
||||||
|
|
||||||
if outfile.String() != "My bog data" {
|
|
||||||
t.Errorf("file corrupted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func TestUAIsolation(t *testing.T) {
|
|
||||||
file_repo := mock.NewMockFileRepository()
|
|
||||||
ua_repo := mock.NewMockUserAgentRepository()
|
|
||||||
s := domain.NewBogFileService(ua_repo, file_repo, 1024, time.Hour)
|
|
||||||
|
|
||||||
ua1_file := bytes.NewBufferString("My bog data ua1")
|
|
||||||
ua2_file := bytes.NewBufferString("My bog data ua2")
|
|
||||||
|
|
||||||
_ = s.SaveFile(file_ref1, ua1_file, int64(ua1_file.Len()))
|
|
||||||
_ = s.SaveFile(file_ref2, ua2_file, int64(ua2_file.Len()))
|
|
||||||
|
|
||||||
outbogfile, _ := s.OpenOutFile(file_ref1)
|
|
||||||
|
|
||||||
outfile := bytes.NewBufferString("")
|
|
||||||
_, _ = outfile.ReadFrom(outbogfile)
|
|
||||||
|
|
||||||
if outfile.String() != "My bog data ua1" {
|
|
||||||
t.Errorf("file corrupted")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func TestQuota(t *testing.T) {
|
|
||||||
quota := domain.FileSizeQuota { 1000, 0 }
|
|
||||||
|
|
||||||
if !quota.Allows(1000) {
|
|
||||||
t.Errorf("It should allow filling completely")
|
|
||||||
}
|
|
||||||
if quota.Allows(1001) {
|
|
||||||
t.Errorf("It should not allow filling completely")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQuotaManipulation(t *testing.T) {
|
|
||||||
quota := domain.FileSizeQuota { 1000, 0 }
|
|
||||||
|
|
||||||
if quota.Add(500) != nil {
|
|
||||||
t.Errorf("It should allow adding")
|
|
||||||
}
|
|
||||||
|
|
||||||
if quota.CurrentUsage != 500 {
|
|
||||||
t.Errorf("It should add the usage correctly")
|
|
||||||
}
|
|
||||||
|
|
||||||
if quota.Add(500) != nil {
|
|
||||||
t.Errorf("It should allow adding up to the limit")
|
|
||||||
}
|
|
||||||
|
|
||||||
if quota.Add(1) != domain.ErrExceedQuota {
|
|
||||||
t.Errorf("It should not allow adding beyond limit")
|
|
||||||
}
|
|
||||||
|
|
||||||
if quota.CurrentUsage != 1000 {
|
|
||||||
t.Errorf("It should not overtaxed after failure to add")
|
|
||||||
}
|
|
||||||
|
|
||||||
if quota.Remove(1001) != domain.ErrQuotaInvalid {
|
|
||||||
t.Errorf("It should not allow reducing further than 0")
|
|
||||||
}
|
|
||||||
|
|
||||||
if quota.CurrentUsage != 1000 {
|
|
||||||
t.Errorf("It should not overtaxed after failure to remove")
|
|
||||||
}
|
|
||||||
|
|
||||||
if quota.Remove(1000) != nil {
|
|
||||||
t.Errorf("It should allow reducing to 0")
|
|
||||||
}
|
|
||||||
|
|
||||||
if quota.CurrentUsage != 0 {
|
|
||||||
t.Errorf("It should reduce accurately")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
"caj-larsson/bog/integration"
|
|
||||||
"caj-larsson/bog/test/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFsFileRepo(t *testing.T) {
|
|
||||||
|
|
||||||
var fac = func()domain.FileDataRepository {
|
|
||||||
r := t.TempDir()
|
|
||||||
d := path.Join(r, "fs")
|
|
||||||
repo := integration.FileSystemBogRepository { d }
|
|
||||||
return &repo
|
|
||||||
}
|
|
||||||
|
|
||||||
mock.BogFileRepositoryContract(fac, t)
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
"path"
|
|
||||||
"os"
|
|
||||||
// "io"
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
type MockBogFile struct {
|
|
||||||
filename string
|
|
||||||
file afero.File
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f MockBogFile) Path() string {
|
|
||||||
return f.filename
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f MockBogFile) Size() int64 {
|
|
||||||
stat, _ := f.file.Stat()
|
|
||||||
return int64(stat.Size())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f MockBogFile) Read(p []byte) (int, error) {
|
|
||||||
return f.file.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f MockBogFile) Write(p []byte) (int, error) {
|
|
||||||
return f.file.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f MockBogFile) Close() error {
|
|
||||||
return f.file.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f MockBogFile) Seek(offset int64, whence int) (int64, error) {
|
|
||||||
return f.file.Seek(offset, whence)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f MockBogFile) Modified() time.Time {
|
|
||||||
stat, _ := f.file.Stat()
|
|
||||||
return stat.ModTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
// The actual repository
|
|
||||||
type MockFileRepository struct {
|
|
||||||
fs afero.Fs
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMockFileRepository() domain.FileDataRepository {
|
|
||||||
return MockFileRepository { afero.NewMemMapFs() }
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MockFileRepository) Create(filename string, user_agent_label string) (domain.BogInFile, error) {
|
|
||||||
abs_path := path.Join( filename, user_agent_label)
|
|
||||||
dir := path.Dir(abs_path)
|
|
||||||
r.fs.MkdirAll(dir, 0750)
|
|
||||||
file, err := r.fs.OpenFile(abs_path, os.O_RDWR|os.O_CREATE, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bf := MockBogFile {filename, file}
|
|
||||||
|
|
||||||
return bf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MockFileRepository) Open(filename string, user_agent_label string) (domain.BogOutFile, error) {
|
|
||||||
abs_path := path.Join(filename, user_agent_label)
|
|
||||||
|
|
||||||
dir := path.Dir(abs_path)
|
|
||||||
r.fs.MkdirAll(dir, 0750)
|
|
||||||
file, err := r.fs.OpenFile(abs_path, os.O_RDONLY, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, domain.ErrNotExists
|
|
||||||
}
|
|
||||||
|
|
||||||
bf := MockBogFile {filename, file}
|
|
||||||
return bf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MockFileRepository) Delete(filename string, user_agent_label string) {
|
|
||||||
abs_path := path.Join(filename, user_agent_label)
|
|
||||||
r.fs.Remove(abs_path)
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func BogFileRepositoryContract(fac func() domain.FileDataRepository, t *testing.T) {
|
|
||||||
basicFileOperationContract(fac, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func basicFileOperationContract(fac func() domain.FileDataRepository, t *testing.T) {
|
|
||||||
repo := fac()
|
|
||||||
|
|
||||||
not_file, err := repo.Open("doesnot", "exist")
|
|
||||||
|
|
||||||
if err != domain.ErrNotExists || not_file != nil{
|
|
||||||
t.Errorf("Must raise not exists and file must not open")
|
|
||||||
}
|
|
||||||
|
|
||||||
new_file, err := repo.Create("newfile.new", "ua1")
|
|
||||||
|
|
||||||
if err != nil || new_file == nil {
|
|
||||||
t.Errorf("Create ")
|
|
||||||
}
|
|
||||||
|
|
||||||
var testdata = "testingdata"
|
|
||||||
|
|
||||||
new_file.Write([]byte(testdata))
|
|
||||||
new_file.Close()
|
|
||||||
|
|
||||||
reopened_file, err := repo.Open("newfile.new", "ua1")
|
|
||||||
|
|
||||||
if err != nil || reopened_file == nil{
|
|
||||||
t.Errorf("Must open existing files")
|
|
||||||
}
|
|
||||||
|
|
||||||
readback := make([]byte, 128)
|
|
||||||
|
|
||||||
size, err := reopened_file.Read(readback)
|
|
||||||
|
|
||||||
if string(readback[0:size]) != testdata {
|
|
||||||
t.Errorf("Must contain previously stored data '%s' '%s'", string(readback), testdata)
|
|
||||||
}
|
|
||||||
|
|
||||||
reopened_file.Close()
|
|
||||||
|
|
||||||
repo.Delete("newfile.new", "ua1")
|
|
||||||
|
|
||||||
deleted_file, err := repo.Open("newfile.new", "ua1")
|
|
||||||
|
|
||||||
if err != domain.ErrNotExists || deleted_file != nil{
|
|
||||||
t.Errorf("Musn't open deleted files")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
//"caj-larsson/bog/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func TestMockFileRepo(t *testing.T) {
|
|
||||||
BogFileRepositoryContract(NewMockFileRepository, t)
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
// "time"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
type MockUserAgentRepository struct {
|
|
||||||
IdIdx map[int64]*domain.UserAgent
|
|
||||||
NameIdx map[string]*domain.UserAgent
|
|
||||||
NextId int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMockUserAgentRepository() *MockUserAgentRepository {
|
|
||||||
r := new(MockUserAgentRepository)
|
|
||||||
r.NextId = 0
|
|
||||||
r.IdIdx = make(map[int64]*domain.UserAgent)
|
|
||||||
r.NameIdx = make(map[string]*domain.UserAgent)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MockUserAgentRepository) Create(useragent domain.UserAgent) (*domain.UserAgent, error) {
|
|
||||||
r.NextId += 1
|
|
||||||
useragent.ID = r.NextId
|
|
||||||
|
|
||||||
r.IdIdx[useragent.ID] = &useragent
|
|
||||||
r.NameIdx[useragent.Name] = &useragent
|
|
||||||
return &useragent, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (r *MockUserAgentRepository) All() ([]domain.UserAgent, error) {
|
|
||||||
v := make([]domain.UserAgent, 0, len(r.IdIdx))
|
|
||||||
|
|
||||||
for _, value := range r.IdIdx {
|
|
||||||
v = append(v, *value)
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (r *MockUserAgentRepository) GetByName(name string) (*domain.UserAgent, error) {
|
|
||||||
useragent, exists := r.NameIdx[name]
|
|
||||||
if exists {
|
|
||||||
return useragent, nil
|
|
||||||
}
|
|
||||||
return nil, domain.ErrNotExists
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (r *MockUserAgentRepository) Update(id int64, useragent domain.UserAgent) (*domain.UserAgent, error) {
|
|
||||||
original := *r.IdIdx[id]
|
|
||||||
useragent.ID = id
|
|
||||||
r.IdIdx[id] = &useragent
|
|
||||||
r.NameIdx[original.Name] = &useragent
|
|
||||||
return &useragent, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (r *MockUserAgentRepository) Delete(id int64) error {
|
|
||||||
original := *r.IdIdx[id]
|
|
||||||
delete(r.NameIdx, original.Name)
|
|
||||||
delete(r.IdIdx, original.ID)
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
"caj-larsson/bog/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMockUserAgentRepo(t *testing.T) {
|
|
||||||
r := NewMockUserAgentRepository()
|
|
||||||
|
|
||||||
all, err := r.All()
|
|
||||||
|
|
||||||
if len(all) != 0 && err != nil {
|
|
||||||
t.Errorf("New repo should be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
ua := domain.UserAgent {23, "n1", time.Now(), time.Duration(time.Hour * 3), domain.FileSizeQuota {1000, 0} }
|
|
||||||
|
|
||||||
|
|
||||||
ua1, _ := r.Create(ua)
|
|
||||||
ua.Name = "n2"
|
|
||||||
ua2, _ := r.Create(ua)
|
|
||||||
if ua1 == ua2 {
|
|
||||||
t.Errorf("Must create unique items")
|
|
||||||
}
|
|
||||||
|
|
||||||
all, err = r.All()
|
|
||||||
|
|
||||||
if len(all) != 2 && err != nil {
|
|
||||||
t.Errorf("After adding there should be two Useragent")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ua.ID != 23 {
|
|
||||||
t.Errorf("It does not change the original UserAgent")
|
|
||||||
}
|
|
||||||
|
|
||||||
ua3, _ := r.GetByName("n2")
|
|
||||||
|
|
||||||
if ua3 != ua2 {
|
|
||||||
t.Errorf("It the correct ua is acquired")
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Delete(ua2.ID) != nil {
|
|
||||||
t.Errorf("Must delete without error")
|
|
||||||
}
|
|
||||||
|
|
||||||
all, err = r.All()
|
|
||||||
if len(all) != 1 && err != nil {
|
|
||||||
t.Errorf("After deleting one there should be one UA ")
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,41 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
eventName string
|
||||||
|
payload interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEvent(eventName string, payload interface{}) *Event {
|
||||||
|
return &Event{
|
||||||
|
eventName,
|
||||||
|
payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Event) EventName() string {
|
||||||
|
return e.eventName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Event) Payload() interface{} {
|
||||||
|
return e.payload
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventHandler func(payload interface{})
|
||||||
|
|
||||||
|
type EventBus struct {
|
||||||
|
handlers map[string][]EventHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEventBus() *EventBus {
|
||||||
|
return &EventBus{make(map[string][]EventHandler)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBus) Register(eventName string, handler EventHandler) {
|
||||||
|
eb.handlers[eventName] = append(eb.handlers[eventName], handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBus) Handle(e Event) {
|
||||||
|
for _, handler := range eb.handlers[e.EventName()] {
|
||||||
|
handler(e.Payload())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (eb *EventBus) Handled(e Event) bool {
|
||||||
|
// TODO: figure out how to verify the event signature here.
|
||||||
|
handlers, exists := eb.handlers[e.EventName()]
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(handlers) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func AcceptsMessage(t *testing.T, eb *EventBus, es []Event) {
|
||||||
|
is := is.New(t)
|
||||||
|
for _, e := range es {
|
||||||
|
is.True(eb.Handled(e))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Debug(format string, a ...interface{})
|
||||||
|
Info(format string, a ...interface{})
|
||||||
|
Warn(format string, a ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestLogger struct{}
|
||||||
|
|
||||||
|
func (t TestLogger) Debug(format string, a ...interface{}) {}
|
||||||
|
func (t TestLogger) Info(format string, a ...interface{}) {}
|
||||||
|
func (t TestLogger) Warn(format string, a ...interface{}) {}
|
Loading…
Reference in New Issue