forked from mirrors/homebox
feat: item-attachments CRUD (#22)
* change /content/ -> /homebox/ * add cache to code generators * update env variables to set data storage * update env variables * set env variables in prod container * implement attachment post route (WIP) * get attachment endpoint * attachment download * implement string utilities lib * implement generic drop zone * use explicit truncate * remove clean dir * drop strings composable for lib * update item types and add attachments * add attachment API * implement service context * consolidate API code * implement editing attachments * implement upload limit configuration * improve error handling * add docs for max upload size * fix test cases
This commit is contained in:
parent
852d312ba7
commit
31b34241e0
165 changed files with 2509 additions and 664 deletions
|
@ -17,12 +17,12 @@ const (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
Mode string `yaml:"mode" conf:"default:development"` // development or production
|
||||
Web WebConfig `yaml:"web"`
|
||||
Database Database `yaml:"database"`
|
||||
Log LoggerConf `yaml:"logger"`
|
||||
Mailer MailerConf `yaml:"mailer"`
|
||||
Swagger SwaggerConf `yaml:"swagger"`
|
||||
Mode string `yaml:"mode" conf:"default:development"` // development or production
|
||||
Web WebConfig `yaml:"web"`
|
||||
Storage Storage `yaml:"storage"`
|
||||
Log LoggerConf `yaml:"logger"`
|
||||
Mailer MailerConf `yaml:"mailer"`
|
||||
Swagger SwaggerConf `yaml:"swagger"`
|
||||
}
|
||||
|
||||
type SwaggerConf struct {
|
||||
|
@ -31,8 +31,9 @@ type SwaggerConf struct {
|
|||
}
|
||||
|
||||
type WebConfig struct {
|
||||
Port string `yaml:"port" conf:"default:7745"`
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port" conf:"default:7745"`
|
||||
Host string `yaml:"host"`
|
||||
MaxUploadSize int64 `yaml:"max_file_upload" conf:"default:10"`
|
||||
}
|
||||
|
||||
// NewConfig parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the
|
||||
|
|
|
@ -4,20 +4,8 @@ const (
|
|||
DriverSqlite3 = "sqlite3"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Driver string `yaml:"driver" conf:"default:sqlite3"`
|
||||
SqliteUrl string `yaml:"sqlite-url" conf:"default:file:ent?mode=memory&cache=shared&_fk=1"`
|
||||
}
|
||||
|
||||
func (d *Database) GetDriver() string {
|
||||
return d.Driver
|
||||
}
|
||||
|
||||
func (d *Database) GetUrl() string {
|
||||
switch d.Driver {
|
||||
case DriverSqlite3:
|
||||
return d.SqliteUrl
|
||||
default:
|
||||
panic("unknown database driver")
|
||||
}
|
||||
type Storage struct {
|
||||
// Data is the path to the root directory
|
||||
Data string `yaml:"data" conf:"default:./homebox-data"`
|
||||
SqliteUrl string `yaml:"sqlite-url" conf:"default:./homebox-data/homebox.db?_fk=1"`
|
||||
}
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_DatabaseConfig_Sqlite(t *testing.T) {
|
||||
dbConf := &Database{
|
||||
Driver: DriverSqlite3,
|
||||
SqliteUrl: "file:ent?mode=memory&cache=shared&_fk=1",
|
||||
}
|
||||
|
||||
assert.Equal(t, "sqlite3", dbConf.GetDriver())
|
||||
assert.Equal(t, "file:ent?mode=memory&cache=shared&_fk=1", dbConf.GetUrl())
|
||||
}
|
||||
|
||||
func Test_DatabaseConfig_Unknown(t *testing.T) {
|
||||
dbConf := &Database{
|
||||
Driver: "null",
|
||||
}
|
||||
|
||||
assert.Panics(t, func() { dbConf.GetUrl() })
|
||||
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package factories
|
||||
|
||||
import (
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/content/backend/pkgs/faker"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/faker"
|
||||
)
|
||||
|
||||
func UserFactory() types.UserCreate {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/hay-kot/content/backend/internal/repo"
|
||||
"github.com/hay-kot/content/backend/internal/services"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/services"
|
||||
)
|
||||
|
||||
func GetMockServices(repos *repo.AllRepos) *services.AllServices {
|
||||
return services.NewServices(repos)
|
||||
return services.NewServices(repos, "/tmp/homebox")
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ package mocks
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/pkgs/faker"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/faker"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/document"
|
||||
"github.com/hay-kot/content/backend/ent/group"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/document"
|
||||
"github.com/hay-kot/homebox/backend/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
// DocumentRepository is a repository for Document entity
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/documenttoken"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/documenttoken"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
// DocumentTokensRepository is a repository for Document entity
|
||||
|
|
|
@ -6,9 +6,9 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/documenttoken"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/documenttoken"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
)
|
||||
|
||||
type GroupRepository struct {
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/attachment"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/attachment"
|
||||
)
|
||||
|
||||
// AttachmentRepo is a repository for Attachments table that links Items to Documents
|
||||
|
@ -34,9 +34,15 @@ func (r *AttachmentRepo) Get(ctx context.Context, id uuid.UUID) (*ent.Attachment
|
|||
}
|
||||
|
||||
func (r *AttachmentRepo) Update(ctx context.Context, itemId uuid.UUID, typ attachment.Type) (*ent.Attachment, error) {
|
||||
return r.db.Attachment.UpdateOneID(itemId).
|
||||
itm, err := r.db.Attachment.UpdateOneID(itemId).
|
||||
SetType(typ).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.Get(ctx, itm.ID)
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/attachment"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/attachment"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/group"
|
||||
"github.com/hay-kot/content/backend/ent/item"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/ent/item"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
type ItemsRepository struct {
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/group"
|
||||
"github.com/hay-kot/content/backend/ent/label"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/ent/label"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
type LabelRepository struct {
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/location"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/location"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
type LocationRepository struct {
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/authtokens"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/authtokens"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
type TokenRepository struct {
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/content/backend/pkgs/hasher"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/hasher"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/user"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/user"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package repo
|
||||
|
||||
import "github.com/hay-kot/content/backend/ent"
|
||||
import "github.com/hay-kot/homebox/backend/ent"
|
||||
|
||||
// AllRepos is a container for all the repository interfaces
|
||||
type AllRepos struct {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package services
|
||||
|
||||
import "github.com/hay-kot/content/backend/internal/repo"
|
||||
import "github.com/hay-kot/homebox/backend/internal/repo"
|
||||
|
||||
type AllServices struct {
|
||||
User *UserService
|
||||
|
@ -10,7 +10,14 @@ type AllServices struct {
|
|||
Items *ItemService
|
||||
}
|
||||
|
||||
func NewServices(repos *repo.AllRepos) *AllServices {
|
||||
func NewServices(repos *repo.AllRepos, root string) *AllServices {
|
||||
if repos == nil {
|
||||
panic("repos cannot be nil")
|
||||
}
|
||||
if root == "" {
|
||||
panic("root cannot be empty")
|
||||
}
|
||||
|
||||
return &AllServices{
|
||||
User: &UserService{repos},
|
||||
Admin: &AdminService{repos},
|
||||
|
@ -18,7 +25,8 @@ func NewServices(repos *repo.AllRepos) *AllServices {
|
|||
Labels: &LabelService{repos},
|
||||
Items: &ItemService{
|
||||
repo: repos,
|
||||
filepath: "/tmp/content",
|
||||
filepath: root,
|
||||
at: attachmentTokens{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@ package services
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
type contextKeys struct {
|
||||
|
@ -15,6 +16,31 @@ var (
|
|||
ContextUserToken = &contextKeys{name: "UserToken"}
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
context.Context
|
||||
|
||||
// UID is a unique identifier for the acting user.
|
||||
UID uuid.UUID
|
||||
|
||||
// GID is a unique identifier for the acting users group.
|
||||
GID uuid.UUID
|
||||
|
||||
// User is the acting user.
|
||||
User *types.UserOut
|
||||
}
|
||||
|
||||
// NewContext is a helper function that returns the service context from the context.
|
||||
// This extracts the users from the context and embeds it into the ServiceContext struct
|
||||
func NewContext(ctx context.Context) Context {
|
||||
user := UseUserCtx(ctx)
|
||||
return Context{
|
||||
Context: ctx,
|
||||
UID: user.ID,
|
||||
GID: user.GroupID,
|
||||
User: user,
|
||||
}
|
||||
}
|
||||
|
||||
// SetUserCtx is a helper function that sets the ContextUser and ContextUserToken
|
||||
// values within the context of a web request (or any context).
|
||||
func SetUserCtx(ctx context.Context, user *types.UserOut, token string) context.Context {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -8,16 +8,17 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/repo"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/content/backend/pkgs/faker"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/faker"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var (
|
||||
fk = faker.NewFaker()
|
||||
|
||||
tCtx = Context{}
|
||||
tClient *ent.Client
|
||||
tRepos *repo.AllRepos
|
||||
tUser *ent.User
|
||||
|
@ -63,10 +64,16 @@ func TestMain(m *testing.M) {
|
|||
|
||||
tClient = client
|
||||
tRepos = repo.EntAllRepos(tClient)
|
||||
tSvc = NewServices(tRepos)
|
||||
tSvc = NewServices(tRepos, "/tmp/homebox")
|
||||
defer client.Close()
|
||||
|
||||
|
||||
bootstrap()
|
||||
tCtx = Context{
|
||||
Context: context.Background(),
|
||||
GID: tGroup.ID,
|
||||
UID: tUser.ID,
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package mappers
|
||||
|
||||
import (
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
func ToItemAttachment(attachment *ent.Attachment) *types.ItemAttachment {
|
||||
|
@ -10,6 +10,7 @@ func ToItemAttachment(attachment *ent.Attachment) *types.ItemAttachment {
|
|||
ID: attachment.ID,
|
||||
CreatedAt: attachment.CreatedAt,
|
||||
UpdatedAt: attachment.UpdatedAt,
|
||||
Type: attachment.Type.String(),
|
||||
Document: types.DocumentOut{
|
||||
ID: attachment.Edges.Document.ID,
|
||||
Title: attachment.Edges.Document.Title,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package mappers
|
||||
|
||||
import (
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
func ToLabelSummary(label *ent.Label) *types.LabelSummary {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package mappers
|
||||
|
||||
import (
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/repo"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
func ToLocationCount(location *repo.LocationWithCount) *types.LocationCount {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package mappers
|
||||
|
||||
import (
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
func ToOutUser(user *ent.User, err error) (*types.UserOut, error) {
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/repo"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
type AdminService struct {
|
||||
|
|
|
@ -2,24 +2,28 @@ package services
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent/attachment"
|
||||
"github.com/hay-kot/content/backend/internal/repo"
|
||||
"github.com/hay-kot/content/backend/internal/services/mappers"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/services/mappers"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrFileNotFound = errors.New("file not found")
|
||||
)
|
||||
|
||||
type ItemService struct {
|
||||
repo *repo.AllRepos
|
||||
|
||||
// filepath is the root of the storage location that will be used to store all files from.
|
||||
filepath string
|
||||
// at is a map of tokens to attachment IDs. This is used to store the attachment ID
|
||||
// for issued URLs
|
||||
at attachmentTokens
|
||||
}
|
||||
|
||||
func (svc *ItemService) GetOne(ctx context.Context, gid uuid.UUID, id uuid.UUID) (*types.ItemOut, error) {
|
||||
|
@ -94,59 +98,6 @@ func (svc *ItemService) Update(ctx context.Context, gid uuid.UUID, data types.It
|
|||
return mappers.ToItemOut(item), nil
|
||||
}
|
||||
|
||||
func (svc *ItemService) attachmentPath(gid, itemId uuid.UUID, filename string) string {
|
||||
return filepath.Join(svc.filepath, gid.String(), itemId.String(), filename)
|
||||
}
|
||||
|
||||
// AddAttachment adds an attachment to an item by creating an entry in the Documents table and linking it to the Attachment
|
||||
// Table and Items table. The file provided via the reader is stored on the file system based on the provided
|
||||
// relative path during construction of the service.
|
||||
func (svc *ItemService) AddAttachment(ctx context.Context, gid, itemId uuid.UUID, filename string, file io.Reader) (*types.ItemOut, error) {
|
||||
// Get the Item
|
||||
item, err := svc.repo.Items.GetOne(ctx, itemId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if item.Edges.Group.ID != gid {
|
||||
return nil, ErrNotOwner
|
||||
}
|
||||
|
||||
// Create the document
|
||||
doc, err := svc.repo.Docs.Create(ctx, gid, types.DocumentCreate{
|
||||
Title: filename,
|
||||
Path: svc.attachmentPath(gid, itemId, filename),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the attachment
|
||||
_, err = svc.repo.Attachments.Create(ctx, itemId, doc.ID, attachment.TypeAttachment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read the contents and write them to a file on the file system
|
||||
err = os.MkdirAll(filepath.Dir(doc.Path), os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Create(doc.Path)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create file")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(f, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return svc.GetOne(ctx, gid, itemId)
|
||||
}
|
||||
|
||||
func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]string) error {
|
||||
loaded := []csvRow{}
|
||||
|
||||
|
|
199
backend/internal/services/service_items_attachments.go
Normal file
199
backend/internal/services/service_items_attachments.go
Normal file
|
@ -0,0 +1,199 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/ent/attachment"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/hasher"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/pathlib"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// TODO: this isn't a scalable solution, tokens should be stored in the database
|
||||
type attachmentTokens map[string]uuid.UUID
|
||||
|
||||
func (at attachmentTokens) Add(token string, id uuid.UUID) {
|
||||
at[token] = id
|
||||
|
||||
log.Debug().Str("token", token).Str("uuid", id.String()).Msg("added token")
|
||||
|
||||
go func() {
|
||||
ch := time.After(1 * time.Minute)
|
||||
<-ch
|
||||
at.Delete(token)
|
||||
log.Debug().Str("token", token).Msg("deleted token")
|
||||
}()
|
||||
}
|
||||
|
||||
func (at attachmentTokens) Get(token string) (uuid.UUID, bool) {
|
||||
id, ok := at[token]
|
||||
return id, ok
|
||||
}
|
||||
|
||||
func (at attachmentTokens) Delete(token string) {
|
||||
delete(at, token)
|
||||
}
|
||||
|
||||
func (svc *ItemService) AttachmentToken(ctx Context, itemId, attachmentId uuid.UUID) (string, error) {
|
||||
item, err := svc.repo.Items.GetOne(ctx, itemId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if item.Edges.Group.ID != ctx.GID {
|
||||
return "", ErrNotOwner
|
||||
}
|
||||
|
||||
token := hasher.GenerateToken()
|
||||
|
||||
// Ensure that the file exists
|
||||
attachment, err := svc.repo.Attachments.Get(ctx, attachmentId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(attachment.Edges.Document.Path); os.IsNotExist(err) {
|
||||
_ = svc.AttachmentDelete(ctx, ctx.GID, itemId, attachmentId)
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
svc.at.Add(token.Raw, attachmentId)
|
||||
|
||||
return token.Raw, nil
|
||||
}
|
||||
|
||||
func (svc *ItemService) attachmentPath(gid, itemId uuid.UUID, filename string) string {
|
||||
path := filepath.Join(svc.filepath, gid.String(), itemId.String(), filename)
|
||||
path = pathlib.Safe(path)
|
||||
log.Debug().Str("path", path).Msg("attachment path")
|
||||
return path
|
||||
}
|
||||
|
||||
func (svc *ItemService) AttachmentPath(ctx context.Context, token string) (string, error) {
|
||||
attachmentId, ok := svc.at.Get(token)
|
||||
if !ok {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
attachment, err := svc.repo.Attachments.Get(ctx, attachmentId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return attachment.Edges.Document.Path, nil
|
||||
}
|
||||
|
||||
func (svc *ItemService) AttachmentUpdate(ctx Context, itemId uuid.UUID, data *types.ItemAttachmentUpdate) (*types.ItemOut, error) {
|
||||
// Update Properties
|
||||
attachment, err := svc.repo.Attachments.Update(ctx, data.ID, attachment.Type(data.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attDoc := attachment.Edges.Document
|
||||
|
||||
if data.Title != attachment.Edges.Document.Title {
|
||||
newPath := pathlib.Safe(svc.attachmentPath(ctx.GID, itemId, data.Title))
|
||||
|
||||
// Move File
|
||||
err = os.Rename(attachment.Edges.Document.Path, newPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = svc.repo.Docs.Update(ctx, attDoc.ID, types.DocumentUpdate{
|
||||
Title: data.Title,
|
||||
Path: newPath,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return svc.GetOne(ctx, ctx.GID, itemId)
|
||||
}
|
||||
|
||||
// AttachmentAdd adds an attachment to an item by creating an entry in the Documents table and linking it to the Attachment
|
||||
// Table and Items table. The file provided via the reader is stored on the file system based on the provided
|
||||
// relative path during construction of the service.
|
||||
func (svc *ItemService) AttachmentAdd(ctx Context, itemId uuid.UUID, filename string, attachmentType attachment.Type, file io.Reader) (*types.ItemOut, error) {
|
||||
// Get the Item
|
||||
item, err := svc.repo.Items.GetOne(ctx, itemId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if item.Edges.Group.ID != ctx.GID {
|
||||
return nil, ErrNotOwner
|
||||
}
|
||||
|
||||
fp := svc.attachmentPath(ctx.GID, itemId, filename)
|
||||
filename = filepath.Base(fp)
|
||||
|
||||
// Create the document
|
||||
doc, err := svc.repo.Docs.Create(ctx, ctx.GID, types.DocumentCreate{
|
||||
Title: filename,
|
||||
Path: fp,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the attachment
|
||||
_, err = svc.repo.Attachments.Create(ctx, itemId, doc.ID, attachmentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read the contents and write them to a file on the file system
|
||||
err = os.MkdirAll(filepath.Dir(doc.Path), os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Create(doc.Path)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create file")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(f, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return svc.GetOne(ctx, ctx.GID, itemId)
|
||||
}
|
||||
|
||||
func (svc *ItemService) AttachmentDelete(ctx context.Context, gid, itemId, attachmentId uuid.UUID) error {
|
||||
// Get the Item
|
||||
item, err := svc.repo.Items.GetOne(ctx, itemId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.Edges.Group.ID != gid {
|
||||
return ErrNotOwner
|
||||
}
|
||||
|
||||
attachment, err := svc.repo.Attachments.Get(ctx, attachmentId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the attachment
|
||||
err = svc.repo.Attachments.Delete(ctx, attachmentId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove File
|
||||
err = os.Remove(attachment.Edges.Document.Path)
|
||||
|
||||
return err
|
||||
}
|
62
backend/internal/services/service_items_attachments_test.go
Normal file
62
backend/internal/services/service_items_attachments_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestItemService_AddAttachment(t *testing.T) {
|
||||
temp := os.TempDir()
|
||||
|
||||
svc := &ItemService{
|
||||
repo: tRepos,
|
||||
filepath: temp,
|
||||
}
|
||||
|
||||
loc, err := tSvc.Location.Create(context.Background(), tGroup.ID, types.LocationCreate{
|
||||
Description: "test",
|
||||
Name: "test",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, loc)
|
||||
|
||||
itmC := types.ItemCreate{
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(10),
|
||||
LocationID: loc.ID,
|
||||
}
|
||||
|
||||
itm, err := svc.Create(context.Background(), tGroup.ID, itmC)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, itm)
|
||||
t.Cleanup(func() {
|
||||
err := svc.repo.Items.Delete(context.Background(), itm.ID)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
contents := fk.Str(1000)
|
||||
reader := strings.NewReader(contents)
|
||||
|
||||
// Setup
|
||||
afterAttachment, err := svc.AttachmentAdd(tCtx, itm.ID, "testfile.txt", "attachment", reader)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, afterAttachment)
|
||||
|
||||
// Check that the file exists
|
||||
storedPath := afterAttachment.Attachments[0].Document.Path
|
||||
|
||||
// {root}/{group}/{item}/{attachment}
|
||||
assert.Equal(t, path.Join(temp, tGroup.ID.String(), itm.ID.String(), "testfile.txt"), storedPath)
|
||||
|
||||
// Check that the file contents are correct
|
||||
bts, err := os.ReadFile(storedPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, contents, string(bts))
|
||||
|
||||
}
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
var ErrInvalidCsv = errors.New("invalid csv")
|
||||
|
|
|
@ -2,13 +2,9 @@ package services
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -92,53 +88,3 @@ func TestItemService_CsvImport(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemService_AddAttachment(t *testing.T) {
|
||||
temp := os.TempDir()
|
||||
|
||||
svc := &ItemService{
|
||||
repo: tRepos,
|
||||
filepath: temp,
|
||||
}
|
||||
|
||||
loc, err := tSvc.Location.Create(context.Background(), tGroup.ID, types.LocationCreate{
|
||||
Description: "test",
|
||||
Name: "test",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, loc)
|
||||
|
||||
itmC := types.ItemCreate{
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(10),
|
||||
LocationID: loc.ID,
|
||||
}
|
||||
|
||||
itm, err := svc.Create(context.Background(), tGroup.ID, itmC)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, itm)
|
||||
t.Cleanup(func() {
|
||||
err := svc.repo.Items.Delete(context.Background(), itm.ID)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
contents := fk.Str(1000)
|
||||
reader := strings.NewReader(contents)
|
||||
|
||||
// Setup
|
||||
afterAttachment, err := svc.AddAttachment(context.Background(), tGroup.ID, itm.ID, "testfile.txt", reader)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, afterAttachment)
|
||||
|
||||
// Check that the file exists
|
||||
storedPath := afterAttachment.Attachments[0].Document.Path
|
||||
|
||||
// {root}/{group}/{item}/{attachment}
|
||||
assert.Equal(t, path.Join(temp, tGroup.ID.String(), itm.ID.String(), "testfile.txt"), storedPath)
|
||||
|
||||
// Check that the file contents are correct
|
||||
bts, err := os.ReadFile(storedPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, contents, string(bts))
|
||||
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/internal/repo"
|
||||
"github.com/hay-kot/content/backend/internal/services/mappers"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/services/mappers"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
type LabelService struct {
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/internal/repo"
|
||||
"github.com/hay-kot/content/backend/internal/services/mappers"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/services/mappers"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -6,10 +6,10 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/internal/repo"
|
||||
"github.com/hay-kot/content/backend/internal/services/mappers"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/hay-kot/content/backend/pkgs/hasher"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/services/mappers"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/hasher"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package services
|
||||
|
||||
import "github.com/hay-kot/content/backend/internal/types"
|
||||
import "github.com/hay-kot/homebox/backend/internal/types"
|
||||
|
||||
func defaultLocations() []types.LocationCreate {
|
||||
return []types.LocationCreate{
|
||||
|
|
|
@ -103,5 +103,16 @@ type ItemAttachment struct {
|
|||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Type string `json:"type"`
|
||||
Document DocumentOut `json:"document"`
|
||||
}
|
||||
|
||||
type ItemAttachmentToken struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type ItemAttachmentUpdate struct {
|
||||
ID uuid.UUID `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ type LabelUpdate struct {
|
|||
|
||||
type LabelSummary struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
GroupID uuid.UUID `json:"groupId"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue