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
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue