Complete restructure of domain and infra layer

master
Caj Larsson 3 years ago
parent a70e8ef74b
commit 8a0e6b80e3

2
.gitignore vendored

@ -0,0 +1,2 @@
sql.db
bog

@ -10,21 +10,3 @@ Don't worry about access credentials, the datasawmp does authorization
without authentication the old school way: encryption. Pass a password
when you create your data and if you pass the same when you retrieve
it, you get the same bits back.
## TODO
Alpha
- [x] Concurrent access safety
- [x] Test domain
- [x] Test integration
- [ ] Test application
Beta
- [ ] Path Sanitation and rejection
- [ ] Usage statistics
- [ ] Clean up background process
1.0
- [ ] Rendered Dashboard
- [ ] Upload page helper for 404

@ -1,39 +0,0 @@
package application
import (
"fmt"
"net/http/httptest"
"net/http"
"testing"
"strings"
"time"
"caj-larsson/bog/domain"
"caj-larsson/bog/test/mock"
)
func TestApplication(t *testing.T) {
file_service := domain.NewBogFileService(
mock.NewMockUserAgentRepository(),
mock.NewMockFileRepository(),
1000,
time.Hour,
)
bog := Bog {
router: new(http.ServeMux),
file_service: file_service,
address: "fake",
}
bog.routes()
req := httptest.NewRequest("POST", "/apath", strings.NewReader("testdata"))
req.Header.Add("User-Agent", "testingclient")
w := httptest.NewRecorder()
bog.router.ServeHTTP(w, req)
if (w.Code != 200){
fmt.Printf("%v", w)
t.Error("not ok")
}
}

@ -1,11 +1,11 @@
package domain
package namespace
import (
"time"
"errors"
)
type UserAgent struct {
type Namespace struct {
ID int64
Name string
LastSeen time.Time
@ -13,13 +13,6 @@ type UserAgent struct {
FileQuota FileSizeQuota
}
type BogFile struct {
UserAgentId int64
Path string
Size int64
CreatedAt time.Time
}
var (
ErrDuplicate = errors.New("record already exists")
ErrExceedQuota = errors.New("file too large")

@ -0,0 +1,15 @@
package namespace
import "errors"
var (
ErrNoNamespace = errors.New("that namespace does not exist")
)
type Repository interface{
Create(namespace Namespace) (*Namespace, error)
All() ([]Namespace, error)
GetByName(name string) (*Namespace, error)
Update(id int64, namespace Namespace) (*Namespace, error)
Delete(id int64) error
}

@ -1,9 +1,5 @@
package domain
package namespace
import (
"io"
"time"
)
type FileSizeQuota struct {
AllowanceKB int64
@ -33,26 +29,3 @@ func (f *FileSizeQuota) Remove(size int64) error {
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,13 +1,12 @@
package test
package namespace
import (
"testing"
"caj-larsson/bog/domain"
)
func TestQuota(t *testing.T) {
quota := domain.FileSizeQuota { 1000, 0 }
quota := FileSizeQuota { 1000, 0 }
if !quota.Allows(1000) {
t.Errorf("It should allow filling completely")
@ -18,7 +17,7 @@ func TestQuota(t *testing.T) {
}
func TestQuotaManipulation(t *testing.T) {
quota := domain.FileSizeQuota { 1000, 0 }
quota := FileSizeQuota { 1000, 0 }
if quota.Add(500) != nil {
t.Errorf("It should allow adding")
@ -32,7 +31,7 @@ func TestQuotaManipulation(t *testing.T) {
t.Errorf("It should allow adding up to the limit")
}
if quota.Add(1) != domain.ErrExceedQuota {
if quota.Add(1) != ErrExceedQuota {
t.Errorf("It should not allow adding beyond limit")
}
@ -40,7 +39,7 @@ func TestQuotaManipulation(t *testing.T) {
t.Errorf("It should not overtaxed after failure to add")
}
if quota.Remove(1001) != domain.ErrQuotaInvalid {
if quota.Remove(1001) != ErrQuotaInvalid {
t.Errorf("It should not allow reducing further than 0")
}

@ -0,0 +1,97 @@
package dataswamp
import (
"io"
"time"
"strconv"
"strings"
"path"
"path/filepath"
"caj-larsson/bog/dataswamp/namespace"
"caj-larsson/bog/dataswamp/swampfile"
)
type SwampFileService struct {
namespace_repo namespace.Repository
swamp_file_repo swampfile.Repository
default_allowance_bytes int64
default_allowance_duration time.Duration
}
func NewSwampFileService(
namespace_repo namespace.Repository,
swamp_file_repo swampfile.Repository,
da_bytes int64,
da_duration time.Duration,
) SwampFileService {
return SwampFileService {namespace_repo, swamp_file_repo, da_bytes, da_duration}
}
func (s SwampFileService) getOrCreateNs(namespace_in string) *namespace.Namespace{
ns, err := s.namespace_repo.GetByName(namespace_in)
if err == namespace.ErrNotExists {
new_ns := namespace.Namespace {
0,
namespace_in,
time.Now(),
s.default_allowance_duration,
namespace.FileSizeQuota { s.default_allowance_bytes, 0 },
}
created_ns, err := s.namespace_repo.Create(new_ns)
if err != nil {
panic(err)
}
return created_ns
}
if err != nil {
panic(err)
}
return ns
}
func (s SwampFileService) SaveFile(ref swampfile.FileReference, src io.Reader, size int64) error {
ns := s.getOrCreateNs(ref.UserAgent)
if !ns.FileQuota.Allows(size) {
return namespace.ErrExceedQuota
}
f, err := s.swamp_file_repo.Create(ref.Path, strconv.FormatInt(ns.ID, 10))
if err != nil {
return err
}
io.Copy(f, src)
f.Close()
ns.FileQuota.Add(size)
s.namespace_repo.Update(ns.ID, *ns)
return nil
}
func (s SwampFileService) OpenOutFile(ref swampfile.FileReference) (swampfile.SwampOutFile, error) {
ns, err := s.namespace_repo.GetByName(ref.UserAgent)
if err == namespace.ErrNotExists {
return nil, err
}
f, err := s.swamp_file_repo.Open(ref.Path, strconv.FormatInt(ns.ID, 10))
if err != nil {
return nil, err
}
return f, nil
}
func CleanPath(inpath string) string {
return filepath.FromSlash(path.Clean("/" + strings.Trim(inpath, "/")))
}

@ -0,0 +1,102 @@
package dataswamp
import (
"time"
"bytes"
"testing"
"github.com/matryer/is"
"caj-larsson/bog/dataswamp/swampfile"
"caj-larsson/bog/dataswamp/namespace"
m_namespace "caj-larsson/bog/infrastructure/memory/namespace"
m_swampfile "caj-larsson/bog/infrastructure/memory/swampfile"
)
var file_ref1 = swampfile.FileReference { "ptah1", "ua1" }
var file_ref2 = swampfile.FileReference { "path1", "ua2" }
var file_ref3 = swampfile.FileReference { "path2", "ua1" }
func NewTestSwampFileService() SwampFileService {
file_repo := m_swampfile.NewRepository()
ns_repo := m_namespace.NewRepository()
return NewSwampFileService(ns_repo, file_repo, 1024, time.Hour)
}
func TestFileDontExist(t *testing.T) {
s := NewTestSwampFileService()
outfile, err := s.OpenOutFile(file_ref1)
if outfile != nil && err != swampfile.ErrNotExists {
t.Errorf("File shall not exist by default")
}
}
func TestFileIsStored(t *testing.T) {
s := NewTestSwampFileService()
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 != namespace.ErrExceedQuota {
t.Errorf("too large files should not be excepted")
}
}
func TestFileIsReadBack(t *testing.T) {
s := NewTestSwampFileService()
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)
if outfile.String() != "My bog data" {
t.Errorf("file corrupted")
}
}
func TestUAIsolation(t *testing.T) {
s := NewTestSwampFileService()
ns1_file := bytes.NewBufferString("My bog data ua1")
ns2_file := bytes.NewBufferString("My bog data ua2")
_ = 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)
if outfile.String() != "My bog data ua1" {
t.Errorf("file corrupted")
}
}
func TestCleanPath(t *testing.T) {
is := is.New(t)
is.Equal(CleanPath("/"), "/")
}

@ -0,0 +1,22 @@
package swampfile
import (
"time"
"errors"
)
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")
ErrNotExists = errors.New("row not exists")
ErrUpdateFailed = errors.New("update failed")
ErrDeleteFailed = errors.New("delete failed")
)

@ -1,22 +1,21 @@
package mock
package swampfile
import (
"testing"
"caj-larsson/bog/domain"
)
func BogFileRepositoryContract(fac func() domain.FileDataRepository, t *testing.T) {
func RepositoryContract(fac func() Repository, t *testing.T) {
basicFileOperationContract(fac, t)
}
func basicFileOperationContract(fac func() domain.FileDataRepository, t *testing.T) {
func basicFileOperationContract(fac func() Repository, t *testing.T) {
repo := fac()
not_file, err := repo.Open("doesnot", "exist")
if err != domain.ErrNotExists || not_file != nil{
if err != ErrNotExists || not_file != nil{
t.Errorf("Must raise not exists and file must not open")
}
@ -51,7 +50,7 @@ func basicFileOperationContract(fac func() domain.FileDataRepository, t *testing
deleted_file, err := repo.Open("newfile.new", "ua1")
if err != domain.ErrNotExists || deleted_file != nil{
if err != ErrNotExists || deleted_file != nil{
t.Errorf("Musn't open deleted files")
}
}

@ -0,0 +1,7 @@
package swampfile
type Repository interface {
Create(filename string, user_agent_label string) (SwampInFile, error)
Open(filename string, user_agent_label string) (SwampOutFile, error)
Delete(filename string, user_agent_label string)
}

@ -0,0 +1,29 @@
package swampfile
import (
"io"
"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
}

@ -1,21 +0,0 @@
package domain
import "errors"
var (
ErrNoUserAgent = errors.New("that useragent does not exist")
)
type UserAgentRepository interface{
Create(useragent UserAgent) (*UserAgent, error)
All() ([]UserAgent, error)
GetByName(name string) (*UserAgent, error)
Update(id int64, useragent UserAgent) (*UserAgent, error)
Delete(id int64) error
}
type FileDataRepository interface {
Create(filename string, user_agent_label string) (BogInFile, error)
Open(filename string, user_agent_label string) (BogOutFile, error)
Delete(filename string, user_agent_label string)
}

@ -1,83 +0,0 @@
package domain
import (
"io"
"time"
"strconv"
)
type BogFileService struct {
user_agent_repo UserAgentRepository
file_data_repo FileDataRepository
default_allowance_bytes int64
default_allowance_duration time.Duration
}
func NewBogFileService(
user_agent_repo UserAgentRepository,
file_data_repo FileDataRepository,
da_bytes int64,
da_duration time.Duration,
) BogFileService {
return BogFileService {user_agent_repo, file_data_repo, da_bytes, da_duration}
}
func (b BogFileService) getOrCreateUA(useragent_in string) *UserAgent{
ua, err := b.user_agent_repo.GetByName(useragent_in)
if err == ErrNotExists {
new_ua := UserAgent {
0, useragent_in, time.Now(), b.default_allowance_duration, FileSizeQuota { b.default_allowance_bytes, 0 },
}
created_ua, err := b.user_agent_repo.Create(new_ua)
if err != nil {
panic(err)
}
return created_ua
}
if err != nil {
panic(err)
}
return ua
}
func (b BogFileService) SaveFile(ref FileReference, src io.Reader, size int64) error {
user_agent := b.getOrCreateUA(ref.UserAgent)
if !user_agent.FileQuota.Allows(size) {
return ErrExceedQuota
}
f, err := b.file_data_repo.Create(ref.Path, strconv.FormatInt(user_agent.ID, 10))
if err != nil {
return err
}
io.Copy(f, src)
f.Close()
user_agent.FileQuota.Add(size)
b.user_agent_repo.Update(user_agent.ID, *user_agent)
return nil
}
func (b BogFileService) OpenOutFile(ref FileReference) (BogOutFile, error) {
user_agent, err := b.user_agent_repo.GetByName(ref.UserAgent)
if err == ErrNotExists {
return nil, err
}
f, err := b.file_data_repo.Open(ref.Path, strconv.FormatInt(user_agent.ID, 10))
if err != nil {
return nil, err
}
return f, nil
}

@ -5,6 +5,7 @@ go 1.18
require (
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 // indirect
github.com/matryer/is v1.4.0 // indirect
github.com/mattn/go-sqlite3 v1.14.12 // indirect
github.com/spf13/afero v1.8.2 // indirect
golang.org/x/text v0.3.4 // indirect

@ -125,6 +125,8 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

@ -0,0 +1,93 @@
package swampfile
import (
"time"
"os"
"path"
"caj-larsson/bog/dataswamp/swampfile"
)
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{
return time.Now()
}
type Repository struct {
Root string
}
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)
}
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
}
bfd := FileSystemSwampFileData {filename, 0, 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)
}

@ -0,0 +1,19 @@
package swampfile
import (
"path"
"testing"
"caj-larsson/bog/dataswamp/swampfile"
)
func TestFsFileRepo(t *testing.T) {
var fac = func() swampfile.Repository {
r := t.TempDir()
d := path.Join(r, "fs")
repo := Repository { d }
return &repo
}
swampfile.RepositoryContract(fac, t)
}

@ -0,0 +1,66 @@
package memory
import (
// "time"
"caj-larsson/bog/dataswamp/namespace"
)
type Repository struct {
IdIdx map[int64] *namespace.Namespace
NameIdx map[string] *namespace.Namespace
NextId int64
}
func NewRepository() *Repository {
r := new(Repository)
r.NextId = 0
r.IdIdx = make(map[int64]*namespace.Namespace)
r.NameIdx = make(map[string]*namespace.Namespace)
return r
}
func (r *Repository) Create(ns namespace.Namespace) (*namespace.Namespace, error) {
r.NextId += 1
ns.ID = r.NextId
r.IdIdx[ns.ID] = &ns
r.NameIdx[ns.Name] = &ns
return &ns, nil
}
func (r *Repository) All() ([]namespace.Namespace, error) {
ns := make([]namespace.Namespace, 0, len(r.IdIdx))
for _, value := range r.IdIdx {
ns = append(ns, *value)
}
return ns, nil
}
func (r *Repository) GetByName(name string) (*namespace.Namespace, error) {
ns, exists := r.NameIdx[name]
if exists {
return ns, nil
}
return nil, namespace.ErrNotExists
}
func (r *Repository) Update(id int64, ns namespace.Namespace) (*namespace.Namespace, error) {
original := *r.IdIdx[id]
ns.ID = id
r.IdIdx[id] = &ns
r.NameIdx[original.Name] = &ns
return &ns, nil
}
func (r *Repository) Delete(id int64) error {
original := *r.IdIdx[id]
delete(r.NameIdx, original.Name)
delete(r.IdIdx, original.ID)
return nil
}

@ -0,0 +1,52 @@
package memory
import (
"testing"
"time"
"caj-larsson/bog/dataswamp/namespace"
)
func TestUserAgentRepo(t *testing.T) {
r := NewRepository()
all, err := r.All()
if len(all) != 0 && err != nil {
t.Errorf("New repo should be empty")
}
ns := namespace.Namespace {23, "n1", time.Now(), time.Duration(time.Hour * 3), namespace.FileSizeQuota {1000, 0} }
ns1, _ := r.Create(ns)
ns.Name = "n2"
ns2, _ := r.Create(ns)
if ns1 == ns2 {
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 ns.ID != 23 {
t.Errorf("It does not change the original UserAgent")
}
ns3, _ := r.GetByName("n2")
if ns3 != ns2 {
t.Errorf("It the correct ns is acquired")
}
if r.Delete(ns2.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 NS ")
}
}

@ -0,0 +1,90 @@
package swampfile
import (
"time"
"path"
"os"
// "io"
"github.com/spf13/afero"
"caj-larsson/bog/dataswamp/swampfile"
)
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()
}
// 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(filename, namespace_stub)
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(filename, namespace_stub)
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(filename, namespace_stub)
r.fs.Remove(abs_path)
}

@ -0,0 +1,11 @@
package swampfile
import (
"testing"
"caj-larsson/bog/dataswamp/swampfile"
)
func TestFileRepo(t *testing.T) {
swampfile.RepositoryContract(NewRepository, t)
}

@ -0,0 +1,45 @@
package namespace
import (
"errors"
"time"
"caj-larsson/bog/dataswamp/namespace"
)
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,31 +1,31 @@
package integration
package namespace
import (
"caj-larsson/bog/domain"
"caj-larsson/bog/dataswamp/namespace"
"database/sql"
"errors"
"github.com/mattn/go-sqlite3"
)
type SQLiteUserAgentRepository struct {
type Repository struct {
db *sql.DB
}
func NewSQLiteUserAgentRepository(filename string) *SQLiteUserAgentRepository {
func NewRepository(filename string) *Repository {
db, err := sql.Open("sqlite3", filename)
if err != nil {
panic(err)
}
repo := SQLiteUserAgentRepository{
repo := Repository{
db: db,
}
repo.migrate()
return &repo
}
func (r SQLiteUserAgentRepository) migrate() error {
func (r Repository) migrate() error {
query := `
CREATE TABLE IF NOT EXISTS useragent(
CREATE TABLE IF NOT EXISTS namespace(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
lastseen text,
@ -38,21 +38,21 @@ func (r SQLiteUserAgentRepository) migrate() error {
return err
}
func (r *SQLiteUserAgentRepository) Create(useragent domain.UserAgent) (*domain.UserAgent, error) {
var record, err = fromEntity(useragent)
func (r *Repository) Create(ns namespace.Namespace) (*namespace.Namespace, error) {
var record, err = fromEntity(ns)
if err != nil {
}
res, err := r.db.Exec(
"INSERT INTO useragent(name, lastseen, allowance_time, quota_kb, quota_usage_kb) values(?,?,?,?,?)",
"INSERT INTO namespace(name, lastseen, allowance_time, quota_kb, quota_usage_kb) values(?,?,?,?,?)",
record.Name, record.LastSeen, record.AllowanceSeconds, record.QuotaKB, record.QuotaUsedKB,
)
if err != nil {
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) {
if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintUnique) {
return nil, domain.ErrDuplicate
return nil, namespace.ErrDuplicate
}
}
return nil, err
@ -62,61 +62,61 @@ func (r *SQLiteUserAgentRepository) Create(useragent domain.UserAgent) (*domain.
if err != nil {
return nil, err
}
useragent.ID = id
ns.ID = id
return &useragent, nil
return &ns, nil
}
func (r SQLiteUserAgentRepository) All() ([]domain.UserAgent, error) {
rows, err := r.db.Query("SELECT * FROM useragent")
func (r Repository) All() ([]namespace.Namespace, error) {
rows, err := r.db.Query("SELECT * FROM namespace")
if err != nil {
return nil, err
}
defer rows.Close()
var all []domain.UserAgent
var all []namespace.Namespace
for rows.Next() {
var record UserAgentDBRecord
var record NamespaceRecord
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()
var ns, err = record.toEntity()
if err != nil {
return nil, err
}
all = append(all, *useragent)
all = append(all, *ns)
}
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)
func (r Repository) GetByName(name string) (*namespace.Namespace, error) {
row := r.db.QueryRow("SELECT id, name, lastseen, allowance_time, quota_kb, quota_usage_kb FROM namespace WHERE name = ?", name)
var record UserAgentDBRecord
var record NamespaceRecord
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, namespace.ErrNotExists
}
return nil, err
}
var useragent, err = record.toEntity()
var ns, err = record.toEntity()
if err != nil {
return nil, err
}
return useragent, nil
return ns, nil
}
func (r SQLiteUserAgentRepository) Update(id int64, updated domain.UserAgent) (*domain.UserAgent, error) {
func (r Repository) Update(id int64, updated namespace.Namespace) (*namespace.Namespace, 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 = ?",
res, err := r.db.Exec("UPDATE namespace 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 {
@ -129,14 +129,14 @@ func (r SQLiteUserAgentRepository) Update(id int64, updated domain.UserAgent) (*
}
if rowsAffected == 0 {
return nil, domain.ErrUpdateFailed
return nil, namespace.ErrUpdateFailed
}
return &updated, nil
}
func (r SQLiteUserAgentRepository) Delete(id int64) error {
res, err := r.db.Exec("DELETE FROM useragent WHERE id = ?", id)
func (r Repository) Delete(id int64) error {
res, err := r.db.Exec("DELETE FROM namespace WHERE id = ?", id)
if err != nil {
return err
}
@ -147,7 +147,7 @@ func (r SQLiteUserAgentRepository) Delete(id int64) error {
}
if rowsAffected == 0 {
return domain.ErrDeleteFailed
return namespace.ErrDeleteFailed
}
return err

@ -1,94 +0,0 @@
package integration
import (
"time"
"os"
"path"
"caj-larsson/bog/domain"
)
type FileSystemBogFileData struct {
path string
size int64
mod_time time.Time
file *os.File
}
func (f FileSystemBogFileData) Read(p []byte) (int, error) {
return f.file.Read(p)
}
func (f FileSystemBogFileData) Write(p []byte) (int, error) {
return f.file.Write(p)
}
func (f FileSystemBogFileData) Close() error {
return f.file.Close()
}
func (f FileSystemBogFileData) Seek(offset int64, whence int) (int64, error) {
return f.file.Seek(offset, whence)
}
func (f FileSystemBogFileData) Path() string {
return f.path
}
func (f FileSystemBogFileData) Size() int64 {
return f.size
}
func (f FileSystemBogFileData) Modified() time.Time{
return time.Now()
}
type FileSystemBogRepository struct {
Root string
}
func (f FileSystemBogRepository) absPath(filename string, user_agent_label string) string {
return path.Join(f.Root, user_agent_label, filename)
}
func (f FileSystemBogRepository) Create(filename string, user_agent_label string) (domain.BogInFile, error) {
abs_path := f.absPath(filename, user_agent_label)
dir := path.Dir(abs_path)
os.MkdirAll(dir, 0750)
file, err := os.OpenFile(abs_path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
stat_info, err := file.Stat()
if err != nil {
panic(err)
}
bfd := FileSystemBogFileData {filename, stat_info.Size(), stat_info.ModTime(), file}
return bfd, nil
}
func (f FileSystemBogRepository) Open(filename string, user_agent_label string) (domain.BogOutFile, error) {
abs_path := f.absPath(filename, user_agent_label)
dir := path.Dir(abs_path)
os.MkdirAll(dir, 0750)
file, err := os.OpenFile(abs_path, os.O_RDONLY, 0644)
if err != nil {
return nil, domain.ErrNotExists
}
bfd := FileSystemBogFileData {filename, 0, time.Now(), file}
return bfd, nil
}
func (f FileSystemBogRepository) Delete(filename string, user_agent_label string) {
abs_path := f.absPath(filename, user_agent_label)
os.Remove(abs_path)
}

@ -1,45 +0,0 @@
package integration
import (
"errors"
"time"
"caj-larsson/bog/domain"
)
type UserAgentDBRecord struct {
ID int64
Name string
LastSeen string
AllowanceSeconds int64
QuotaKB int64
QuotaUsedKB int64
}
var ErrUnparseableRecord = errors.New("record could not be mapped to entity")
func (r *UserAgentDBRecord) toEntity() (*domain.UserAgent, error) {
lastseen, err := time.Parse(time.RFC3339, r.LastSeen)
if err != nil {
return nil, ErrUnparseableRecord
}
var useragent = new(domain.UserAgent)
useragent.ID = r.ID
useragent.Name = r.Name
useragent.LastSeen = lastseen
useragent.AllowanceDuration = time.Duration(r.AllowanceSeconds * int64(time.Second))
useragent.FileQuota = domain.FileSizeQuota { r.QuotaKB, r.QuotaUsedKB }
return useragent, err
}
func fromEntity(useragent domain.UserAgent) (*UserAgentDBRecord, error) {
var record = new(UserAgentDBRecord)
record.ID = useragent.ID
record.Name = useragent.Name
record.LastSeen = useragent.LastSeen.Format(time.RFC3339)
record.AllowanceSeconds = int64(useragent.AllowanceDuration.Seconds())
record.QuotaKB = useragent.FileQuota.AllowanceKB
record.QuotaUsedKB = useragent.FileQuota.CurrentUsage
return record, nil
}

@ -1,7 +1,7 @@
package main
import (
"caj-larsson/bog/application"
"caj-larsson/bog/server"
"io/ioutil"
"fmt"
)
@ -10,16 +10,16 @@ func main() {
content, err := ioutil.ReadFile("default.toml")
if err != nil {
panic(err)
panic(err)
}
config, err := application.ConfigFromToml(string(content))
config, err := server.ConfigFromToml(string(content))
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", config)
fmt.Printf("Dataswamp running")
bog := application.New(config)
bog := server.New(config)
bog.Run()
}

@ -1,12 +1,15 @@
package application
package server
import (
"net/http"
"fmt"
"strconv"
// "io"
"caj-larsson/bog/domain"
"caj-larsson/bog/integration"
"caj-larsson/bog/dataswamp"
"caj-larsson/bog/dataswamp/namespace"
"caj-larsson/bog/dataswamp/swampfile"
sql_namespace "caj-larsson/bog/infrastructure/sqlite/namespace"
fs_swampfile "caj-larsson/bog/infrastructure/fs/swampfile"
)
type Router interface {
@ -16,21 +19,21 @@ type Router interface {
type Bog struct {
router Router
file_service domain.BogFileService
file_service dataswamp.SwampFileService
address string
}
func buildFileDataRepository(config FileConfig) domain.FileDataRepository{
fsBogRepo := new(integration.FileSystemBogRepository)
fsBogRepo.Root = config.Path
return fsBogRepo
func buildFileDataRepository(config FileConfig) swampfile.Repository{
fsSwampfileRepo := new(fs_swampfile.Repository)
fsSwampfileRepo.Root = config.Path
return fsSwampfileRepo
}
func buildUserAgentRepository(config DatabaseConfig) *integration.SQLiteUserAgentRepository{
func buildUserAgentRepository(config DatabaseConfig) namespace.Repository{
if config.Backend != "sqlite" {
panic("Can only handle sqlite")
}
return integration.NewSQLiteUserAgentRepository(config.Connection)
return sql_namespace.NewRepository(config.Connection)
}
func (b *Bog) fileHandler(w http.ResponseWriter, r *http.Request) {
@ -39,13 +42,13 @@ func (b *Bog) fileHandler(w http.ResponseWriter, r *http.Request) {
return
}
ref := domain.FileReference {r.URL.Path, r.Header["User-Agent"][0]}
ref := swampfile.FileReference {r.URL.Path, r.Header["User-Agent"][0]}
switch r.Method {
case "GET":
bog_file, err := b.file_service.OpenOutFile(ref)
swamp_file, err := b.file_service.OpenOutFile(ref)
if err == domain.ErrNotExists {
if err == swampfile.ErrNotExists {
http.NotFound(w, r)
return
}
@ -54,7 +57,7 @@ func (b *Bog) fileHandler(w http.ResponseWriter, r *http.Request) {
panic(err)
}
http.ServeContent(w, r, bog_file.Path(), bog_file.Modified(), bog_file)
http.ServeContent(w, r, swamp_file.Path(), swamp_file.Modified(), swamp_file)
case "POST":
fallthrough
case "PUT":
@ -84,11 +87,11 @@ func New(config *Configuration) *Bog {
b := new(Bog)
b.address = config.bindAddress()
fsBogRepo := buildFileDataRepository(config.File)
fsSwampRepo := buildFileDataRepository(config.File)
uaRepo := buildUserAgentRepository(config.Database)
b.file_service = domain.NewBogFileService(
uaRepo, fsBogRepo, config.Quota.ParsedSizeBytes(), config.Quota.ParsedDuration(),
b.file_service = dataswamp.NewSwampFileService(
uaRepo, fsSwampRepo, config.Quota.ParsedSizeBytes(), config.Quota.ParsedDuration(),
)
b.router = new(http.ServeMux)

@ -0,0 +1,40 @@
package server
import (
"testing"
// "fmt"
// "net/http/httptest"
// "net/http"
// "strings"
// "time"
// "caj-larsson/bog/domain_dataswamp"
)
func TestApplication(t *testing.T) {
// file_service := domain_dataswamp.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")
// req.Header.Add("Content-Length", "8")
// w := httptest.NewRecorder()
// bog.router.ServeHTTP(w, req)
// if (w.Code != 200){
// fmt.Printf("%v", w)
// t.Error("not ok")
// }
}

@ -1,4 +1,4 @@
package application
package server
import (
"fmt"

@ -1,4 +1,4 @@
package application
package server
import (

@ -1,94 +0,0 @@
package test
import (
"time"
"bytes"
"testing"
"caj-larsson/bog/domain"
"caj-larsson/bog/test/mock"
)
var file_ref1 = domain.FileReference { "path1", "ua1" }
var file_ref2 = domain.FileReference { "path1", "ua2" }
var file_ref3 = domain.FileReference { "path2", "ua1" }
func NewTestBogFileService() domain.BogFileService {
file_repo := mock.NewMockFileRepository()
ua_repo := mock.NewMockUserAgentRepository()
return domain.NewBogFileService(ua_repo, file_repo, 1024, time.Hour)
}
func TestFileDontExist(t *testing.T) {
s := NewTestBogFileService()
outfile, err := s.OpenOutFile(file_ref1)
if outfile != nil && err != domain.ErrNotExists {
t.Errorf("File shall not exist by default")
}
}
func TestFileIsStored(t *testing.T) {
s := NewTestBogFileService()
fakefile := bytes.NewBufferString("My bog data")
err := s.SaveFile(file_ref1, fakefile, int64(fakefile.Len()))
if err != nil {
t.Errorf("A small file should be writable %s", err)
}
largefakefile := bytes.NewBufferString("")
for largefakefile.Len() < 64000 {
_, err = largefakefile.WriteString("A very repetitive file")
}
err = s.SaveFile(file_ref3, largefakefile, int64(largefakefile.Len()))
if err != domain.ErrExceedQuota {
t.Errorf("too large files should not be excepted")
}
}
func TestFileIsReadBack(t *testing.T) {
s := NewTestBogFileService()
infile := bytes.NewBufferString("My bog data")
_ = s.SaveFile(file_ref1, infile, int64(infile.Len()))
outbogfile, _ := s.OpenOutFile(file_ref1)
outfile := bytes.NewBufferString("")
_, _ = outfile.ReadFrom(outbogfile)
if outfile.String() != "My bog data" {
t.Errorf("file corrupted")
}
}
func TestUAIsolation(t *testing.T) {
file_repo := mock.NewMockFileRepository()
ua_repo := mock.NewMockUserAgentRepository()
s := domain.NewBogFileService(ua_repo, file_repo, 1024, time.Hour)
ua1_file := bytes.NewBufferString("My bog data ua1")
ua2_file := bytes.NewBufferString("My bog data ua2")
_ = s.SaveFile(file_ref1, ua1_file, int64(ua1_file.Len()))
_ = s.SaveFile(file_ref2, ua2_file, int64(ua2_file.Len()))
outbogfile, _ := s.OpenOutFile(file_ref1)
outfile := bytes.NewBufferString("")
_, _ = outfile.ReadFrom(outbogfile)
if outfile.String() != "My bog data ua1" {
t.Errorf("file corrupted")
}
}

@ -1,21 +0,0 @@
package test
import (
"path"
"testing"
"caj-larsson/bog/domain"
"caj-larsson/bog/integration"
"caj-larsson/bog/test/mock"
)
func TestFsFileRepo(t *testing.T) {
var fac = func()domain.FileDataRepository {
r := t.TempDir()
d := path.Join(r, "fs")
repo := integration.FileSystemBogRepository { d }
return &repo
}
mock.BogFileRepositoryContract(fac, t)
}

@ -1,90 +0,0 @@
package mock
import (
"time"
"path"
"os"
// "io"
"github.com/spf13/afero"
"caj-larsson/bog/domain"
)
type MockBogFile struct {
filename string
file afero.File
}
func (f MockBogFile) Path() string {
return f.filename
}
func (f MockBogFile) Size() int64 {
stat, _ := f.file.Stat()
return int64(stat.Size())
}
func (f MockBogFile) Read(p []byte) (int, error) {
return f.file.Read(p)
}
func (f MockBogFile) Write(p []byte) (int, error) {
return f.file.Write(p)
}
func (f MockBogFile) Close() error {
return f.file.Close()
}
func (f MockBogFile) Seek(offset int64, whence int) (int64, error) {
return f.file.Seek(offset, whence)
}
func (f MockBogFile) Modified() time.Time {
stat, _ := f.file.Stat()
return stat.ModTime()
}
// The actual repository
type MockFileRepository struct {
fs afero.Fs
}
func NewMockFileRepository() domain.FileDataRepository {
return MockFileRepository { afero.NewMemMapFs() }
}
func (r MockFileRepository) Create(filename string, user_agent_label string) (domain.BogInFile, error) {
abs_path := path.Join( filename, user_agent_label)
dir := path.Dir(abs_path)
r.fs.MkdirAll(dir, 0750)
file, err := r.fs.OpenFile(abs_path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
bf := MockBogFile {filename, file}
return bf, nil
}
func (r MockFileRepository) Open(filename string, user_agent_label string) (domain.BogOutFile, error) {
abs_path := path.Join(filename, user_agent_label)
dir := path.Dir(abs_path)
r.fs.MkdirAll(dir, 0750)
file, err := r.fs.OpenFile(abs_path, os.O_RDONLY, 0644)
if err != nil {
return nil, domain.ErrNotExists
}
bf := MockBogFile {filename, file}
return bf, nil
}
func (r MockFileRepository) Delete(filename string, user_agent_label string) {
abs_path := path.Join(filename, user_agent_label)
r.fs.Remove(abs_path)
}

@ -1,11 +0,0 @@
package mock
import (
"testing"
//"caj-larsson/bog/domain"
)
func TestMockFileRepo(t *testing.T) {
BogFileRepositoryContract(NewMockFileRepository, t)
}

@ -1,66 +0,0 @@
package mock
import (
// "time"
"caj-larsson/bog/domain"
)
type MockUserAgentRepository struct {
IdIdx map[int64]*domain.UserAgent
NameIdx map[string]*domain.UserAgent
NextId int64
}
func NewMockUserAgentRepository() *MockUserAgentRepository {
r := new(MockUserAgentRepository)
r.NextId = 0
r.IdIdx = make(map[int64]*domain.UserAgent)
r.NameIdx = make(map[string]*domain.UserAgent)
return r
}
func (r *MockUserAgentRepository) Create(useragent domain.UserAgent) (*domain.UserAgent, error) {
r.NextId += 1
useragent.ID = r.NextId
r.IdIdx[useragent.ID] = &useragent
r.NameIdx[useragent.Name] = &useragent
return &useragent, nil
}
func (r *MockUserAgentRepository) All() ([]domain.UserAgent, error) {
v := make([]domain.UserAgent, 0, len(r.IdIdx))
for _, value := range r.IdIdx {
v = append(v, *value)
}
return v, nil
}
func (r *MockUserAgentRepository) GetByName(name string) (*domain.UserAgent, error) {
useragent, exists := r.NameIdx[name]
if exists {
return useragent, nil
}
return nil, domain.ErrNotExists
}
func (r *MockUserAgentRepository) Update(id int64, useragent domain.UserAgent) (*domain.UserAgent, error) {
original := *r.IdIdx[id]
useragent.ID = id
r.IdIdx[id] = &useragent
r.NameIdx[original.Name] = &useragent
return &useragent, nil
}
func (r *MockUserAgentRepository) Delete(id int64) error {
original := *r.IdIdx[id]
delete(r.NameIdx, original.Name)
delete(r.IdIdx, original.ID)
return nil
}

@ -1,52 +0,0 @@
package mock
import (
"testing"
"time"
"caj-larsson/bog/domain"
)
func TestMockUserAgentRepo(t *testing.T) {
r := NewMockUserAgentRepository()
all, err := r.All()
if len(all) != 0 && err != nil {
t.Errorf("New repo should be empty")
}
ua := domain.UserAgent {23, "n1", time.Now(), time.Duration(time.Hour * 3), domain.FileSizeQuota {1000, 0} }
ua1, _ := r.Create(ua)
ua.Name = "n2"
ua2, _ := r.Create(ua)
if ua1 == ua2 {
t.Errorf("Must create unique items")
}
all, err = r.All()
if len(all) != 2 && err != nil {
t.Errorf("After adding there should be two Useragent")
}
if ua.ID != 23 {
t.Errorf("It does not change the original UserAgent")
}
ua3, _ := r.GetByName("n2")
if ua3 != ua2 {
t.Errorf("It the correct ua is acquired")
}
if r.Delete(ua2.ID) != nil {
t.Errorf("Must delete without error")
}
all, err = r.All()
if len(all) != 1 && err != nil {
t.Errorf("After deleting one there should be one UA ")
}
}
Loading…
Cancel
Save