using System.Text.Json; using PlotLine.Data; using PlotLine.Models; namespace PlotLine.Services; public interface IProjectExportService { Task ExportAsync(int projectId); } public sealed class ProjectExportService( IProjectBackupRepository backups, IProjectActivityService activity, ICurrentUserService currentUser, ILogger logger) : IProjectExportService { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }; public async Task ExportAsync(int projectId) { if (!currentUser.UserId.HasValue) { return null; } var backup = await backups.GetProjectBackupAsync(projectId, currentUser.UserId.Value); if (backup?.Project is null) { logger.LogWarning("Project backup export requested for missing project {ProjectID}.", projectId); return null; } var json = JsonSerializer.Serialize(backup, JsonOptions); var fileName = BuildFileName(backup.Project.ProjectName, backup.ExportedAtUtc); logger.LogInformation("Project backup export generated for project {ProjectID} with {ByteCount} JSON bytes.", projectId, json.Length); await activity.RecordAsync(projectId, "Exported", "Project", projectId, backup.Project.ProjectName, "Project backup exported."); return new ProjectBackupResult { FileName = fileName, ContentType = "application/json", Json = json }; } private static string BuildFileName(string projectName, DateTime exportedAtUtc) { 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 = "PlotDirector_Project"; } return $"{safeTitle}_{exportedAtUtc:yyyy-MM-dd_HHmm}.plotdirector"; } }