diff --git a/application/bog.go b/application/bog.go new file mode 100644 index 0000000..5f20e48 --- /dev/null +++ b/application/bog.go @@ -0,0 +1,71 @@ +package application + +import ( + "net/http" + "fmt" + "log" + "io" + "caj-larsson/bog/domain" + "caj-larsson/bog/integration" +) + +type Bog struct { + config *Configuration + mux *http.ServeMux + // file_service *domain.BogFileService + fileDataRepo domain.FileDataRepository +} + +func New(config *Configuration) *Bog { + b := new(Bog) + b.config = config + log.Print(config) + + fsBogRepo := new(integration.FileSystemBogRepository) + fsBogRepo.Root = "/tmp/datta" + + b.fileDataRepo = *fsBogRepo + // uaRepo *domain.UserAgentRepository + + b.mux = http.NewServeMux() + + b.mux.HandleFunc("/",func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + fmt.Fprintf(w, "Hi") + return + } + + bog_file, err := b.fileDataRepo.OpenOrCreate(r.URL.Path, r.Header["User-Agent"][0]) + + if err != nil { + + } + + switch r.Method { + case "GET": + src := domainFileSource { bog_file.Path(), + http.ServeContent(w, r, , bog_file.Modified(), bog_file) + + case "POST": + fallthrough + case "PUT": + + io.Copy(bog_file, r.Body) + bog_file.Close() + } + return + }) + + + return b +} + +func (b *Bog) Run() { + http.ListenAndServe(b.config.address(), b.mux) +} + + +func CreateFileHandler(w http.ResponseWriter, r *http.Request) { + + +} diff --git a/application/configuration.go b/application/configuration.go new file mode 100644 index 0000000..a32b3c0 --- /dev/null +++ b/application/configuration.go @@ -0,0 +1,29 @@ +package application + +import ( + "fmt" + "github.com/BurntSushi/toml" + +) + +type Configuration struct { + Port int64 + Host string + Path string +} + +func (c *Configuration) address() string { + return fmt.Sprintf("%s:%d", c.Host, c.Port) +} + +func ConfigFromToml(toml_data string) (*Configuration, error) { + var config Configuration + + _, err := toml.Decode(toml_data, &config) + + if err != nil { + return nil, err + } + + return &config, nil +} diff --git a/default.toml b/default.toml new file mode 100644 index 0000000..24dccbb --- /dev/null +++ b/default.toml @@ -0,0 +1,2 @@ +port = 8002 +host = "127.0.0.1" \ No newline at end of file diff --git a/domain/entities.go b/domain/entities.go new file mode 100644 index 0000000..30cdc2e --- /dev/null +++ b/domain/entities.go @@ -0,0 +1,28 @@ +package domain + +import ( + "time" + "errors" +) + +type UserAgent struct { + ID int64 + Name string + LastSeen time.Time + AllowanceDuration time.Duration + FileQuota FileSizeQuota +} + +type BogFile struct { + UserAgentId int64 + Path string + Size int64 + CreatedAt time.Time +} + +var ( + ErrDuplicate = errors.New("record already exists") + ErrNotExists = errors.New("row not exists") + ErrUpdateFailed = errors.New("update failed") + ErrDeleteFailed = errors.New("delete failed") +) diff --git a/domain/entities_test.go b/domain/entities_test.go new file mode 100644 index 0000000..688b02b --- /dev/null +++ b/domain/entities_test.go @@ -0,0 +1,10 @@ +package domain + +import "testing" + +func TestSum(t *testing.T) { + total := 3 + if total != 10 { + t.Errorf("It failed ok") + } +} diff --git a/domain/repository.go b/domain/repository.go new file mode 100644 index 0000000..273ca2f --- /dev/null +++ b/domain/repository.go @@ -0,0 +1,15 @@ +package domain + +type UserAgentRepository interface{ + Migrate() error + CreateUserAgent(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 { + OpenOrCreate(path string, user_agent string) (BogFileData, error) + Delete(file_data *BogFileData) +} diff --git a/domain/services.go b/domain/services.go new file mode 100644 index 0000000..50811e0 --- /dev/null +++ b/domain/services.go @@ -0,0 +1,30 @@ +package domain + +import "io" + +type BogFileService struct { + user_agent_repo UserAgentRepository + file_data_repo FileDataRepository +} + +func (b *BogFileService) RecieveFile(src FileSource) error { + f, err := b.file_data_repo.OpenOrCreate(src.Path(), src.UserAgent()) + + if err != nil { + return err + } + + io.Copy(f, src) + return nil +} + +func (b *BogFileService) SendFile(dst FileDestination) error { + f, err := b.file_data_repo.OpenOrCreate(dst.Path(), dst.UserAgent()) + + if err != nil { + return err + } + + io.Copy(dst, f) + return nil +} diff --git a/domain/valueobjects.go b/domain/valueobjects.go new file mode 100644 index 0000000..3035b0c --- /dev/null +++ b/domain/valueobjects.go @@ -0,0 +1,46 @@ +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) { + f.CurrentUsage += size +} + +func (f *FileSizeQuota) Remove(size int64) { + f.CurrentUsage -= size +} + +type BogFileData interface { + Path() string + Size() int64 + Modified() time.Time + io.Reader + io.Seeker + io.Writer + io.Closer +} + +type FileSource struct { + Path string + UserAgent string + Size int64 + Src io.ReadCloser +} + +type FileDestination struct { + Path string + UserAgent string + Dst io.WriteCloser +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6b7993a --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module caj-larsson/bog + +go 1.18 + +require ( + github.com/BurntSushi/toml v1.1.0 // indirect + github.com/mattn/go-sqlite3 v1.14.12 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..341c8de --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +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= diff --git a/integration/filesystem_bog_file_repository.go b/integration/filesystem_bog_file_repository.go new file mode 100644 index 0000000..76ef273 --- /dev/null +++ b/integration/filesystem_bog_file_repository.go @@ -0,0 +1,68 @@ +package integration + +import ( + "time" + "os" + "path" + "caj-larsson/bog/domain" +) + +type FileSystemBogFileData struct { + path string + 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 0 +} + +func (f FileSystemBogFileData) Modified() time.Time{ + return time.Now() +} + + +type FileSystemBogRepository struct { + Root string +} + +func (f FileSystemBogRepository) OpenOrCreate(open_path string, user_agent string) (domain.BogFileData, error) { + //file_name := path.Base(open_path) + abs_path := path.Join(f.Root, open_path) + + 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) + } + + bfd := FileSystemBogFileData {open_path, file} + + return bfd, nil +} + +func (f FileSystemBogRepository) Delete(file *domain.BogFileData) { + +} diff --git a/integration/record.go b/integration/record.go new file mode 100644 index 0000000..7e318ad --- /dev/null +++ b/integration/record.go @@ -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 +} diff --git a/integration/sqliterepo.go b/integration/sqliterepo.go new file mode 100644 index 0000000..d85196b --- /dev/null +++ b/integration/sqliterepo.go @@ -0,0 +1,149 @@ +package integration + +import ( + "caj-larsson/bog/domain" + "database/sql" + "errors" + "github.com/mattn/go-sqlite3" +) + +type SQLiteUserAgentRepository struct { + db *sql.DB +} + +func NewSQLiteUserAgentRepository(db *sql.DB) *SQLiteUserAgentRepository { + return &SQLiteUserAgentRepository{ + db: db, + } +} + +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, allowance_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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4b8fa3e --- /dev/null +++ b/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "caj-larsson/bog/application" + "io/ioutil" +) + +func main() { + content, err := ioutil.ReadFile("default.toml") + + if err != nil { + panic(err) + } + + config, err := application.ConfigFromToml(string(content)) + if err != nil { + panic(err) + } + + bog := application.New(config) + bog.Run() +}