From 63acd7570f726917323c33cbc61d41e75cfd81e8 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 25 Sep 2022 21:20:35 -0800 Subject: [PATCH] refactor document storage location --- backend/app/api/main.go | 4 +- .../app/api/v1/v1_ctrl_items_attachments.go | 7 +- backend/internal/repo/main_test.go | 2 +- backend/internal/repo/repo_documents.go | 79 +++++-- backend/internal/repo/repo_documents_test.go | 223 ++++++------------ backend/internal/repo/repos_all.go | 4 +- backend/internal/services/all.go | 10 +- backend/internal/services/main_test.go | 5 +- .../services/service_items_attachments.go | 60 ++--- 9 files changed, 157 insertions(+), 237 deletions(-) diff --git a/backend/app/api/main.go b/backend/app/api/main.go index 5d233e7..bd833b3 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -81,8 +81,8 @@ func run(cfg *config.Config) error { } app.db = c - app.repos = repo.EntAllRepos(c) - app.services = services.NewServices(app.repos, cfg.Storage.Data) + app.repos = repo.EntAllRepos(c, cfg.Storage.Data) + app.services = services.NewServices(app.repos) // ========================================================================= // Start Server diff --git a/backend/app/api/v1/v1_ctrl_items_attachments.go b/backend/app/api/v1/v1_ctrl_items_attachments.go index b373ead..002f415 100644 --- a/backend/app/api/v1/v1_ctrl_items_attachments.go +++ b/backend/app/api/v1/v1_ctrl_items_attachments.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/http" - "path/filepath" "github.com/go-chi/chi/v5" "github.com/google/uuid" @@ -105,7 +104,7 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := server.GetParam(r, "token", "") - path, err := ctrl.svc.Items.AttachmentPath(r.Context(), token) + doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), token) if err != nil { log.Err(err).Msg("failed to get attachment") @@ -113,9 +112,9 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc { return } - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(path))) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", doc.Title)) w.Header().Set("Content-Type", "application/octet-stream") - http.ServeFile(w, r, path) + http.ServeFile(w, r, doc.Path) } } diff --git a/backend/internal/repo/main_test.go b/backend/internal/repo/main_test.go index 14bfbd8..5e84968 100644 --- a/backend/internal/repo/main_test.go +++ b/backend/internal/repo/main_test.go @@ -53,7 +53,7 @@ func TestMain(m *testing.M) { } tClient = client - tRepos = EntAllRepos(tClient) + tRepos = EntAllRepos(tClient, os.TempDir()) defer client.Close() bootstrap() diff --git a/backend/internal/repo/repo_documents.go b/backend/internal/repo/repo_documents.go index e91b9a6..34cf088 100644 --- a/backend/internal/repo/repo_documents.go +++ b/backend/internal/repo/repo_documents.go @@ -2,25 +2,36 @@ package repo import ( "context" + "errors" + "io" + "os" + "path/filepath" "github.com/google/uuid" "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" + "github.com/hay-kot/homebox/backend/pkgs/pathlib" +) + +var ( + ErrInvalidDocExtension = errors.New("invalid document extension") ) -// DocumentRepository is a repository for Document entity type DocumentRepository struct { - db *ent.Client + db *ent.Client + dir string } -func (r *DocumentRepository) Create(ctx context.Context, gid uuid.UUID, doc types.DocumentCreate) (*ent.Document, error) { - return r.db.Document.Create(). - SetGroupID(gid). - SetTitle(doc.Title). - SetPath(doc.Path). - Save(ctx) +type ( + DocumentCreate struct { + Title string + Content io.Reader + } +) + +func (r *DocumentRepository) path(gid uuid.UUID, ext string) string { + return pathlib.Safe(filepath.Join(r.dir, gid.String(), "documents", uuid.NewString()+ext)) } func (r *DocumentRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]*ent.Document, error) { @@ -30,18 +41,56 @@ func (r *DocumentRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]*ent. } func (r *DocumentRepository) Get(ctx context.Context, id uuid.UUID) (*ent.Document, error) { - return r.db.Document.Query(). - Where(document.ID(id)). - Only(ctx) + return r.db.Document.Get(ctx, id) } -func (r *DocumentRepository) Update(ctx context.Context, id uuid.UUID, doc types.DocumentUpdate) (*ent.Document, error) { - return r.db.Document.UpdateOneID(id). +func (r *DocumentRepository) Create(ctx context.Context, gid uuid.UUID, doc DocumentCreate) (*ent.Document, error) { + ext := filepath.Ext(doc.Title) + if ext == "" { + return nil, ErrInvalidDocExtension + } + + path := r.path(gid, ext) + + parent := filepath.Dir(path) + err := os.MkdirAll(parent, 0755) + if err != nil { + return nil, err + } + + f, err := os.Create(path) + if err != nil { + return nil, err + } + + _, err = io.Copy(f, doc.Content) + if err != nil { + return nil, err + } + + return r.db.Document.Create(). + SetGroupID(gid). SetTitle(doc.Title). - SetPath(doc.Path). + SetPath(path). + Save(ctx) +} + +func (r *DocumentRepository) Rename(ctx context.Context, id uuid.UUID, title string) (*ent.Document, error) { + return r.db.Document.UpdateOneID(id). + SetTitle(title). Save(ctx) } func (r *DocumentRepository) Delete(ctx context.Context, id uuid.UUID) error { + doc, err := r.db.Document.Get(ctx, id) + if err != nil { + return err + } + + err = os.Remove(doc.Path) + if err != nil { + return err + } + return r.db.Document.DeleteOneID(id).Exec(ctx) } diff --git a/backend/internal/repo/repo_documents_test.go b/backend/internal/repo/repo_documents_test.go index df5cd7b..bb1467a 100644 --- a/backend/internal/repo/repo_documents_test.go +++ b/backend/internal/repo/repo_documents_test.go @@ -1,100 +1,18 @@ package repo import ( + "bytes" "context" + "fmt" + "os" + "path/filepath" "testing" "github.com/google/uuid" "github.com/hay-kot/homebox/backend/ent" - "github.com/hay-kot/homebox/backend/internal/types" "github.com/stretchr/testify/assert" ) -func TestDocumentRepository_Create(t *testing.T) { - type args struct { - ctx context.Context - gid uuid.UUID - doc types.DocumentCreate - } - tests := []struct { - name string - args args - want *ent.Document - wantErr bool - }{ - { - name: "create document", - args: args{ - ctx: context.Background(), - gid: tGroup.ID, - doc: types.DocumentCreate{ - Title: "test document", - Path: "/test/document", - }, - }, - want: &ent.Document{ - Title: "test document", - Path: "/test/document", - }, - wantErr: false, - }, - { - name: "create document with empty title", - args: args{ - ctx: context.Background(), - gid: tGroup.ID, - doc: types.DocumentCreate{ - Title: "", - Path: "/test/document", - }, - }, - want: nil, - wantErr: true, - }, - { - name: "create document with empty path", - args: args{ - ctx: context.Background(), - gid: tGroup.ID, - doc: types.DocumentCreate{ - Title: "test document", - Path: "", - }, - }, - want: nil, - wantErr: true, - }, - } - ids := make([]uuid.UUID, 0, len(tests)) - - t.Cleanup(func() { - for _, id := range ids { - err := tRepos.Docs.Delete(context.Background(), id) - assert.NoError(t, err) - } - }) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := tRepos.Docs.Create(tt.args.ctx, tt.args.gid, tt.args.doc) - if (err != nil) != tt.wantErr { - t.Errorf("DocumentRepository.Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if tt.wantErr { - assert.Error(t, err) - assert.Nil(t, got) - return - } - - assert.Equal(t, tt.want.Title, got.Title) - assert.Equal(t, tt.want.Path, got.Path) - ids = append(ids, got.ID) - }) - } -} - func useDocs(t *testing.T, num int) []*ent.Document { t.Helper() @@ -102,9 +20,9 @@ func useDocs(t *testing.T, num int) []*ent.Document { ids := make([]uuid.UUID, 0, num) for i := 0; i < num; i++ { - doc, err := tRepos.Docs.Create(context.Background(), tGroup.ID, types.DocumentCreate{ - Title: fk.Str(10), - Path: fk.Path(), + doc, err := tRepos.Docs.Create(context.Background(), tGroup.ID, DocumentCreate{ + Title: fk.Str(10) + ".md", + Content: bytes.NewReader([]byte(fk.Str(10))), }) assert.NoError(t, err) @@ -126,77 +44,68 @@ func useDocs(t *testing.T, num int) []*ent.Document { return results } -func TestDocumentRepository_GetAll(t *testing.T) { - entities := useDocs(t, 10) - - for _, entity := range entities { - assert.NotNil(t, entity) +func TestDocumentRepository_CreateUpdateDelete(t *testing.T) { + temp := t.TempDir() + r := DocumentRepository{ + db: tClient, + dir: temp, } - all, err := tRepos.Docs.GetAll(context.Background(), tGroup.ID) - assert.NoError(t, err) + type args struct { + ctx context.Context + gid uuid.UUID + doc DocumentCreate + } + tests := []struct { + name string + content string + args args + title string + wantErr bool + }{ + { + name: "basic create", + title: "test.md", + content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + args: args{ + ctx: context.Background(), + gid: tGroup.ID, + doc: DocumentCreate{ + Title: "test.md", + Content: bytes.NewReader([]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create Document + got, err := r.Create(tt.args.ctx, tt.args.gid, tt.args.doc) + assert.NoError(t, err) + assert.Equal(t, tt.title, got.Title) + assert.Equal(t, fmt.Sprintf("%s/%s/documents", temp, tt.args.gid), filepath.Dir(got.Path)) - assert.Len(t, all, 10) - for _, entity := range all { - assert.NotNil(t, entity) - - for _, e := range entities { - if e.ID == entity.ID { - assert.Equal(t, e.Title, entity.Title) - assert.Equal(t, e.Path, entity.Path) + ensureRead := func() { + // Read Document + bts, err := os.ReadFile(got.Path) + assert.NoError(t, err) + assert.Equal(t, tt.content, string(bts)) } - } - } -} - -func TestDocumentRepository_Get(t *testing.T) { - entities := useDocs(t, 10) - - for _, entity := range entities { - got, err := tRepos.Docs.Get(context.Background(), entity.ID) - - assert.NoError(t, err) - assert.Equal(t, entity.ID, got.ID) - assert.Equal(t, entity.Title, got.Title) - assert.Equal(t, entity.Path, got.Path) - } -} - -func TestDocumentRepository_Update(t *testing.T) { - entities := useDocs(t, 10) - - for _, entity := range entities { - got, err := tRepos.Docs.Get(context.Background(), entity.ID) - - assert.NoError(t, err) - assert.Equal(t, entity.ID, got.ID) - assert.Equal(t, entity.Title, got.Title) - assert.Equal(t, entity.Path, got.Path) - } - - for _, entity := range entities { - updateData := types.DocumentUpdate{ - Title: fk.Str(10), - Path: fk.Path(), - } - - updated, err := tRepos.Docs.Update(context.Background(), entity.ID, updateData) - - assert.NoError(t, err) - assert.Equal(t, entity.ID, updated.ID) - assert.Equal(t, updateData.Title, updated.Title) - assert.Equal(t, updateData.Path, updated.Path) - } -} - -func TestDocumentRepository_Delete(t *testing.T) { - entities := useDocs(t, 10) - - for _, entity := range entities { - err := tRepos.Docs.Delete(context.Background(), entity.ID) - assert.NoError(t, err) - - _, err = tRepos.Docs.Get(context.Background(), entity.ID) - assert.Error(t, err) + ensureRead() + + // Update Document + got, err = r.Rename(tt.args.ctx, got.ID, "__"+tt.title+"__") + assert.NoError(t, err) + assert.Equal(t, "__"+tt.title+"__", got.Title) + + ensureRead() + + // Delete Document + err = r.Delete(tt.args.ctx, got.ID) + assert.NoError(t, err) + + _, err = os.Stat(got.Path) + assert.Error(t, err) + }) } } diff --git a/backend/internal/repo/repos_all.go b/backend/internal/repo/repos_all.go index c47f5a4..88ca1e4 100644 --- a/backend/internal/repo/repos_all.go +++ b/backend/internal/repo/repos_all.go @@ -15,7 +15,7 @@ type AllRepos struct { Attachments *AttachmentRepo } -func EntAllRepos(db *ent.Client) *AllRepos { +func EntAllRepos(db *ent.Client, root string) *AllRepos { return &AllRepos{ Users: &UserRepository{db}, AuthTokens: &TokenRepository{db}, @@ -23,7 +23,7 @@ func EntAllRepos(db *ent.Client) *AllRepos { Locations: &LocationRepository{db}, Labels: &LabelRepository{db}, Items: &ItemsRepository{db}, - Docs: &DocumentRepository{db}, + Docs: &DocumentRepository{db, root}, DocTokens: &DocumentTokensRepository{db}, Attachments: &AttachmentRepo{db}, } diff --git a/backend/internal/services/all.go b/backend/internal/services/all.go index e255e28..bcae1d9 100644 --- a/backend/internal/services/all.go +++ b/backend/internal/services/all.go @@ -10,13 +10,10 @@ type AllServices struct { Items *ItemService } -func NewServices(repos *repo.AllRepos, root string) *AllServices { +func NewServices(repos *repo.AllRepos) *AllServices { if repos == nil { panic("repos cannot be nil") } - if root == "" { - panic("root cannot be empty") - } return &AllServices{ User: &UserService{repos}, @@ -24,9 +21,8 @@ func NewServices(repos *repo.AllRepos, root string) *AllServices { Location: &LocationService{repos}, Labels: &LabelService{repos}, Items: &ItemService{ - repo: repos, - filepath: root, - at: attachmentTokens{}, + repo: repos, + at: attachmentTokens{}, }, } } diff --git a/backend/internal/services/main_test.go b/backend/internal/services/main_test.go index df0369f..dbae4f8 100644 --- a/backend/internal/services/main_test.go +++ b/backend/internal/services/main_test.go @@ -63,11 +63,10 @@ func TestMain(m *testing.M) { } tClient = client - tRepos = repo.EntAllRepos(tClient) - tSvc = NewServices(tRepos, "/tmp/homebox") + tRepos = repo.EntAllRepos(tClient, os.TempDir()+"/homebox") + tSvc = NewServices(tRepos) defer client.Close() - bootstrap() tCtx = Context{ Context: context.Background(), diff --git a/backend/internal/services/service_items_attachments.go b/backend/internal/services/service_items_attachments.go index 9969ee9..c6839b9 100644 --- a/backend/internal/services/service_items_attachments.go +++ b/backend/internal/services/service_items_attachments.go @@ -8,7 +8,9 @@ import ( "time" "github.com/google/uuid" + "github.com/hay-kot/homebox/backend/ent" "github.com/hay-kot/homebox/backend/ent/attachment" + "github.com/hay-kot/homebox/backend/internal/repo" "github.com/hay-kot/homebox/backend/internal/types" "github.com/hay-kot/homebox/backend/pkgs/hasher" "github.com/hay-kot/homebox/backend/pkgs/pathlib" @@ -74,45 +76,32 @@ func (svc *ItemService) attachmentPath(gid, itemId uuid.UUID, filename string) s return path } -func (svc *ItemService) AttachmentPath(ctx context.Context, token string) (string, error) { +func (svc *ItemService) AttachmentPath(ctx context.Context, token string) (*ent.Document, error) { attachmentId, ok := svc.at.Get(token) if !ok { - return "", ErrNotFound + return nil, ErrNotFound } attachment, err := svc.repo.Attachments.Get(ctx, attachmentId) if err != nil { - return "", err + return nil, err } - return attachment.Edges.Document.Path, nil + return attachment.Edges.Document, nil } func (svc *ItemService) AttachmentUpdate(ctx Context, itemId uuid.UUID, data *types.ItemAttachmentUpdate) (*types.ItemOut, error) { - // Update Properties + // Update Attachment attachment, err := svc.repo.Attachments.Update(ctx, data.ID, attachment.Type(data.Type)) if err != nil { return nil, err } + // Update Document 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 - } + _, err = svc.repo.Docs.Rename(ctx, attDoc.ID, data.Title) + if err != nil { + return nil, err } return svc.GetOne(ctx, ctx.GID, itemId) @@ -132,38 +121,17 @@ func (svc *ItemService) AttachmentAdd(ctx Context, itemId uuid.UUID, filename st 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, - }) + doc, err := svc.repo.Docs.Create(ctx, ctx.GID, repo.DocumentCreate{Title: filename, Content: file}) if err != nil { + log.Err(err).Msg("failed to create document") 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 { + log.Err(err).Msg("failed to create attachment") return nil, err }