Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,488 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/service/collection/softdelete.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
uc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/collection"
|
||||
uc_fileobjectstorage "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/fileobjectstorage"
|
||||
uc_storagedailyusage "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/storagedailyusage"
|
||||
uc_storageusageevent "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/storageusageevent"
|
||||
uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/transaction"
|
||||
"github.com/gocql/gocql"
|
||||
)
|
||||
|
||||
type SoftDeleteCollectionRequestDTO struct {
|
||||
ID gocql.UUID `json:"id"`
|
||||
ForceHardDelete bool `json:"force_hard_delete"` // Skip tombstone for GDPR right-to-be-forgotten
|
||||
}
|
||||
|
||||
type SoftDeleteCollectionResponseDTO struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type SoftDeleteCollectionService interface {
|
||||
Execute(ctx context.Context, req *SoftDeleteCollectionRequestDTO) (*SoftDeleteCollectionResponseDTO, error)
|
||||
}
|
||||
|
||||
type softDeleteCollectionServiceImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
getCollectionUseCase uc_collection.GetCollectionUseCase
|
||||
updateCollectionUseCase uc_collection.UpdateCollectionUseCase
|
||||
hardDeleteCollectionUseCase uc_collection.HardDeleteCollectionUseCase
|
||||
deleteMultipleDataUseCase uc_fileobjectstorage.DeleteMultipleEncryptedDataUseCase
|
||||
// Storage quota management
|
||||
storageQuotaHelperUseCase uc_user.UserStorageQuotaHelperUseCase
|
||||
createStorageUsageEventUseCase uc_storageusageevent.CreateStorageUsageEventUseCase
|
||||
updateStorageUsageUseCase uc_storagedailyusage.UpdateStorageUsageUseCase
|
||||
}
|
||||
|
||||
func NewSoftDeleteCollectionService(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
getCollectionUseCase uc_collection.GetCollectionUseCase,
|
||||
updateCollectionUseCase uc_collection.UpdateCollectionUseCase,
|
||||
hardDeleteCollectionUseCase uc_collection.HardDeleteCollectionUseCase,
|
||||
deleteMultipleDataUseCase uc_fileobjectstorage.DeleteMultipleEncryptedDataUseCase,
|
||||
storageQuotaHelperUseCase uc_user.UserStorageQuotaHelperUseCase,
|
||||
createStorageUsageEventUseCase uc_storageusageevent.CreateStorageUsageEventUseCase,
|
||||
updateStorageUsageUseCase uc_storagedailyusage.UpdateStorageUsageUseCase,
|
||||
) SoftDeleteCollectionService {
|
||||
logger = logger.Named("SoftDeleteCollectionService")
|
||||
return &softDeleteCollectionServiceImpl{
|
||||
config: config,
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
fileRepo: fileRepo,
|
||||
getCollectionUseCase: getCollectionUseCase,
|
||||
updateCollectionUseCase: updateCollectionUseCase,
|
||||
hardDeleteCollectionUseCase: hardDeleteCollectionUseCase,
|
||||
deleteMultipleDataUseCase: deleteMultipleDataUseCase,
|
||||
storageQuotaHelperUseCase: storageQuotaHelperUseCase,
|
||||
createStorageUsageEventUseCase: createStorageUsageEventUseCase,
|
||||
updateStorageUsageUseCase: updateStorageUsageUseCase,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *softDeleteCollectionServiceImpl) Execute(ctx context.Context, req *SoftDeleteCollectionRequestDTO) (*SoftDeleteCollectionResponseDTO, error) {
|
||||
//
|
||||
// STEP 1: Validation
|
||||
//
|
||||
if req == nil {
|
||||
svc.logger.Warn("Failed validation with nil request")
|
||||
return nil, httperror.NewForBadRequestWithSingleField("non_field_error", "Collection ID is required")
|
||||
}
|
||||
|
||||
if req.ID.String() == "" {
|
||||
svc.logger.Warn("Empty collection ID")
|
||||
return nil, httperror.NewForBadRequestWithSingleField("id", "Collection ID is required")
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get user ID from context
|
||||
//
|
||||
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
||||
if !ok {
|
||||
svc.logger.Error("Failed getting user ID from context")
|
||||
return nil, httperror.NewForInternalServerErrorWithSingleField("message", "Authentication context error")
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 3: Retrieve related records
|
||||
//
|
||||
collection, err := svc.getCollectionUseCase.Execute(ctx, req.ID)
|
||||
if err != nil {
|
||||
svc.logger.Error("Failed to get collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("collection_id", req.ID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if collection == nil {
|
||||
svc.logger.Debug("Collection not found",
|
||||
zap.Any("collection_id", req.ID))
|
||||
return nil, httperror.NewForNotFoundWithSingleField("message", "Collection not found")
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 4: Check if user has rights to delete this collection
|
||||
//
|
||||
if collection.OwnerID != userID {
|
||||
svc.logger.Warn("Unauthorized collection deletion attempt",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Any("collection_id", req.ID))
|
||||
return nil, httperror.NewForForbiddenWithSingleField("message", "Only the collection owner can delete a collection")
|
||||
}
|
||||
|
||||
// Check valid transitions.
|
||||
if err := dom_collection.IsValidStateTransition(collection.State, dom_collection.CollectionStateDeleted); err != nil {
|
||||
svc.logger.Warn("Invalid collection state transition",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc.logger.Info("Starting soft delete of collection hierarchy",
|
||||
zap.String("collection_id", collection.ID.String()),
|
||||
zap.Int("member_count", len(collection.Members)))
|
||||
|
||||
//
|
||||
// SAGA: Initialize distributed transaction manager
|
||||
//
|
||||
saga := transaction.NewSaga("soft-delete-collection", svc.logger)
|
||||
|
||||
//
|
||||
// STEP 5: Find all descendant collections
|
||||
//
|
||||
descendants, err := svc.repo.FindDescendants(ctx, req.ID)
|
||||
if err != nil {
|
||||
svc.logger.Error("Failed to check for descendant collections",
|
||||
zap.Any("error", err),
|
||||
zap.Any("collection_id", req.ID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc.logger.Info("Found descendant collections for deletion",
|
||||
zap.Any("collection_id", req.ID),
|
||||
zap.Int("descendants_count", len(descendants)))
|
||||
|
||||
//
|
||||
// STEP 6: Delete all files in the parent collection
|
||||
//
|
||||
parentFiles, err := svc.fileRepo.GetByCollection(req.ID)
|
||||
if err != nil {
|
||||
svc.logger.Error("Failed to get files for parent collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("collection_id", req.ID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Collect all S3 storage paths to delete and calculate total storage to release
|
||||
var allStoragePaths []string
|
||||
var totalStorageToRelease int64 = 0
|
||||
|
||||
if len(parentFiles) > 0 {
|
||||
parentFileIDs := make([]gocql.UUID, len(parentFiles))
|
||||
for i, file := range parentFiles {
|
||||
parentFileIDs[i] = file.ID
|
||||
// Collect S3 paths for deletion
|
||||
allStoragePaths = append(allStoragePaths, file.EncryptedFileObjectKey)
|
||||
if file.EncryptedThumbnailObjectKey != "" {
|
||||
allStoragePaths = append(allStoragePaths, file.EncryptedThumbnailObjectKey)
|
||||
}
|
||||
// Calculate storage to release (only for active files)
|
||||
if file.State == dom_file.FileStateActive {
|
||||
totalStorageToRelease += file.EncryptedFileSizeInBytes + file.EncryptedThumbnailSizeInBytes
|
||||
}
|
||||
}
|
||||
|
||||
// Execute parent file deletion (hard or soft based on flag)
|
||||
if req.ForceHardDelete {
|
||||
svc.logger.Info("Hard deleting parent collection files (GDPR mode)",
|
||||
zap.Int("file_count", len(parentFileIDs)))
|
||||
if err := svc.fileRepo.HardDeleteMany(parentFileIDs); err != nil {
|
||||
svc.logger.Error("Failed to hard-delete files in parent collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("collection_id", req.ID),
|
||||
zap.Int("file_count", len(parentFileIDs)))
|
||||
saga.Rollback(ctx)
|
||||
return nil, err
|
||||
}
|
||||
// No compensation for hard delete - GDPR requires permanent deletion
|
||||
} else {
|
||||
if err := svc.fileRepo.SoftDeleteMany(parentFileIDs); err != nil {
|
||||
svc.logger.Error("Failed to soft-delete files in parent collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("collection_id", req.ID),
|
||||
zap.Int("file_count", len(parentFileIDs)))
|
||||
saga.Rollback(ctx) // Rollback any previous operations
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// SAGA: Register compensation for parent files deletion
|
||||
// IMPORTANT: Capture parentFileIDs by value for closure
|
||||
parentFileIDsCaptured := parentFileIDs
|
||||
saga.AddCompensation(func(ctx context.Context) error {
|
||||
svc.logger.Warn("SAGA compensation: restoring parent collection files",
|
||||
zap.String("collection_id", req.ID.String()),
|
||||
zap.Int("file_count", len(parentFileIDsCaptured)))
|
||||
return svc.fileRepo.RestoreMany(parentFileIDsCaptured)
|
||||
})
|
||||
}
|
||||
|
||||
svc.logger.Info("Deleted files in parent collection",
|
||||
zap.Any("collection_id", req.ID),
|
||||
zap.Int("file_count", len(parentFileIDs)))
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 7: Delete all files in descendant collections
|
||||
//
|
||||
totalDescendantFiles := 0
|
||||
for _, descendant := range descendants {
|
||||
descendantFiles, err := svc.fileRepo.GetByCollection(descendant.ID)
|
||||
if err != nil {
|
||||
svc.logger.Error("Failed to get files for descendant collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("descendant_id", descendant.ID))
|
||||
saga.Rollback(ctx) // Rollback all previous operations
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(descendantFiles) > 0 {
|
||||
descendantFileIDs := make([]gocql.UUID, len(descendantFiles))
|
||||
for i, file := range descendantFiles {
|
||||
descendantFileIDs[i] = file.ID
|
||||
// Collect S3 paths for deletion
|
||||
allStoragePaths = append(allStoragePaths, file.EncryptedFileObjectKey)
|
||||
if file.EncryptedThumbnailObjectKey != "" {
|
||||
allStoragePaths = append(allStoragePaths, file.EncryptedThumbnailObjectKey)
|
||||
}
|
||||
// Calculate storage to release (only for active files)
|
||||
if file.State == dom_file.FileStateActive {
|
||||
totalStorageToRelease += file.EncryptedFileSizeInBytes + file.EncryptedThumbnailSizeInBytes
|
||||
}
|
||||
}
|
||||
|
||||
// Execute descendant file deletion (hard or soft based on flag)
|
||||
if req.ForceHardDelete {
|
||||
if err := svc.fileRepo.HardDeleteMany(descendantFileIDs); err != nil {
|
||||
svc.logger.Error("Failed to hard-delete files in descendant collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("descendant_id", descendant.ID),
|
||||
zap.Int("file_count", len(descendantFileIDs)))
|
||||
saga.Rollback(ctx)
|
||||
return nil, err
|
||||
}
|
||||
// No compensation for hard delete - GDPR requires permanent deletion
|
||||
} else {
|
||||
if err := svc.fileRepo.SoftDeleteMany(descendantFileIDs); err != nil {
|
||||
svc.logger.Error("Failed to soft-delete files in descendant collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("descendant_id", descendant.ID),
|
||||
zap.Int("file_count", len(descendantFileIDs)))
|
||||
saga.Rollback(ctx) // Rollback all previous operations
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// SAGA: Register compensation for this batch of descendant files
|
||||
// IMPORTANT: Capture by value for closure
|
||||
descendantFileIDsCaptured := descendantFileIDs
|
||||
descendantIDCaptured := descendant.ID
|
||||
saga.AddCompensation(func(ctx context.Context) error {
|
||||
svc.logger.Warn("SAGA compensation: restoring descendant collection files",
|
||||
zap.String("descendant_id", descendantIDCaptured.String()),
|
||||
zap.Int("file_count", len(descendantFileIDsCaptured)))
|
||||
return svc.fileRepo.RestoreMany(descendantFileIDsCaptured)
|
||||
})
|
||||
}
|
||||
|
||||
totalDescendantFiles += len(descendantFileIDs)
|
||||
svc.logger.Debug("Deleted files in descendant collection",
|
||||
zap.Any("descendant_id", descendant.ID),
|
||||
zap.Int("file_count", len(descendantFileIDs)))
|
||||
}
|
||||
}
|
||||
|
||||
svc.logger.Info("Soft-deleted all files in descendant collections",
|
||||
zap.Int("total_descendant_files", totalDescendantFiles),
|
||||
zap.Int("descendants_count", len(descendants)))
|
||||
|
||||
//
|
||||
// STEP 8: Delete all descendant collections
|
||||
//
|
||||
for _, descendant := range descendants {
|
||||
// Execute descendant collection deletion (hard or soft based on flag)
|
||||
if req.ForceHardDelete {
|
||||
if err := svc.hardDeleteCollectionUseCase.Execute(ctx, descendant.ID); err != nil {
|
||||
svc.logger.Error("Failed to hard-delete descendant collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("descendant_id", descendant.ID))
|
||||
saga.Rollback(ctx)
|
||||
return nil, err
|
||||
}
|
||||
// No compensation for hard delete - GDPR requires permanent deletion
|
||||
} else {
|
||||
if err := svc.repo.SoftDelete(ctx, descendant.ID); err != nil {
|
||||
svc.logger.Error("Failed to soft-delete descendant collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("descendant_id", descendant.ID))
|
||||
saga.Rollback(ctx) // Rollback all previous operations
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// SAGA: Register compensation for this descendant collection
|
||||
// IMPORTANT: Capture by value for closure
|
||||
descendantIDCaptured := descendant.ID
|
||||
saga.AddCompensation(func(ctx context.Context) error {
|
||||
svc.logger.Warn("SAGA compensation: restoring descendant collection",
|
||||
zap.String("descendant_id", descendantIDCaptured.String()))
|
||||
return svc.repo.Restore(ctx, descendantIDCaptured)
|
||||
})
|
||||
}
|
||||
|
||||
svc.logger.Debug("Deleted descendant collection",
|
||||
zap.Any("descendant_id", descendant.ID),
|
||||
zap.String("descendant_name", descendant.EncryptedName))
|
||||
}
|
||||
|
||||
svc.logger.Info("Deleted all descendant collections",
|
||||
zap.Int("descendants_count", len(descendants)))
|
||||
|
||||
//
|
||||
// STEP 9: Finally, delete the parent collection
|
||||
//
|
||||
if req.ForceHardDelete {
|
||||
svc.logger.Info("Hard deleting parent collection (GDPR mode)",
|
||||
zap.String("collection_id", req.ID.String()))
|
||||
if err := svc.hardDeleteCollectionUseCase.Execute(ctx, req.ID); err != nil {
|
||||
svc.logger.Error("Failed to hard-delete parent collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("collection_id", req.ID))
|
||||
saga.Rollback(ctx)
|
||||
return nil, err
|
||||
}
|
||||
// No compensation for hard delete - GDPR requires permanent deletion
|
||||
} else {
|
||||
if err := svc.repo.SoftDelete(ctx, req.ID); err != nil {
|
||||
svc.logger.Error("Failed to soft-delete parent collection",
|
||||
zap.Any("error", err),
|
||||
zap.Any("collection_id", req.ID))
|
||||
saga.Rollback(ctx) // Rollback all previous operations
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// SAGA: Register compensation for parent collection deletion
|
||||
// IMPORTANT: Capture by value for closure
|
||||
parentCollectionIDCaptured := req.ID
|
||||
saga.AddCompensation(func(ctx context.Context) error {
|
||||
svc.logger.Warn("SAGA compensation: restoring parent collection",
|
||||
zap.String("collection_id", parentCollectionIDCaptured.String()))
|
||||
return svc.repo.Restore(ctx, parentCollectionIDCaptured)
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 10: Update storage tracking (quota, events, daily usage)
|
||||
//
|
||||
if totalStorageToRelease > 0 {
|
||||
svc.logger.Info("Releasing storage quota for collection deletion",
|
||||
zap.String("collection_id", req.ID.String()),
|
||||
zap.Int64("total_storage_to_release", totalStorageToRelease))
|
||||
|
||||
// Release storage quota
|
||||
err = svc.storageQuotaHelperUseCase.OnFileDeleted(ctx, userID, totalStorageToRelease)
|
||||
if err != nil {
|
||||
svc.logger.Error("Failed to release storage quota after collection deletion",
|
||||
zap.Error(err))
|
||||
saga.Rollback(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Register compensation: re-reserve the released quota
|
||||
totalStorageCaptured := totalStorageToRelease
|
||||
userIDCaptured := userID
|
||||
saga.AddCompensation(func(ctx context.Context) error {
|
||||
svc.logger.Warn("SAGA compensation: re-reserving released storage quota",
|
||||
zap.Int64("size", totalStorageCaptured))
|
||||
return svc.storageQuotaHelperUseCase.CheckAndReserveQuota(ctx, userIDCaptured, totalStorageCaptured)
|
||||
})
|
||||
|
||||
// Create storage usage event
|
||||
err = svc.createStorageUsageEventUseCase.Execute(ctx, userID, totalStorageToRelease, "remove")
|
||||
if err != nil {
|
||||
svc.logger.Error("Failed to create storage usage event for collection deletion",
|
||||
zap.Error(err))
|
||||
saga.Rollback(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Register compensation: create compensating "add" event
|
||||
saga.AddCompensation(func(ctx context.Context) error {
|
||||
svc.logger.Warn("SAGA compensation: creating compensating usage event")
|
||||
return svc.createStorageUsageEventUseCase.Execute(ctx, userIDCaptured, totalStorageCaptured, "add")
|
||||
})
|
||||
|
||||
// Update daily storage usage
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
updateReq := &uc_storagedailyusage.UpdateStorageUsageRequest{
|
||||
UserID: userID,
|
||||
UsageDay: &today,
|
||||
TotalBytes: -totalStorageToRelease,
|
||||
AddBytes: 0,
|
||||
RemoveBytes: totalStorageToRelease,
|
||||
IsIncrement: true,
|
||||
}
|
||||
err = svc.updateStorageUsageUseCase.Execute(ctx, updateReq)
|
||||
if err != nil {
|
||||
svc.logger.Error("Failed to update daily storage usage for collection deletion",
|
||||
zap.Error(err))
|
||||
saga.Rollback(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Register compensation: reverse the usage update
|
||||
saga.AddCompensation(func(ctx context.Context) error {
|
||||
svc.logger.Warn("SAGA compensation: reversing daily usage update")
|
||||
compensateReq := &uc_storagedailyusage.UpdateStorageUsageRequest{
|
||||
UserID: userIDCaptured,
|
||||
UsageDay: &today,
|
||||
TotalBytes: totalStorageCaptured, // Positive to reverse
|
||||
AddBytes: totalStorageCaptured,
|
||||
RemoveBytes: 0,
|
||||
IsIncrement: true,
|
||||
}
|
||||
return svc.updateStorageUsageUseCase.Execute(ctx, compensateReq)
|
||||
})
|
||||
|
||||
svc.logger.Info("Storage quota released successfully",
|
||||
zap.Int64("released_bytes", totalStorageToRelease))
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 11: Delete all S3 objects
|
||||
//
|
||||
if len(allStoragePaths) > 0 {
|
||||
svc.logger.Info("Deleting S3 objects for collection",
|
||||
zap.Any("collection_id", req.ID),
|
||||
zap.Int("s3_objects_count", len(allStoragePaths)))
|
||||
|
||||
if err := svc.deleteMultipleDataUseCase.Execute(allStoragePaths); err != nil {
|
||||
// Log but don't fail - S3 deletion is best effort after metadata is deleted
|
||||
svc.logger.Error("Failed to delete some S3 objects (continuing anyway)",
|
||||
zap.Any("error", err),
|
||||
zap.Int("s3_objects_count", len(allStoragePaths)))
|
||||
} else {
|
||||
svc.logger.Info("Successfully deleted all S3 objects",
|
||||
zap.Int("s3_objects_deleted", len(allStoragePaths)))
|
||||
}
|
||||
}
|
||||
|
||||
svc.logger.Info("Collection hierarchy deleted successfully",
|
||||
zap.Any("collection_id", req.ID),
|
||||
zap.Int("parent_files_deleted", len(parentFiles)),
|
||||
zap.Int("descendant_files_deleted", totalDescendantFiles),
|
||||
zap.Int("descendants_deleted", len(descendants)),
|
||||
zap.Int("total_files_deleted", len(parentFiles)+totalDescendantFiles),
|
||||
zap.Int("s3_objects_deleted", len(allStoragePaths)))
|
||||
|
||||
return &SoftDeleteCollectionResponseDTO{
|
||||
Success: true,
|
||||
Message: "Collection, descendants, and all associated files deleted successfully",
|
||||
}, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue