feat(svc): implement soft-delete in FileService

Delete now performs soft-delete (status='user_deleted') instead of physical deletion.

Removes storage content deletion — files are preserved on disk after soft-delete.

Second delete on already-deleted file returns nil (idempotent).

Add tests: SoftDelete, SoftDeleteKeepsStorage, SoftDeleteIdempotent, SoftDeleteForbidden.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-07-04 17:20:34 +08:00
parent ac98b5ddbd
commit e04e39bea8
2 changed files with 91 additions and 11 deletions
+7 -9
View File
@@ -129,6 +129,7 @@ func (s *FileService) Upload(ctx context.Context, userID string, parentID *strin
MimeType: mimeType,
StoragePath: storagePath,
Hash: hex.EncodeToString(hasher.Sum(nil)),
Status: model.StatusActive,
IsDir: false,
}
@@ -240,10 +241,14 @@ func (s *FileService) Update(ctx context.Context, userID, fileID string, newName
return modelToFileInfo(file), nil
}
// Delete removes a file or (empty) directory.
// Delete soft-deletes a file or (empty) directory. Storage content is preserved.
func (s *FileService) Delete(ctx context.Context, userID, fileID string) error {
file, err := s.getOwnedFile(ctx, userID, fileID)
if err != nil {
var ae *model.AppError
if errors.As(err, &ae) && errors.Is(ae.Err, model.ErrNotFound) {
return nil // idempotent: already deleted
}
return err
}
@@ -262,14 +267,6 @@ func (s *FileService) Delete(ctx context.Context, userID, fileID string) error {
return model.NewInternalError("delete file record", err)
}
// Remove content from storage (directories have no stored content).
if !file.IsDir {
if err := s.storage.Delete(ctx, file.StoragePath); err != nil {
s.logger.WarnContext(ctx, "failed to delete file from storage",
"path", file.StoragePath, "error", err)
}
}
return nil
}
@@ -297,6 +294,7 @@ func (s *FileService) CreateDir(ctx context.Context, userID string, parentID *st
UserID: userID,
ParentID: parentID,
Name: dirName,
Status: model.StatusActive,
IsDir: true,
}