From 6984bb209e4e1651ba3c17b5e167788b68c5a932 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Sun, 30 Nov 2025 01:16:44 +0000 Subject: [PATCH] refactor: node deletion --- apps/backend/internal/blob/fs_store.go | 9 ++ apps/backend/internal/blob/store.go | 1 + apps/backend/internal/drexa/server.go | 4 +- apps/backend/internal/upload/http.go | 10 +- apps/backend/internal/upload/service.go | 1 + apps/backend/internal/virtualfs/err.go | 5 +- .../internal/virtualfs/flat_key_resolver.go | 35 +++++++ .../virtualfs/hierarchical_key_resolver.go | 39 ++++++++ .../internal/virtualfs/key_resolver.go | 46 +-------- apps/backend/internal/virtualfs/vfs.go | 94 +++++++++++++------ 10 files changed, 165 insertions(+), 79 deletions(-) create mode 100644 apps/backend/internal/virtualfs/flat_key_resolver.go create mode 100644 apps/backend/internal/virtualfs/hierarchical_key_resolver.go diff --git a/apps/backend/internal/blob/fs_store.go b/apps/backend/internal/blob/fs_store.go index bb8c645..304a4f1 100644 --- a/apps/backend/internal/blob/fs_store.go +++ b/apps/backend/internal/blob/fs_store.go @@ -111,6 +111,15 @@ func (s *FSStore) Delete(ctx context.Context, key Key) error { return nil } +func (s *FSStore) DeletePrefix(ctx context.Context, prefix Key) error { + prefixPath := filepath.Join(s.config.Root, string(prefix)) + err := os.RemoveAll(prefixPath) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + func (s *FSStore) Update(ctx context.Context, key Key, opts UpdateOptions) error { // Update is a no-op for FSStore return nil diff --git a/apps/backend/internal/blob/store.go b/apps/backend/internal/blob/store.go index 0c9c243..caf2c15 100644 --- a/apps/backend/internal/blob/store.go +++ b/apps/backend/internal/blob/store.go @@ -20,6 +20,7 @@ type Store interface { Put(ctx context.Context, key Key, reader io.Reader) error Update(ctx context.Context, key Key, opts UpdateOptions) error Delete(ctx context.Context, key Key) error + DeletePrefix(ctx context.Context, prefix Key) error Move(ctx context.Context, srcKey, dstKey Key) error Read(ctx context.Context, key Key) (io.ReadCloser, error) ReadRange(ctx context.Context, key Key, offset, length int64) (io.ReadCloser, error) diff --git a/apps/backend/internal/drexa/server.go b/apps/backend/internal/drexa/server.go index f2bdc8e..3cd0d0e 100644 --- a/apps/backend/internal/drexa/server.go +++ b/apps/backend/internal/drexa/server.go @@ -62,9 +62,11 @@ func NewServer(c Config) (*fiber.App, error) { }) uploadService := upload.NewService(vfs, blobStore) + authMiddleware := auth.NewBearerAuthMiddleware(authService, db) + api := app.Group("/api") auth.NewHTTPHandler(authService, db).RegisterRoutes(api) - upload.NewHTTPHandler(uploadService).RegisterRoutes(api) + upload.NewHTTPHandler(uploadService, authMiddleware).RegisterRoutes(api, authMiddleware) return app, nil } diff --git a/apps/backend/internal/upload/http.go b/apps/backend/internal/upload/http.go index d0d8191..fd7aa7f 100644 --- a/apps/backend/internal/upload/http.go +++ b/apps/backend/internal/upload/http.go @@ -17,15 +17,17 @@ type updateUploadRequest struct { } type HTTPHandler struct { - service *Service + service *Service + authMiddleware fiber.Handler } -func NewHTTPHandler(s *Service) *HTTPHandler { - return &HTTPHandler{service: s} +func NewHTTPHandler(s *Service, authMiddleware fiber.Handler) *HTTPHandler { + return &HTTPHandler{service: s, authMiddleware: authMiddleware} } -func (h *HTTPHandler) RegisterRoutes(api fiber.Router) { +func (h *HTTPHandler) RegisterRoutes(api fiber.Router, authMiddleware fiber.Handler) { upload := api.Group("/uploads") + upload.Use(authMiddleware) upload.Post("/", h.Create) upload.Put("/:uploadID/content", h.ReceiveContent) diff --git a/apps/backend/internal/upload/service.go b/apps/backend/internal/upload/service.go index a01af1b..0a5a742 100644 --- a/apps/backend/internal/upload/service.go +++ b/apps/backend/internal/upload/service.go @@ -61,6 +61,7 @@ func (s *Service) CreateUpload(ctx context.Context, userID uuid.UUID, opts Creat Duration: 1 * time.Hour, }) if err != nil { + _ = s.vfs.PermanentlyDeleteNode(ctx, node) return nil, err } diff --git a/apps/backend/internal/virtualfs/err.go b/apps/backend/internal/virtualfs/err.go index ea5517f..e68ca17 100644 --- a/apps/backend/internal/virtualfs/err.go +++ b/apps/backend/internal/virtualfs/err.go @@ -3,6 +3,7 @@ package virtualfs import "errors" var ( - ErrNodeNotFound = errors.New("node not found") - ErrNodeConflict = errors.New("node conflict") + ErrNodeNotFound = errors.New("node not found") + ErrNodeConflict = errors.New("node conflict") + ErrUnsupportedOperation = errors.New("unsupported operation") ) diff --git a/apps/backend/internal/virtualfs/flat_key_resolver.go b/apps/backend/internal/virtualfs/flat_key_resolver.go new file mode 100644 index 0000000..609d847 --- /dev/null +++ b/apps/backend/internal/virtualfs/flat_key_resolver.go @@ -0,0 +1,35 @@ +package virtualfs + +import ( + "context" + + "github.com/get-drexa/drexa/internal/blob" + "github.com/google/uuid" +) + +type FlatKeyResolver struct{} + +var _ BlobKeyResolver = &FlatKeyResolver{} + +func NewFlatKeyResolver() *FlatKeyResolver { + return &FlatKeyResolver{} +} + +func (r *FlatKeyResolver) KeyMode() blob.KeyMode { + return blob.KeyModeStable +} + +func (r *FlatKeyResolver) Resolve(ctx context.Context, node *Node) (blob.Key, error) { + if node.BlobKey == "" { + id, err := uuid.NewV7() + if err != nil { + return "", err + } + return blob.Key(id.String()), nil + } + return node.BlobKey, nil +} + +func (r *FlatKeyResolver) ResolveDeletionKeys(ctx context.Context, node *Node, allKeys []blob.Key) (*DeletionPlan, error) { + return &DeletionPlan{Keys: allKeys}, nil +} diff --git a/apps/backend/internal/virtualfs/hierarchical_key_resolver.go b/apps/backend/internal/virtualfs/hierarchical_key_resolver.go new file mode 100644 index 0000000..e29986c --- /dev/null +++ b/apps/backend/internal/virtualfs/hierarchical_key_resolver.go @@ -0,0 +1,39 @@ +package virtualfs + +import ( + "context" + + "github.com/get-drexa/drexa/internal/blob" + "github.com/uptrace/bun" +) + +type HierarchicalKeyResolver struct { + db *bun.DB +} + +var _ BlobKeyResolver = &HierarchicalKeyResolver{} + +func NewHierarchicalKeyResolver(db *bun.DB) *HierarchicalKeyResolver { + return &HierarchicalKeyResolver{db: db} +} + +func (r *HierarchicalKeyResolver) KeyMode() blob.KeyMode { + return blob.KeyModeDerived +} + +func (r *HierarchicalKeyResolver) Resolve(ctx context.Context, node *Node) (blob.Key, error) { + path, err := buildNodeAbsolutePath(ctx, r.db, node.ID) + if err != nil { + return "", err + } + + return blob.Key(path), nil +} + +func (r *HierarchicalKeyResolver) ResolveDeletionKeys(ctx context.Context, node *Node, allKeys []blob.Key) (*DeletionPlan, error) { + path, err := buildNodeAbsolutePath(ctx, r.db, node.ID) + if err != nil { + return nil, err + } + return &DeletionPlan{Prefix: blob.Key(path)}, nil +} diff --git a/apps/backend/internal/virtualfs/key_resolver.go b/apps/backend/internal/virtualfs/key_resolver.go index 6ceaa51..60b1c60 100644 --- a/apps/backend/internal/virtualfs/key_resolver.go +++ b/apps/backend/internal/virtualfs/key_resolver.go @@ -4,53 +4,15 @@ import ( "context" "github.com/get-drexa/drexa/internal/blob" - "github.com/google/uuid" - "github.com/uptrace/bun" ) type BlobKeyResolver interface { KeyMode() blob.KeyMode Resolve(ctx context.Context, node *Node) (blob.Key, error) + ResolveDeletionKeys(ctx context.Context, node *Node, allKeys []blob.Key) (*DeletionPlan, error) } -type FlatKeyResolver struct{} - -func NewFlatKeyResolver() *FlatKeyResolver { - return &FlatKeyResolver{} -} - -func (r *FlatKeyResolver) KeyMode() blob.KeyMode { - return blob.KeyModeStable -} - -func (r *FlatKeyResolver) Resolve(ctx context.Context, node *Node) (blob.Key, error) { - if node.BlobKey == "" { - id, err := uuid.NewV7() - if err != nil { - return "", err - } - return blob.Key(id.String()), nil - } - return node.BlobKey, nil -} - -type HierarchicalKeyResolver struct { - db *bun.DB -} - -func NewHierarchicalKeyResolver(db *bun.DB) *HierarchicalKeyResolver { - return &HierarchicalKeyResolver{db: db} -} - -func (r *HierarchicalKeyResolver) KeyMode() blob.KeyMode { - return blob.KeyModeDerived -} - -func (r *HierarchicalKeyResolver) Resolve(ctx context.Context, node *Node) (blob.Key, error) { - path, err := buildNodeAbsolutePath(ctx, r.db, node.ID) - if err != nil { - return "", err - } - - return blob.Key(path), nil +type DeletionPlan struct { + Prefix blob.Key + Keys []blob.Key } diff --git a/apps/backend/internal/virtualfs/vfs.go b/apps/backend/internal/virtualfs/vfs.go index b006247..921a4ef 100644 --- a/apps/backend/internal/virtualfs/vfs.go +++ b/apps/backend/internal/virtualfs/vfs.go @@ -385,7 +385,31 @@ func (vfs *VirtualFS) PermanentlyDeleteNode(ctx context.Context, node *Node) err if !node.IsAccessible() { return ErrNodeNotFound } + switch node.Kind { + case NodeKindFile: + return vfs.permanentlyDeleteFileNode(ctx, node) + case NodeKindDirectory: + return vfs.permanentlyDeleteDirectoryNode(ctx, node) + default: + return ErrUnsupportedOperation + } +} +func (vfs *VirtualFS) permanentlyDeleteFileNode(ctx context.Context, node *Node) error { + err := vfs.blobStore.Delete(ctx, node.BlobKey) + if err != nil { + return err + } + + _, err = vfs.db.NewDelete().Model(node).WherePK().Exec(ctx) + if err != nil { + return err + } + + return nil +} + +func (vfs *VirtualFS) permanentlyDeleteDirectoryNode(ctx context.Context, node *Node) error { const descendantsQuery = `WITH RECURSIVE descendants AS ( SELECT id, blob_key FROM vfs_nodes WHERE id = ? UNION ALL @@ -399,46 +423,56 @@ func (vfs *VirtualFS) PermanentlyDeleteNode(ctx context.Context, node *Node) err BlobKey blob.Key `bun:"blob_key"` } - var blobKeys []blob.Key + tx, err := vfs.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() - err := vfs.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { - var records []nodeRecord - err := tx.NewRaw(descendantsQuery, node.ID).Scan(ctx, &records) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return ErrNodeNotFound - } - return err - } - - if len(records) == 0 { + var records []nodeRecord + err = tx.NewRaw(descendantsQuery, node.ID).Scan(ctx, &records) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { return ErrNodeNotFound } - - nodeIDs := make([]uuid.UUID, 0, len(records)) - for _, r := range records { - nodeIDs = append(nodeIDs, r.ID) - if !r.BlobKey.IsNil() { - blobKeys = append(blobKeys, r.BlobKey) - } - } - - _, err = tx.NewDelete(). - Model((*Node)(nil)). - Where("id IN (?)", bun.In(nodeIDs)). - Exec(ctx) return err - }) + } + + if len(records) == 0 { + return ErrNodeNotFound + } + + nodeIDs := make([]uuid.UUID, 0, len(records)) + blobKeys := make([]blob.Key, 0, len(records)) + for _, r := range records { + nodeIDs = append(nodeIDs, r.ID) + if !r.BlobKey.IsNil() { + blobKeys = append(blobKeys, r.BlobKey) + } + } + + plan, err := vfs.keyResolver.ResolveDeletionKeys(ctx, node, blobKeys) if err != nil { return err } - // Delete blobs outside transaction (best effort) - for _, key := range blobKeys { - _ = vfs.blobStore.Delete(ctx, key) + _, err = tx.NewDelete(). + Model((*Node)(nil)). + Where("id IN (?)", bun.In(nodeIDs)). + Exec(ctx) + if err != nil { + return err } - return nil + if !plan.Prefix.IsNil() { + _ = vfs.blobStore.DeletePrefix(ctx, plan.Prefix) + } else { + for _, key := range plan.Keys { + _ = vfs.blobStore.Delete(ctx, key) + } + } + + return tx.Commit() } func (vfs *VirtualFS) generatePublicID() (string, error) {