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.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

View file

@ -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)
}
}

View file

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

View file

@ -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
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)
}

View file

@ -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)
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))
ensureRead := func() {
// Read Document
bts, err := os.ReadFile(got.Path)
assert.NoError(t, err)
assert.Equal(t, tt.content, string(bts))
}
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)
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)
}
}
}
}
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)
_, err = os.Stat(got.Path)
assert.Error(t, err)
})
}
}

View file

@ -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},
}

View file

@ -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},
@ -25,7 +22,6 @@ func NewServices(repos *repo.AllRepos, root string) *AllServices {
Labels: &LabelService{repos},
Items: &ItemService{
repo: repos,
filepath: root,
at: attachmentTokens{},
},
}

View file

@ -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(),

View file

@ -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,47 +76,34 @@ 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)
_, err = svc.repo.Docs.Rename(ctx, attDoc.ID, data.Title)
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)
}
@ -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
}