PlotDirector/PlotLine/Services/ProjectRestoreService.cs
2026-06-08 15:23:15 +01:00

124 lines
4.5 KiB
C#

using System.Text.Json;
using PlotLine.Data;
using PlotLine.Models;
namespace PlotLine.Services;
public interface IProjectRestoreService
{
Task<ProjectRestoreResult> RestoreAsync(int projectId, int projectRestorePointId);
}
public sealed class ProjectRestoreService(
IProjectRestorePointRepository restorePointRepository,
IProjectRestorePointService restorePointService,
ILogger<ProjectRestoreService> logger) : IProjectRestoreService
{
private static readonly JsonSerializerOptions ReadOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true
};
private static readonly JsonSerializerOptions RestoreSqlOptions = new()
{
WriteIndented = false
};
public async Task<ProjectRestoreResult> RestoreAsync(int projectId, int projectRestorePointId)
{
var restorePoint = await restorePointRepository.GetAsync(projectRestorePointId, projectId);
if (restorePoint?.JsonData is null)
{
return Failure("Restore point could not be found.");
}
ProjectBackupExport? backup;
try
{
backup = JsonSerializer.Deserialize<ProjectBackupExport>(restorePoint.JsonData, ReadOptions);
}
catch (JsonException ex)
{
logger.LogWarning(ex, "Restore point {ProjectRestorePointID} contains invalid JSON.", projectRestorePointId);
return Failure("Restore point JSON is invalid. No project data was changed.");
}
var validationMessage = ValidateBackup(projectId, backup);
if (validationMessage is not null)
{
return Failure(validationMessage);
}
var safetyBackup = await restorePointService.CreatePreRestoreAsync(projectId, restorePoint.RestorePointName);
if (safetyBackup is null)
{
logger.LogError("PreRestore backup creation failed for project {ProjectID}. Restore was aborted.", projectId);
return Failure("A safety backup could not be created. Restore was aborted and no project data was changed.");
}
try
{
var restoreJson = JsonSerializer.Serialize(backup, RestoreSqlOptions);
await restorePointRepository.RestoreAsync(projectId, projectRestorePointId, safetyBackup.ProjectRestorePointID, restoreJson);
logger.LogInformation("Project {ProjectID} restored from restore point {ProjectRestorePointID}. Safety backup {SafetyRestorePointID} was created first.",
projectId,
projectRestorePointId,
safetyBackup.ProjectRestorePointID);
return new ProjectRestoreResult
{
Succeeded = true,
Message = "Project restored. A safety backup of the previous state was created first.",
SafetyBackup = safetyBackup
};
}
catch (Exception ex)
{
logger.LogError(ex, "Project restore failed for project {ProjectID} from restore point {ProjectRestorePointID}. Safety backup {SafetyRestorePointID} was retained.",
projectId,
projectRestorePointId,
safetyBackup.ProjectRestorePointID);
return new ProjectRestoreResult
{
Succeeded = false,
Message = "Restore failed. The safety backup was created, and the project data was left unchanged.",
SafetyBackup = safetyBackup
};
}
}
private static string? ValidateBackup(int projectId, ProjectBackupExport? backup)
{
if (backup is null)
{
return "Restore point JSON is invalid. No project data was changed.";
}
if (!string.Equals(backup.Format, "PlotDirector.ProjectBackup", StringComparison.Ordinal))
{
return "Restore point format is not recognised. No project data was changed.";
}
if (!string.Equals(backup.FormatVersion, "1.0", StringComparison.Ordinal))
{
return "Restore point format version is not supported. No project data was changed.";
}
if (backup.Project?.ProjectID != projectId)
{
return "This restore point does not belong to the selected project. No project data was changed.";
}
return null;
}
private static ProjectRestoreResult Failure(string message) => new()
{
Succeeded = false,
Message = message
};
}