namespace PlotLine.Services; public interface IHardDeleteFileCleanupService { HardDeleteFileCleanupResult DeleteFiles(string operationName, int ownerId, IReadOnlyList storagePaths); } public sealed record HardDeleteFileCleanupResult(int Deleted, int Missing, int Failed); public sealed class HardDeleteFileCleanupService( IUploadStorageService uploadStorage, ILogger logger) : IHardDeleteFileCleanupService { public HardDeleteFileCleanupResult DeleteFiles(string operationName, int ownerId, IReadOnlyList storagePaths) { var deleted = 0; var missing = 0; var failed = 0; var deletedDirectories = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var storagePath in storagePaths) { var absolutePath = uploadStorage.TryResolvePublicPath(storagePath); if (absolutePath is null) { logger.LogWarning("Hard delete skipped a non-local upload path during {OperationName} for owner {OwnerID}.", operationName, ownerId); continue; } if (!File.Exists(absolutePath)) { missing++; logger.LogInformation("Hard delete file cleanup found a missing file during {OperationName} for owner {OwnerID}.", operationName, ownerId); continue; } try { File.Delete(absolutePath); deleted++; logger.LogInformation("Hard delete removed an uploaded file during {OperationName} for owner {OwnerID}.", operationName, ownerId); var directory = Path.GetDirectoryName(absolutePath); if (!string.IsNullOrWhiteSpace(directory)) { deletedDirectories.Add(directory); } } catch (Exception ex) { failed++; logger.LogWarning(ex, "Hard delete could not remove an uploaded file during {OperationName} for owner {OwnerID}.", operationName, ownerId); } } foreach (var directory in deletedDirectories.OrderByDescending(x => x.Length)) { TryDeleteEmptyUploadDirectory(operationName, ownerId, directory); } return new HardDeleteFileCleanupResult(deleted, missing, failed); } private void TryDeleteEmptyUploadDirectory(string operationName, int ownerId, string directory) { var uploadsRoot = Path.EndsInDirectorySeparator(uploadStorage.UploadsRootPath) ? uploadStorage.UploadsRootPath : uploadStorage.UploadsRootPath + Path.DirectorySeparatorChar; var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; var fullDirectory = Path.GetFullPath(directory); if (!fullDirectory.StartsWith(uploadsRoot, comparison) || string.Equals(fullDirectory, uploadStorage.UploadsRootPath, comparison)) { return; } try { if (Directory.Exists(fullDirectory) && !Directory.EnumerateFileSystemEntries(fullDirectory).Any()) { Directory.Delete(fullDirectory); logger.LogInformation("Hard delete removed an empty upload directory during {OperationName} for owner {OwnerID}.", operationName, ownerId); } } catch (Exception ex) { logger.LogWarning(ex, "Hard delete could not remove an empty upload directory during {OperationName} for owner {OwnerID}.", operationName, ownerId); } } }