using PlotLine.Data; using PlotLine.Models; using PlotLine.ViewModels; namespace PlotLine.Services; public interface IProjectRestorePointService { Task GetAsync(int projectId); Task CreateManualAsync(int projectId, string? restorePointName, string? notes); Task CreatePreRestoreAsync(int projectId, string sourceRestorePointName); Task DownloadAsync(int projectRestorePointId, int projectId); Task HasReachedManualLimitAsync(int projectId); Task DeleteAsync(int projectRestorePointId, int projectId); } public sealed class ProjectRestorePointService( IProjectRepository projects, IProjectExportService projectExports, IProjectRestorePointRepository restorePoints, ILogger logger) : IProjectRestorePointService { private const string ManualRestorePointType = "Manual"; private const string PreRestorePointType = "PreRestore"; private const int MaxRestorePointsPerProject = 10; public const string LimitReachedMessage = "You have reached the limit of 10 restore points for this project. Delete an old restore point before creating a new one. You can download a restore point first if you want to keep a local backup copy."; public async Task GetAsync(int projectId) { var project = await projects.GetAsync(projectId); if (project is null) { return null; } return new ProjectRestorePointListViewModel { Project = project, RestorePoints = await restorePoints.ListAsync(projectId) }; } public async Task CreateManualAsync(int projectId, string? restorePointName, string? notes) { if (await HasReachedManualLimitAsync(projectId)) { logger.LogInformation("Manual restore point creation blocked for project {ProjectID} because the project already has {MaxRestorePointCount} restore points.", projectId, MaxRestorePointsPerProject); return null; } var name = string.IsNullOrWhiteSpace(restorePointName) ? $"Manual restore point - {DateTime.Now:yyyy-MM-dd HH:mm}" : restorePointName.Trim(); return await CreateAsync(projectId, name, ManualRestorePointType, Clean(notes)); } public async Task CreatePreRestoreAsync(int projectId, string sourceRestorePointName) { var name = $"Automatic Pre-Restore Backup - {DateTime.Now:yyyy-MM-dd HH:mm}"; var notes = $"Created automatically before restoring from \"{sourceRestorePointName}\"."; return await CreateAsync(projectId, name, PreRestorePointType, notes); } private async Task CreateAsync(int projectId, string restorePointName, string restorePointType, string? notes) { var export = await projectExports.ExportAsync(projectId); if (export is null) { logger.LogWarning("{RestorePointType} restore point requested for missing project {ProjectID}.", restorePointType, projectId); return null; } var created = await restorePoints.CreateAsync(projectId, restorePointName, restorePointType, export.Json, notes); logger.LogInformation("{RestorePointType} restore point {ProjectRestorePointID} created for project {ProjectID} with {JsonSizeBytes} JSON bytes.", restorePointType, created.ProjectRestorePointID, projectId, created.JsonSizeBytes); return created; } public async Task DownloadAsync(int projectRestorePointId, int projectId) { var restorePoint = await restorePoints.GetAsync(projectRestorePointId, projectId); if (restorePoint?.JsonData is null) { logger.LogWarning("Restore point download requested for missing restore point {ProjectRestorePointID} in project {ProjectID}.", projectRestorePointId, projectId); return null; } return new ProjectRestorePointDownload { FileName = BuildFileName(restorePoint.ProjectName, restorePoint.CreatedDateUtc), ContentType = "application/json", Json = restorePoint.JsonData }; } public async Task HasReachedManualLimitAsync(int projectId) => await restorePoints.CountAsync(projectId) >= MaxRestorePointsPerProject; public async Task DeleteAsync(int projectRestorePointId, int projectId) { var deleted = await restorePoints.DeleteAsync(projectRestorePointId, projectId); if (!deleted) { logger.LogWarning("Restore point delete failed for restore point {ProjectRestorePointID} in project {ProjectID}.", projectRestorePointId, projectId); } return deleted; } private static string? Clean(string? value) { return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } private static string BuildFileName(string projectName, DateTime createdDateUtc) { var safeTitle = new string(projectName .Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c) .ToArray()) .Trim(); safeTitle = string.Join("_", safeTitle.Split(' ', StringSplitOptions.RemoveEmptyEntries)); if (string.IsNullOrWhiteSpace(safeTitle)) { safeTitle = "PlotWeaver_Project"; } return $"{safeTitle}_RestorePoint_{createdDateUtc:yyyy-MM-dd_HHmm}.plotweaver"; } }