refactor document storage location

This commit is contained in:
Hayden 2022-09-25 21:20:35 -08:00
parent 19e73378d3
commit 63acd7570f
9 changed files with 157 additions and 237 deletions

View file

@ -81,8 +81,8 @@ func run(cfg *config.Config) error {
} }
app.db = c app.db = c
app.repos = repo.EntAllRepos(c) app.repos = repo.EntAllRepos(c, cfg.Storage.Data)
app.services = services.NewServices(app.repos, cfg.Storage.Data) app.services = services.NewServices(app.repos)
// ========================================================================= // =========================================================================
// Start Server // Start Server

View file

@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/google/uuid" "github.com/google/uuid"
@ -105,7 +104,7 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
token := server.GetParam(r, "token", "") 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 { if err != nil {
log.Err(err).Msg("failed to get attachment") log.Err(err).Msg("failed to get attachment")
@ -113,9 +112,9 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
return 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") w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, path) http.ServeFile(w, r, doc.Path)
} }
} }

View file

@ -53,7 +53,7 @@ func TestMain(m *testing.M) {
} }
tClient = client tClient = client
tRepos = EntAllRepos(tClient) tRepos = EntAllRepos(tClient, os.TempDir())
defer client.Close() defer client.Close()
bootstrap() bootstrap()

View file

@ -2,25 +2,36 @@ package repo
import ( import (
"context" "context"
"errors"
"io"
"os"
"path/filepath"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent" "github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/ent/document" "github.com/hay-kot/homebox/backend/ent/document"
"github.com/hay-kot/homebox/backend/ent/group" "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 { 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) { type (
return r.db.Document.Create(). DocumentCreate struct {
SetGroupID(gid). Title string
SetTitle(doc.Title). Content io.Reader
SetPath(doc.Path). }
Save(ctx) )
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) { 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) { func (r *DocumentRepository) Get(ctx context.Context, id uuid.UUID) (*ent.Document, error) {
return r.db.Document.Query(). return r.db.Document.Get(ctx, id)
Where(document.ID(id)).
Only(ctx)
} }
func (r *DocumentRepository) Update(ctx context.Context, id uuid.UUID, doc types.DocumentUpdate) (*ent.Document, error) { func (r *DocumentRepository) Create(ctx context.Context, gid uuid.UUID, doc DocumentCreate) (*ent.Document, error) {
return r.db.Document.UpdateOneID(id). 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). 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) Save(ctx)
} }
func (r *DocumentRepository) Delete(ctx context.Context, id uuid.UUID) error { 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) return r.db.Document.DeleteOneID(id).Exec(ctx)
} }

View file

@ -1,100 +1,18 @@
package repo package repo
import ( import (
"bytes"
"context" "context"
"fmt"
"os"
"path/filepath"
"testing" "testing"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent" "github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/stretchr/testify/assert" "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 { func useDocs(t *testing.T, num int) []*ent.Document {
t.Helper() t.Helper()
@ -102,9 +20,9 @@ func useDocs(t *testing.T, num int) []*ent.Document {
ids := make([]uuid.UUID, 0, num) ids := make([]uuid.UUID, 0, num)
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
doc, err := tRepos.Docs.Create(context.Background(), tGroup.ID, types.DocumentCreate{ doc, err := tRepos.Docs.Create(context.Background(), tGroup.ID, DocumentCreate{
Title: fk.Str(10), Title: fk.Str(10) + ".md",
Path: fk.Path(), Content: bytes.NewReader([]byte(fk.Str(10))),
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -126,77 +44,68 @@ func useDocs(t *testing.T, num int) []*ent.Document {
return results return results
} }
func TestDocumentRepository_GetAll(t *testing.T) { func TestDocumentRepository_CreateUpdateDelete(t *testing.T) {
entities := useDocs(t, 10) temp := t.TempDir()
r := DocumentRepository{
for _, entity := range entities { db: tClient,
assert.NotNil(t, entity) dir: temp,
} }
all, err := tRepos.Docs.GetAll(context.Background(), tGroup.ID) type args struct {
assert.NoError(t, err) 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) ensureRead := func() {
for _, entity := range all { // Read Document
assert.NotNil(t, entity) bts, err := os.ReadFile(got.Path)
assert.NoError(t, err)
for _, e := range entities { assert.Equal(t, tt.content, string(bts))
if e.ID == entity.ID {
assert.Equal(t, e.Title, entity.Title)
assert.Equal(t, e.Path, entity.Path)
} }
} ensureRead()
}
} // Update Document
got, err = r.Rename(tt.args.ctx, got.ID, "__"+tt.title+"__")
func TestDocumentRepository_Get(t *testing.T) { assert.NoError(t, err)
entities := useDocs(t, 10) assert.Equal(t, "__"+tt.title+"__", got.Title)
for _, entity := range entities { ensureRead()
got, err := tRepos.Docs.Get(context.Background(), entity.ID)
// Delete Document
assert.NoError(t, err) err = r.Delete(tt.args.ctx, got.ID)
assert.Equal(t, entity.ID, got.ID) assert.NoError(t, err)
assert.Equal(t, entity.Title, got.Title)
assert.Equal(t, entity.Path, got.Path) _, err = os.Stat(got.Path)
} assert.Error(t, err)
} })
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)
} }
} }

View file

@ -15,7 +15,7 @@ type AllRepos struct {
Attachments *AttachmentRepo Attachments *AttachmentRepo
} }
func EntAllRepos(db *ent.Client) *AllRepos { func EntAllRepos(db *ent.Client, root string) *AllRepos {
return &AllRepos{ return &AllRepos{
Users: &UserRepository{db}, Users: &UserRepository{db},
AuthTokens: &TokenRepository{db}, AuthTokens: &TokenRepository{db},
@ -23,7 +23,7 @@ func EntAllRepos(db *ent.Client) *AllRepos {
Locations: &LocationRepository{db}, Locations: &LocationRepository{db},
Labels: &LabelRepository{db}, Labels: &LabelRepository{db},
Items: &ItemsRepository{db}, Items: &ItemsRepository{db},
Docs: &DocumentRepository{db}, Docs: &DocumentRepository{db, root},
DocTokens: &DocumentTokensRepository{db}, DocTokens: &DocumentTokensRepository{db},
Attachments: &AttachmentRepo{db}, Attachments: &AttachmentRepo{db},
} }

View file

@ -10,13 +10,10 @@ type AllServices struct {
Items *ItemService Items *ItemService
} }
func NewServices(repos *repo.AllRepos, root string) *AllServices { func NewServices(repos *repo.AllRepos) *AllServices {
if repos == nil { if repos == nil {
panic("repos cannot be nil") panic("repos cannot be nil")
} }
if root == "" {
panic("root cannot be empty")
}
return &AllServices{ return &AllServices{
User: &UserService{repos}, User: &UserService{repos},
@ -24,9 +21,8 @@ func NewServices(repos *repo.AllRepos, root string) *AllServices {
Location: &LocationService{repos}, Location: &LocationService{repos},
Labels: &LabelService{repos}, Labels: &LabelService{repos},
Items: &ItemService{ Items: &ItemService{
repo: repos, repo: repos,
filepath: root, at: attachmentTokens{},
at: attachmentTokens{},
}, },
} }
} }

View file

@ -63,11 +63,10 @@ func TestMain(m *testing.M) {
} }
tClient = client tClient = client
tRepos = repo.EntAllRepos(tClient) tRepos = repo.EntAllRepos(tClient, os.TempDir()+"/homebox")
tSvc = NewServices(tRepos, "/tmp/homebox") tSvc = NewServices(tRepos)
defer client.Close() defer client.Close()
bootstrap() bootstrap()
tCtx = Context{ tCtx = Context{
Context: context.Background(), Context: context.Background(),

View file

@ -8,7 +8,9 @@ import (
"time" "time"
"github.com/google/uuid" "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/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/types" "github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/hasher" "github.com/hay-kot/homebox/backend/pkgs/hasher"
"github.com/hay-kot/homebox/backend/pkgs/pathlib" "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 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) attachmentId, ok := svc.at.Get(token)
if !ok { if !ok {
return "", ErrNotFound return nil, ErrNotFound
} }
attachment, err := svc.repo.Attachments.Get(ctx, attachmentId) attachment, err := svc.repo.Attachments.Get(ctx, attachmentId)
if err != nil { 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) { 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)) attachment, err := svc.repo.Attachments.Update(ctx, data.ID, attachment.Type(data.Type))
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Update Document
attDoc := attachment.Edges.Document attDoc := attachment.Edges.Document
_, err = svc.repo.Docs.Rename(ctx, attDoc.ID, data.Title)
if data.Title != attachment.Edges.Document.Title { if err != nil {
newPath := pathlib.Safe(svc.attachmentPath(ctx.GID, itemId, data.Title)) return nil, err
// 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) 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 return nil, ErrNotOwner
} }
fp := svc.attachmentPath(ctx.GID, itemId, filename)
filename = filepath.Base(fp)
// Create the document // Create the document
doc, err := svc.repo.Docs.Create(ctx, ctx.GID, types.DocumentCreate{ doc, err := svc.repo.Docs.Create(ctx, ctx.GID, repo.DocumentCreate{Title: filename, Content: file})
Title: filename,
Path: fp,
})
if err != nil { if err != nil {
log.Err(err).Msg("failed to create document")
return nil, err return nil, err
} }
// Create the attachment // Create the attachment
_, err = svc.repo.Attachments.Create(ctx, itemId, doc.ID, attachmentType) _, err = svc.repo.Attachments.Create(ctx, itemId, doc.ID, attachmentType)
if err != nil { if err != nil {
return nil, err log.Err(err).Msg("failed to create attachment")
}
// 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 nil, err
} }