124 lines
4.5 KiB
C#
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
|
|
};
|
|
}
|