139 lines
5.6 KiB
C#
139 lines
5.6 KiB
C#
using PlotLine.Data;
|
|
using PlotLine.Models;
|
|
using PlotLine.ViewModels;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public interface IProjectRestorePointService
|
|
{
|
|
Task<ProjectRestorePointListViewModel?> GetAsync(int projectId);
|
|
Task<ProjectRestorePoint?> CreateManualAsync(int projectId, string? restorePointName, string? notes);
|
|
Task<ProjectRestorePoint?> CreatePreRestoreAsync(int projectId, string sourceRestorePointName);
|
|
Task<ProjectRestorePointDownload?> DownloadAsync(int projectRestorePointId, int projectId);
|
|
Task<bool> HasReachedManualLimitAsync(int projectId);
|
|
Task<bool> DeleteAsync(int projectRestorePointId, int projectId);
|
|
}
|
|
|
|
public sealed class ProjectRestorePointService(
|
|
IProjectRepository projects,
|
|
IProjectExportService projectExports,
|
|
IProjectRestorePointRepository restorePoints,
|
|
ILogger<ProjectRestorePointService> 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<ProjectRestorePointListViewModel?> 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<ProjectRestorePoint?> 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<ProjectRestorePoint?> 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<ProjectRestorePoint?> 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<ProjectRestorePointDownload?> 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<bool> HasReachedManualLimitAsync(int projectId)
|
|
=> await restorePoints.CountAsync(projectId) >= MaxRestorePointsPerProject;
|
|
|
|
public async Task<bool> 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";
|
|
}
|
|
}
|