monorepo/native/desktop/maplefile/internal/app/app_files_cleanup.go

191 lines
6.2 KiB
Go

package app
import (
"fmt"
"os"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/domain/file"
)
// =============================================================================
// FILE CLEANUP OPERATIONS
// =============================================================================
// DeleteFile soft-deletes a file from both the cloud and local storage
func (a *Application) DeleteFile(fileID string) error {
a.logger.Info("DeleteFile called", zap.String("file_id", fileID))
// Use the SDK client which has automatic token refresh on 401
apiClient := a.authService.GetAPIClient()
// Use SDK's DeleteFile method which has automatic 401 retry
if err := apiClient.DeleteFile(a.ctx, fileID); err != nil {
a.logger.Error("Failed to delete file from cloud",
zap.String("file_id", fileID),
zap.Error(err))
return fmt.Errorf("failed to delete file: %w", err)
}
// Cloud delete succeeded - now clean up local data
a.cleanupLocalFile(fileID)
a.logger.Info("File deleted successfully", zap.String("file_id", fileID))
return nil
}
// cleanupLocalFile removes physical binary files immediately and marks the metadata as deleted.
// The metadata record is kept for background cleanup later.
func (a *Application) cleanupLocalFile(fileID string) {
// Get the local file record
localFile, err := a.mustGetFileRepo().Get(fileID)
if err != nil || localFile == nil {
a.logger.Debug("No local file record to clean up", zap.String("file_id", fileID))
return
}
// IMMEDIATELY delete physical binary files
// Delete the physical decrypted file if it exists
if localFile.FilePath != "" {
if err := os.Remove(localFile.FilePath); err != nil {
if !os.IsNotExist(err) {
a.logger.Warn("Failed to delete local decrypted file",
zap.String("file_id", fileID),
zap.String("path", localFile.FilePath),
zap.Error(err))
}
} else {
a.logger.Info("Deleted local decrypted file",
zap.String("file_id", fileID),
zap.String("path", localFile.FilePath))
}
}
// Delete the physical encrypted file if it exists
if localFile.EncryptedFilePath != "" {
if err := os.Remove(localFile.EncryptedFilePath); err != nil {
if !os.IsNotExist(err) {
a.logger.Warn("Failed to delete local encrypted file",
zap.String("file_id", fileID),
zap.String("path", localFile.EncryptedFilePath),
zap.Error(err))
}
} else {
a.logger.Info("Deleted local encrypted file",
zap.String("file_id", fileID),
zap.String("path", localFile.EncryptedFilePath))
}
}
// Delete the thumbnail if it exists
if localFile.ThumbnailPath != "" {
if err := os.Remove(localFile.ThumbnailPath); err != nil {
if !os.IsNotExist(err) {
a.logger.Warn("Failed to delete local thumbnail",
zap.String("file_id", fileID),
zap.String("path", localFile.ThumbnailPath),
zap.Error(err))
}
} else {
a.logger.Info("Deleted local thumbnail",
zap.String("file_id", fileID),
zap.String("path", localFile.ThumbnailPath))
}
}
// Mark the metadata record as deleted (will be cleaned up later by background process)
// Clear the file paths since the physical files are now deleted
localFile.State = file.StateDeleted
localFile.FilePath = ""
localFile.EncryptedFilePath = ""
localFile.ThumbnailPath = ""
localFile.ModifiedAt = time.Now()
if err := a.mustGetFileRepo().Update(localFile); err != nil {
a.logger.Warn("Failed to mark local file metadata as deleted",
zap.String("file_id", fileID),
zap.Error(err))
} else {
a.logger.Info("Marked local file metadata as deleted (will be cleaned up later)",
zap.String("file_id", fileID))
// Remove from search index
if err := a.searchService.DeleteFile(fileID); err != nil {
a.logger.Warn("Failed to remove file from search index",
zap.String("file_id", fileID),
zap.Error(err))
}
}
}
// purgeDeletedFileMetadata permanently removes a deleted file's metadata record.
// This is called by the background cleanup process after a retention period.
func (a *Application) purgeDeletedFileMetadata(fileID string) {
if err := a.mustGetFileRepo().Delete(fileID); err != nil {
a.logger.Warn("Failed to purge deleted file metadata",
zap.String("file_id", fileID),
zap.Error(err))
} else {
a.logger.Info("Purged deleted file metadata",
zap.String("file_id", fileID))
}
}
// deletedFileRetentionPeriod is how long to keep deleted file metadata before purging.
// This allows for potential recovery or sync conflict resolution.
const deletedFileRetentionPeriod = 7 * 24 * time.Hour // 7 days
// cleanupDeletedFiles runs in the background to clean up deleted files.
// It handles two cases:
// 1. Files marked as deleted that still have physical files (cleans up binaries immediately)
// 2. Files marked as deleted past the retention period (purges metadata)
func (a *Application) cleanupDeletedFiles() {
a.logger.Info("Starting background cleanup of deleted files")
// Get all local files
localFiles, err := a.mustGetFileRepo().List()
if err != nil {
a.logger.Error("Failed to list local files for cleanup", zap.Error(err))
return
}
binaryCleanedCount := 0
metadataPurgedCount := 0
now := time.Now()
for _, localFile := range localFiles {
// Only process deleted files
if localFile.State != file.StateDeleted {
continue
}
// Check if there are still physical files to clean up
if localFile.FilePath != "" || localFile.EncryptedFilePath != "" || localFile.ThumbnailPath != "" {
a.logger.Info("Cleaning up orphaned binary files for deleted record",
zap.String("file_id", localFile.ID))
a.cleanupLocalFile(localFile.ID)
binaryCleanedCount++
continue
}
// Check if metadata is past retention period and can be purged
if now.Sub(localFile.ModifiedAt) > deletedFileRetentionPeriod {
a.logger.Info("Purging deleted file metadata (past retention period)",
zap.String("file_id", localFile.ID),
zap.Time("deleted_at", localFile.ModifiedAt))
a.purgeDeletedFileMetadata(localFile.ID)
metadataPurgedCount++
}
}
if binaryCleanedCount > 0 || metadataPurgedCount > 0 {
a.logger.Info("Background cleanup completed",
zap.Int("binaries_cleaned", binaryCleanedCount),
zap.Int("metadata_purged", metadataPurgedCount))
} else {
a.logger.Debug("Background cleanup completed, no cleanup needed")
}
}