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