refactor: node deletion

This commit is contained in:
2025-11-30 01:16:44 +00:00
parent 629d56b5ab
commit 6984bb209e
10 changed files with 165 additions and 79 deletions

View File

@@ -111,6 +111,15 @@ func (s *FSStore) Delete(ctx context.Context, key Key) error {
return nil 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 { func (s *FSStore) Update(ctx context.Context, key Key, opts UpdateOptions) error {
// Update is a no-op for FSStore // Update is a no-op for FSStore
return nil return nil

View File

@@ -20,6 +20,7 @@ type Store interface {
Put(ctx context.Context, key Key, reader io.Reader) error Put(ctx context.Context, key Key, reader io.Reader) error
Update(ctx context.Context, key Key, opts UpdateOptions) error Update(ctx context.Context, key Key, opts UpdateOptions) error
Delete(ctx context.Context, key Key) error Delete(ctx context.Context, key Key) error
DeletePrefix(ctx context.Context, prefix Key) error
Move(ctx context.Context, srcKey, dstKey Key) error Move(ctx context.Context, srcKey, dstKey Key) error
Read(ctx context.Context, key Key) (io.ReadCloser, error) Read(ctx context.Context, key Key) (io.ReadCloser, error)
ReadRange(ctx context.Context, key Key, offset, length int64) (io.ReadCloser, error) ReadRange(ctx context.Context, key Key, offset, length int64) (io.ReadCloser, error)

View File

@@ -62,9 +62,11 @@ func NewServer(c Config) (*fiber.App, error) {
}) })
uploadService := upload.NewService(vfs, blobStore) uploadService := upload.NewService(vfs, blobStore)
authMiddleware := auth.NewBearerAuthMiddleware(authService, db)
api := app.Group("/api") api := app.Group("/api")
auth.NewHTTPHandler(authService, db).RegisterRoutes(api) auth.NewHTTPHandler(authService, db).RegisterRoutes(api)
upload.NewHTTPHandler(uploadService).RegisterRoutes(api) upload.NewHTTPHandler(uploadService, authMiddleware).RegisterRoutes(api, authMiddleware)
return app, nil return app, nil
} }

View File

@@ -18,14 +18,16 @@ type updateUploadRequest struct {
type HTTPHandler struct { type HTTPHandler struct {
service *Service service *Service
authMiddleware fiber.Handler
} }
func NewHTTPHandler(s *Service) *HTTPHandler { func NewHTTPHandler(s *Service, authMiddleware fiber.Handler) *HTTPHandler {
return &HTTPHandler{service: s} 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 := api.Group("/uploads")
upload.Use(authMiddleware)
upload.Post("/", h.Create) upload.Post("/", h.Create)
upload.Put("/:uploadID/content", h.ReceiveContent) upload.Put("/:uploadID/content", h.ReceiveContent)

View File

@@ -61,6 +61,7 @@ func (s *Service) CreateUpload(ctx context.Context, userID uuid.UUID, opts Creat
Duration: 1 * time.Hour, Duration: 1 * time.Hour,
}) })
if err != nil { if err != nil {
_ = s.vfs.PermanentlyDeleteNode(ctx, node)
return nil, err return nil, err
} }

View File

@@ -5,4 +5,5 @@ import "errors"
var ( var (
ErrNodeNotFound = errors.New("node not found") ErrNodeNotFound = errors.New("node not found")
ErrNodeConflict = errors.New("node conflict") ErrNodeConflict = errors.New("node conflict")
ErrUnsupportedOperation = errors.New("unsupported operation")
) )

View File

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

View File

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

View File

@@ -4,53 +4,15 @@ import (
"context" "context"
"github.com/get-drexa/drexa/internal/blob" "github.com/get-drexa/drexa/internal/blob"
"github.com/google/uuid"
"github.com/uptrace/bun"
) )
type BlobKeyResolver interface { type BlobKeyResolver interface {
KeyMode() blob.KeyMode KeyMode() blob.KeyMode
Resolve(ctx context.Context, node *Node) (blob.Key, error) Resolve(ctx context.Context, node *Node) (blob.Key, error)
ResolveDeletionKeys(ctx context.Context, node *Node, allKeys []blob.Key) (*DeletionPlan, error)
} }
type FlatKeyResolver struct{} type DeletionPlan struct {
Prefix blob.Key
func NewFlatKeyResolver() *FlatKeyResolver { Keys []blob.Key
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
} }

View File

@@ -385,7 +385,31 @@ func (vfs *VirtualFS) PermanentlyDeleteNode(ctx context.Context, node *Node) err
if !node.IsAccessible() { if !node.IsAccessible() {
return ErrNodeNotFound 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 ( const descendantsQuery = `WITH RECURSIVE descendants AS (
SELECT id, blob_key FROM vfs_nodes WHERE id = ? SELECT id, blob_key FROM vfs_nodes WHERE id = ?
UNION ALL UNION ALL
@@ -399,11 +423,14 @@ func (vfs *VirtualFS) PermanentlyDeleteNode(ctx context.Context, node *Node) err
BlobKey blob.Key `bun:"blob_key"` 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 var records []nodeRecord
err := tx.NewRaw(descendantsQuery, node.ID).Scan(ctx, &records) err = tx.NewRaw(descendantsQuery, node.ID).Scan(ctx, &records)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return ErrNodeNotFound return ErrNodeNotFound
@@ -416,6 +443,7 @@ func (vfs *VirtualFS) PermanentlyDeleteNode(ctx context.Context, node *Node) err
} }
nodeIDs := make([]uuid.UUID, 0, len(records)) nodeIDs := make([]uuid.UUID, 0, len(records))
blobKeys := make([]blob.Key, 0, len(records))
for _, r := range records { for _, r := range records {
nodeIDs = append(nodeIDs, r.ID) nodeIDs = append(nodeIDs, r.ID)
if !r.BlobKey.IsNil() { if !r.BlobKey.IsNil() {
@@ -423,22 +451,28 @@ func (vfs *VirtualFS) PermanentlyDeleteNode(ctx context.Context, node *Node) err
} }
} }
_, err = tx.NewDelete(). plan, err := vfs.keyResolver.ResolveDeletionKeys(ctx, node, blobKeys)
Model((*Node)(nil)).
Where("id IN (?)", bun.In(nodeIDs)).
Exec(ctx)
return err
})
if err != nil { if err != nil {
return err return err
} }
// Delete blobs outside transaction (best effort) _, err = tx.NewDelete().
for _, key := range blobKeys { Model((*Node)(nil)).
_ = vfs.blobStore.Delete(ctx, key) 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) { func (vfs *VirtualFS) generatePublicID() (string, error) {