108 lines
4.3 KiB
C#
108 lines
4.3 KiB
C#
namespace PlotLine.Services;
|
|
|
|
public interface IHardDeleteFileCleanupService
|
|
{
|
|
HardDeleteFileCleanupResult DeleteFiles(string operationName, int ownerId, IReadOnlyList<string> storagePaths);
|
|
}
|
|
|
|
public sealed record HardDeleteFileCleanupResult(int Deleted, int Missing, int Failed);
|
|
|
|
public sealed class HardDeleteFileCleanupService(
|
|
IWebHostEnvironment environment,
|
|
ILogger<HardDeleteFileCleanupService> logger) : IHardDeleteFileCleanupService
|
|
{
|
|
public HardDeleteFileCleanupResult DeleteFiles(string operationName, int ownerId, IReadOnlyList<string> storagePaths)
|
|
{
|
|
var deleted = 0;
|
|
var missing = 0;
|
|
var failed = 0;
|
|
var deletedDirectories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var storagePath in storagePaths)
|
|
{
|
|
var absolutePath = ResolveUploadPath(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 string? ResolveUploadPath(string storagePath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(storagePath)
|
|
|| storagePath.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
|
|| storagePath.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var normalised = storagePath.Replace('\\', '/').TrimStart('/');
|
|
if (!normalised.StartsWith("uploads/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var uploadsRoot = Path.GetFullPath(Path.Combine(environment.WebRootPath, "uploads"));
|
|
var absolutePath = Path.GetFullPath(Path.Combine(environment.WebRootPath, normalised.Replace('/', Path.DirectorySeparatorChar)));
|
|
return absolutePath.StartsWith(uploadsRoot, StringComparison.OrdinalIgnoreCase) ? absolutePath : null;
|
|
}
|
|
|
|
private void TryDeleteEmptyUploadDirectory(string operationName, int ownerId, string directory)
|
|
{
|
|
var uploadsRoot = Path.GetFullPath(Path.Combine(environment.WebRootPath, "uploads"));
|
|
var fullDirectory = Path.GetFullPath(directory);
|
|
if (!fullDirectory.StartsWith(uploadsRoot, StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(fullDirectory, uploadsRoot, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|