mirror of
https://github.com/hay-kot/homebox.git
synced 2025-08-03 08:10:28 +00:00
refactor document storage location
This commit is contained in:
parent
19e73378d3
commit
63acd7570f
9 changed files with 157 additions and 237 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ func TestMain(m *testing.M) {
|
|||
}
|
||||
|
||||
tClient = client
|
||||
tRepos = EntAllRepos(tClient)
|
||||
tRepos = EntAllRepos(tClient, os.TempDir())
|
||||
defer client.Close()
|
||||
|
||||
bootstrap()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
|
|
|
@ -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{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue