From 6ec919d1be6b855522cfd8cf7e26dcf7708c3de3 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sun, 27 Jul 2025 17:51:50 -0500 Subject: [PATCH] Separate import and export functionality This refactor splits the import and export functionality out of the ImportContext and adds an ExportContext. This also introduced separate importer and exporter implementations. Additionally, adding archive, save, scripts, and server files to the final export archive has been implemented at the exporter implementation for each instead of handling it in the context. This should be a good pattern if other files need to be added down the road. --- LANCommander.SDK/Enums/ExportRecordFlags.cs | 1 + .../Exceptions/ExportSkippedException.cs | 17 + .../Exceptions}/ImportSkippedException.cs | 2 +- .../ExportContext.cs | 524 ++++++++++ .../Exporters/ActionExporter.cs | 20 + .../Exporters/ArchiveExporter.cs | 70 ++ .../Exporters/CollectionExporter.cs | 28 + .../Exporters/CustomFieldExporter.cs | 28 + .../Exporters/DeveloperExporter.cs | 28 + .../Exporters/EngineExporter.cs | 28 + .../Exporters/GameExporter.cs | 46 + .../Exporters/GenreExporter.cs | 28 + .../Exporters/KeyExporter.cs | 28 + .../Exporters/MediaExporter.cs | 65 ++ .../Exporters/MultiplayerModeExporter.cs | 28 + .../Exporters/PlatformExporter.cs | 27 + .../Exporters/PlaySessionExporter.cs | 27 + .../Exporters/PublisherExporter.cs | 27 + .../Exporters/RedistributableExporter.cs | 25 + .../Exporters/SaveExporter.cs | 64 ++ .../Exporters/SavePathExporter.cs | 27 + .../Exporters/ScriptExporter.cs | 60 ++ .../Exporters/ServerConsoleExporter.cs | 27 + .../Exporters/ServerExporter.cs | 65 ++ .../Exporters/ServerHttpPathExporter.cs | 27 + .../Exporters/TagExporter.cs | 27 + .../Exporters/_BaseExporter.cs | 17 + .../Exporters/_IExporter.cs | 17 + .../IServiceCollectionExtensions.cs | 51 + .../Factories/ExportContextFactory.cs | 13 + .../Factories/ImportContextFactory.cs | 3 +- .../ImportContext.cs | 395 ++++++++ .../Importers/ActionImporter.cs | 14 +- .../Importers/ArchiveImporter.cs | 48 +- .../Importers/CollectionImporter.cs | 22 +- .../Importers/CustomFieldImporter.cs | 24 +- .../Importers/DeveloperImporter.cs | 22 +- .../Importers/EngineImporter.cs | 22 +- .../Importers/GameImporter.cs | 42 +- .../Importers/GenreImporter.cs | 22 +- .../Importers/KeyImporter.cs | 21 +- .../Importers/MediaImporter.cs | 45 +- .../Importers/MultiplayerModeImporter.cs | 22 +- .../Importers/PlatformImporter.cs | 22 +- .../Importers/PlaySessionImporter.cs | 22 +- .../Importers/PublisherImporter.cs | 22 +- .../Importers/RedistributableImporter.cs | 23 +- .../Importers/SaveImporter.cs | 47 +- .../Importers/SavePathImporter.cs | 22 +- .../Importers/ScriptImporter.cs | 42 +- .../Importers/ServerConsoleImporter.cs | 22 +- .../Importers/ServerHttpPathImporter.cs | 23 +- .../Importers/ServerImporter.cs | 48 +- .../Importers/TagImporter.cs | 21 +- .../Importers/_BaseImporter.cs | 7 +- .../Importers/_IImporter.cs | 8 +- .../LANCommander.Server.ImportExport.csproj | 13 + .../Models/ExportQueueItem.cs | 23 + .../Models}/ExportitemInfo.cs | 5 +- .../Models}/ImportItemInfo.cs | 3 +- .../Models}/ImportQueueItem.cs | 2 +- .../Services/ExportService.cs | 40 + .../Services}/ImportService.cs | 14 +- .../IServiceCollectionExtensions.cs | 32 - .../Extensions/JsonSerializerExtensions.cs | 6 + .../Importers/ImportContext.cs | 923 ------------------ .../Controllers/Api/ExportController.cs | 12 +- .../Controllers/Api/GamesController.cs | 2 +- .../Api/RedistributablesController.cs | 2 +- .../Controllers/Api/ServersController.cs | 2 +- .../Controllers/GamesController.cs | 22 +- .../LANCommander.Server.csproj | 1 + LANCommander.Server/Startup/Services.cs | 2 + .../UI/Components/ExportDialog.razor | 55 +- .../UI/Components/ImportUploadDialog.razor | 27 +- LANCommander.sln | 7 + 76 files changed, 2093 insertions(+), 1573 deletions(-) create mode 100644 LANCommander.Server.ImportExport/Exceptions/ExportSkippedException.cs rename {LANCommander.Server.Services/Importers => LANCommander.Server.ImportExport/Exceptions}/ImportSkippedException.cs (88%) create mode 100644 LANCommander.Server.ImportExport/ExportContext.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/ActionExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/ArchiveExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/CollectionExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/CustomFieldExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/DeveloperExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/EngineExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/GameExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/GenreExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/KeyExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/MediaExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/MultiplayerModeExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/PlatformExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/PlaySessionExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/PublisherExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/RedistributableExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/SaveExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/SavePathExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/ScriptExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/ServerConsoleExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/ServerExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/ServerHttpPathExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/TagExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/_BaseExporter.cs create mode 100644 LANCommander.Server.ImportExport/Exporters/_IExporter.cs create mode 100644 LANCommander.Server.ImportExport/Extensions/IServiceCollectionExtensions.cs create mode 100644 LANCommander.Server.ImportExport/Factories/ExportContextFactory.cs rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Factories/ImportContextFactory.cs (74%) create mode 100644 LANCommander.Server.ImportExport/ImportContext.cs rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/ActionImporter.cs (81%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/ArchiveImporter.cs (75%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/CollectionImporter.cs (78%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/CustomFieldImporter.cs (74%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/DeveloperImporter.cs (82%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/EngineImporter.cs (78%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/GameImporter.cs (63%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/GenreImporter.cs (78%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/KeyImporter.cs (79%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/MediaImporter.cs (73%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/MultiplayerModeImporter.cs (79%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/PlatformImporter.cs (78%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/PlaySessionImporter.cs (79%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/PublisherImporter.cs (79%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/RedistributableImporter.cs (79%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/SaveImporter.cs (74%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/SavePathImporter.cs (78%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/ScriptImporter.cs (80%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/ServerConsoleImporter.cs (79%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/ServerHttpPathImporter.cs (78%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/ServerImporter.cs (74%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/TagImporter.cs (79%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/_BaseImporter.cs (70%) rename {LANCommander.Server.Services => LANCommander.Server.ImportExport}/Importers/_IImporter.cs (68%) create mode 100644 LANCommander.Server.ImportExport/LANCommander.Server.ImportExport.csproj create mode 100644 LANCommander.Server.ImportExport/Models/ExportQueueItem.cs rename {LANCommander.Server.Services/Importers => LANCommander.Server.ImportExport/Models}/ExportitemInfo.cs (54%) rename {LANCommander.Server.Services/Importers => LANCommander.Server.ImportExport/Models}/ImportItemInfo.cs (67%) rename {LANCommander.Server.Services/Importers => LANCommander.Server.ImportExport/Models}/ImportQueueItem.cs (90%) create mode 100644 LANCommander.Server.ImportExport/Services/ExportService.cs rename {LANCommander.Server.Services => LANCommander.Server.ImportExport/Services}/ImportService.cs (59%) create mode 100644 LANCommander.Server.Services/Extensions/JsonSerializerExtensions.cs delete mode 100644 LANCommander.Server.Services/Importers/ImportContext.cs diff --git a/LANCommander.SDK/Enums/ExportRecordFlags.cs b/LANCommander.SDK/Enums/ExportRecordFlags.cs index 317869df..3d20867d 100644 --- a/LANCommander.SDK/Enums/ExportRecordFlags.cs +++ b/LANCommander.SDK/Enums/ExportRecordFlags.cs @@ -5,6 +5,7 @@ namespace LANCommander.SDK.Enums; [Flags] public enum ExportRecordFlags { + None = 0, Actions = 1 << 0, Archives = 1 << 1, Collections = 1 << 2, diff --git a/LANCommander.Server.ImportExport/Exceptions/ExportSkippedException.cs b/LANCommander.Server.ImportExport/Exceptions/ExportSkippedException.cs new file mode 100644 index 00000000..38402b82 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exceptions/ExportSkippedException.cs @@ -0,0 +1,17 @@ +namespace LANCommander.Server.ImportExport.Exceptions; + +public class ExportSkippedException : Exception where TRecord : class +{ + public TRecord Record { get; set; } + + public ExportSkippedException(TRecord record, string message) : base(message) + { + Record = record; + } + + public ExportSkippedException(TRecord record, string message, Exception innerException) : base(message, + innerException) + { + Record = record; + } +} \ No newline at end of file diff --git a/LANCommander.Server.Services/Importers/ImportSkippedException.cs b/LANCommander.Server.ImportExport/Exceptions/ImportSkippedException.cs similarity index 88% rename from LANCommander.Server.Services/Importers/ImportSkippedException.cs rename to LANCommander.Server.ImportExport/Exceptions/ImportSkippedException.cs index 2cff6012..9daeeb20 100644 --- a/LANCommander.Server.Services/Importers/ImportSkippedException.cs +++ b/LANCommander.Server.ImportExport/Exceptions/ImportSkippedException.cs @@ -1,4 +1,4 @@ -namespace LANCommander.Server.Services.Importers; +namespace LANCommander.Server.ImportExport.Exceptions; public class ImportSkippedException : Exception where TRecord : class { diff --git a/LANCommander.Server.ImportExport/ExportContext.cs b/LANCommander.Server.ImportExport/ExportContext.cs new file mode 100644 index 00000000..1c520705 --- /dev/null +++ b/LANCommander.Server.ImportExport/ExportContext.cs @@ -0,0 +1,524 @@ +using System.IO.Compression; +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Helpers; +using LANCommander.Server.Data.Models; +using LANCommander.Server.ImportExport.Exporters; +using LANCommander.Server.ImportExport.Importers; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using ZipArchive = System.IO.Compression.ZipArchive; + +namespace LANCommander.Server.ImportExport; + +public class ExportContext( + BaseExporter gameExporter, + BaseExporter redistributableExporter, + BaseExporter serverExporter, + GameService gameService, + RedistributableService redistributableService, + ServerService serverService, + ArchiveService archiveService, + MediaService mediaService, + GameSaveService saveService, + BaseExporter actionExporter, + BaseExporter archiveExporter, + BaseExporter collectionExporter, + BaseExporter customFieldExporter, + DeveloperExporter developerExporter, + PublisherExporter publisherExporter, + BaseExporter engineExporter, + BaseExporter genreExporter, + BaseExporter keyExporter, + BaseExporter mediaExporter, + BaseExporter multiplayerModeExporter, + BaseExporter platformExporter, + BaseExporter playSessionExporter, + BaseExporter saveExporter, + BaseExporter savePathExporter, + BaseExporter scriptExporter, + BaseExporter serverConsoleExporter, + BaseExporter serverHttpPathExporter, + BaseExporter tagExporter, + IMapper mapper, + ILogger logger) : IDisposable +{ + public object Manifest { get; private set; } + public BaseModel DataRecord { get; private set; } + public StorageLocation ArchiveStorageLocation { get; } + public ZipArchive Archive { get; private set; } + + public BaseExporter Games { get; private set; } = gameExporter; + public BaseExporter Redistributables { get; private set; } = redistributableExporter; + public BaseExporter Servers { get; private set; } = serverExporter; + + public BaseExporter Actions = actionExporter; + public BaseExporter Archives = archiveExporter; + public BaseExporter Collections = collectionExporter; + public BaseExporter CustomFields = customFieldExporter; + public BaseExporter Developers = developerExporter; + public BaseExporter Engines = engineExporter; + public BaseExporter Genres = genreExporter; + public BaseExporter Keys = keyExporter; + public BaseExporter Media = mediaExporter; + public BaseExporter MultiplayerModes = multiplayerModeExporter; + public BaseExporter Platforms = platformExporter; + public BaseExporter PlaySessions = playSessionExporter; + public BaseExporter Publishers = publisherExporter; + public BaseExporter Saves = saveExporter; + public BaseExporter SavePaths = savePathExporter; + public BaseExporter Scripts = scriptExporter; + public BaseExporter ServerConsoles = serverConsoleExporter; + public BaseExporter ServerHttpPaths = serverHttpPathExporter; + public BaseExporter Tags = tagExporter; + + public int Remaining => _queue.Count; + public int Processed => _queue.Count(qi => qi.Processed); + public int Total => _queue.Count; + + private List _queue { get; } = new(); + public Dictionary Errored { get; } = new(); + + public EventHandler OnRecordAdded; + public EventHandler OnRecordProcessed; + public EventHandler OnRecordError; + + private void UseContext(ExportContext context) + { + Games.UseContext(context); + Redistributables.UseContext(context); + Servers.UseContext(context); + + Actions.UseContext(context); + Archives.UseContext(context); + Collections.UseContext(context); + CustomFields.UseContext(context); + Developers.UseContext(context); + Engines.UseContext(context); + Genres.UseContext(context); + Keys.UseContext(context); + Media.UseContext(context); + MultiplayerModes.UseContext(context); + Platforms.UseContext(context); + PlaySessions.UseContext(context); + Publishers.UseContext(context); + Saves.UseContext(context); + SavePaths.UseContext(context); + Scripts.UseContext(context); + ServerConsoles.UseContext(context); + ServerHttpPaths.UseContext(context); + Tags.UseContext(context); + } + + #region Initialize Export + public async Task> InitializeExportAsync(Guid recordId, ExportRecordType recordType) + { + UseContext(this); + + switch (recordType) + { + case ExportRecordType.Game: + return await InitializeGameExportAsync(recordId); + + case ExportRecordType.Redistributable: + return await InitializeRedistributableExportAsync(recordId); + + case ExportRecordType.Server: + return await InitializeServerExportAsync(recordId); + + default: + throw new InvalidOperationException("Unknown record type"); + } + } + + private async Task> InitializeGameExportAsync(Guid gameId) + { + var game = await gameService.Query(q => + { + return q + .AsNoTracking() + .AsSplitQuery() + .Include(g => g.Actions) + .Include(g => g.Archives) + .Include(g => g.BaseGame) + .Include(g => g.Categories) + .Include(g => g.Collections) + .Include(g => g.CustomFields) + .Include(g => g.DependentGames) + .Include(g => g.Developers) + .Include(g => g.Engine) + .Include(g => g.Genres) + .Include(g => g.Media) + .Include(g => g.MultiplayerModes) + .Include(g => g.Platforms) + .Include(g => g.Publishers) + .Include(g => g.Redistributables) + .Include(g => g.SavePaths) + .Include(g => g.Tags); + }).GetAsync(gameId); + + var gameManifest = mapper.Map(game); + + DataRecord = game; + Manifest = gameManifest; + + var exportItemInfo = new List(); + + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Actions, Actions).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Archives, Archives).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Collections, Collections).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.CustomFields, CustomFields).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Developers, Developers).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync([game.Engine], Engines).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Genres, Genres).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Keys, Keys).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Media, Media).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.MultiplayerModes, MultiplayerModes).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Platforms, Platforms).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.PlaySessions, PlaySessions).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Publishers, Publishers).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.GameSaves, Saves).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.SavePaths, SavePaths).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Scripts, Scripts).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(game.Tags, Tags).ToListAsync()); + + return exportItemInfo; + } + + private async Task> InitializeRedistributableExportAsync(Guid redistributableId) + { + var redistributable = await redistributableService + .AsNoTracking() + .AsSplitQuery() + .Query(q => + { + return q + .Include(r => r.Archives) + .Include(r => r.Scripts); + }) + .GetAsync(redistributableId); + + Manifest = mapper.Map(redistributable); + + var exportItemInfo = new List(); + + exportItemInfo.AddRange(await GetExportItemInfoAsync(redistributable.Archives, Archives).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(redistributable.Scripts, Scripts).ToListAsync()); + + return exportItemInfo; + } + + private async Task> InitializeServerExportAsync(Guid serverId) + { + var server = await serverService + .AsNoTracking() + .AsSplitQuery() + .Query(q => + { + return q + .Include(s => s.Actions) + .Include(s => s.HttpPaths) + .Include(s => s.ServerConsoles) + .Include(s => s.Scripts); + + }) + .GetAsync(serverId); + + Manifest = mapper.Map(server); + + var exportItemInfo = new List(); + + exportItemInfo.AddRange(await GetExportItemInfoAsync(server.Actions, Actions).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(server.Scripts, Scripts).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(server.ServerConsoles, ServerConsoles).ToListAsync()); + exportItemInfo.AddRange(await GetExportItemInfoAsync(server.HttpPaths, ServerHttpPaths).ToListAsync()); + + return exportItemInfo; + } + #endregion + + #region Prepare Export Queue + public async Task PrepareExportQueueAsync(ExportRecordFlags importRecordFlags) + { + if (DataRecord is Data.Models.Game game) + await PrepareGameExportQueueAsync(game, importRecordFlags); + + if (DataRecord is Data.Models.Redistributable redistributable) + await PrepareRedistributableExportQueueAsync(redistributable, importRecordFlags); + + if (DataRecord is Data.Models.Server server) + await PrepareServerExportQueueAsync(server, importRecordFlags); + } + + public async Task PrepareGameExportQueueAsync(Game game, ExportRecordFlags importRecordFlags) + { + if (importRecordFlags.HasFlag(ExportRecordFlags.Actions)) + await AddToExportQueueAsync(ExportRecordFlags.Actions, game.Actions); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Archives)) + await AddToExportQueueAsync(ExportRecordFlags.Archives, game.Archives); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Collections)) + await AddToExportQueueAsync(ExportRecordFlags.Collections, game.Collections); + + if (importRecordFlags.HasFlag(ExportRecordFlags.CustomFields)) + await AddToExportQueueAsync(ExportRecordFlags.CustomFields, game.CustomFields); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Developers)) + await AddToExportQueueAsync(ExportRecordFlags.Developers, game.Developers); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Engine) && game.Engine != null) + await AddToExportQueueAsync(ExportRecordFlags.Engine, [game.Engine]); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Genres)) + await AddToExportQueueAsync(ExportRecordFlags.Genres, game.Genres); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Keys)) + await AddToExportQueueAsync(ExportRecordFlags.Keys, game.Keys); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Media)) + await AddToExportQueueAsync(ExportRecordFlags.Media, game.Media); + + if (importRecordFlags.HasFlag(ExportRecordFlags.MultiplayerModes)) + await AddToExportQueueAsync(ExportRecordFlags.MultiplayerModes, game.MultiplayerModes); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Platforms)) + await AddToExportQueueAsync(ExportRecordFlags.Platforms, game.Platforms); + + if (importRecordFlags.HasFlag(ExportRecordFlags.PlaySessions)) + await AddToExportQueueAsync(ExportRecordFlags.PlaySessions, game.PlaySessions); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Publishers)) + await AddToExportQueueAsync(ExportRecordFlags.Publishers, game.Publishers); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Saves)) + await AddToExportQueueAsync(ExportRecordFlags.Saves, game.GameSaves); + + if (importRecordFlags.HasFlag(ExportRecordFlags.SavePaths)) + await AddToExportQueueAsync(ExportRecordFlags.SavePaths, game.SavePaths); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Scripts)) + await AddToExportQueueAsync(ExportRecordFlags.Scripts, game.Scripts); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Tags)) + await AddToExportQueueAsync(ExportRecordFlags.Tags, game.Tags); + } + + public async Task PrepareRedistributableExportQueueAsync(Data.Models.Redistributable redistributable, + ExportRecordFlags importRecordFlags) + { + if (importRecordFlags.HasFlag(ExportRecordFlags.Archives)) + await AddToExportQueueAsync(ExportRecordFlags.Archives, redistributable.Archives); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Scripts)) + await AddToExportQueueAsync(ExportRecordFlags.Scripts, redistributable.Scripts); + } + + public async Task PrepareServerExportQueueAsync(Data.Models.Server server, + ExportRecordFlags importRecordFlags) + { + if (importRecordFlags.HasFlag(ExportRecordFlags.Actions)) + await AddToExportQueueAsync(ExportRecordFlags.Actions, server.Actions); + + if (importRecordFlags.HasFlag(ExportRecordFlags.Scripts)) + await AddToExportQueueAsync(ExportRecordFlags.Scripts, server.Scripts); + + if (importRecordFlags.HasFlag(ExportRecordFlags.ServerConsoles)) + await AddToExportQueueAsync(ExportRecordFlags.ServerConsoles, server.ServerConsoles); + + if (importRecordFlags.HasFlag(ExportRecordFlags.ServerHttpPaths)) + await AddToExportQueueAsync(ExportRecordFlags.ServerHttpPaths, server.HttpPaths); + } + #endregion + + private async Task AddToExportQueueAsync(ExportRecordFlags type, IEnumerable records) where TEntity : Data.Models.BaseModel + { + if (records != null) + _queue.AddRange(records.Select(r => new ExportQueueItem(r.Id, type, r))); + } + + /// + /// Process the context's data record and write the archive to the supplied stream + /// + /// Output stream for the resulting archive to be written to + public async Task ExportQueueAsync(Stream stream) + { + SDK.Models.Manifest.Game gameManifest = null; + SDK.Models.Manifest.Redistributable redistributableManifest = null; + SDK.Models.Manifest.Server serverManifest = null; + + if (DataRecord is Data.Models.Game game) + gameManifest = await ExportGameQueueAsync(game); + else if (DataRecord is Data.Models.Redistributable redistributable) + redistributableManifest = await ExportRedistributableQueueAsync(redistributable); + else if (DataRecord is Data.Models.Server server) + serverManifest = await ExportServerQueueAsync(server); + + using (var export = new ZipArchive(stream, ZipArchiveMode.Create)) + { + using (var ms = new MemoryStream()) + using (var writer = new StreamWriter(ms)) + { + if (gameManifest != null) + await writer.WriteAsync(ManifestHelper.Serialize(gameManifest)); + else if (redistributableManifest != null) + await writer.WriteAsync(ManifestHelper.Serialize(redistributableManifest)); + else if (serverManifest != null) + await writer.WriteAsync(ManifestHelper.Serialize(serverManifest)); + + await writer.FlushAsync(); + + var manifestEntry = export.CreateEntry(ManifestHelper.ManifestFilename, CompressionLevel.NoCompression); + + await using (var entryStream = manifestEntry.Open()) + { + ms.Seek(0, SeekOrigin.Begin); + await ms.CopyToAsync(entryStream); + } + } + } + } + + public async Task ExportGameQueueAsync(Data.Models.Game game) + { + var manifest = await Games.ExportAsync(game.Id); + + foreach (var queueItem in _queue) + { + if (queueItem.Type == ExportRecordFlags.Actions) + manifest.Actions.Add(await ExportRecordAsync(queueItem, Actions)); + else if (queueItem.Type == ExportRecordFlags.Archives) + manifest.Archives.Add(await ExportRecordAsync(queueItem, Archives)); + else if (queueItem.Type == ExportRecordFlags.Collections) + manifest.Collections.Add(await ExportRecordAsync(queueItem, Collections)); + else if (queueItem.Type == ExportRecordFlags.CustomFields) + manifest.CustomFields.Add(await ExportRecordAsync(queueItem, CustomFields)); + else if (queueItem.Type == ExportRecordFlags.Developers) + manifest.Developers.Add(await ExportRecordAsync(queueItem, Developers)); + else if (queueItem.Type == ExportRecordFlags.Publishers) + manifest.Publishers.Add(await ExportRecordAsync(queueItem, Publishers)); + else if (queueItem.Type == ExportRecordFlags.Engine) + manifest.Engine = await ExportRecordAsync(queueItem, Engines); + else if (queueItem.Type == ExportRecordFlags.Genres) + manifest.Genres.Add(await ExportRecordAsync(queueItem, Genres)); + else if (queueItem.Type == ExportRecordFlags.Keys) + manifest.Keys.Add(await ExportRecordAsync(queueItem, Keys)); + else if (queueItem.Type == ExportRecordFlags.Media) + manifest.Media.Add(await ExportRecordAsync(queueItem, Media)); + else if (queueItem.Type == ExportRecordFlags.MultiplayerModes) + manifest.MultiplayerModes.Add(await ExportRecordAsync(queueItem, MultiplayerModes)); + else if (queueItem.Type == ExportRecordFlags.Platforms) + manifest.Platforms.Add(await ExportRecordAsync(queueItem, Platforms)); + else if (queueItem.Type == ExportRecordFlags.PlaySessions) + manifest.PlaySessions.Add(await ExportRecordAsync(queueItem, PlaySessions)); + else if (queueItem.Type == ExportRecordFlags.Saves) + manifest.Saves.Add(await ExportRecordAsync(queueItem, Saves)); + else if (queueItem.Type == ExportRecordFlags.SavePaths) + manifest.SavePaths.Add(await ExportRecordAsync(queueItem, SavePaths)); + else if (queueItem.Type == ExportRecordFlags.Scripts) + manifest.Scripts.Add(await ExportRecordAsync(queueItem, Scripts)); + else if (queueItem.Type == ExportRecordFlags.Tags) + manifest.Tags.Add(await ExportRecordAsync(queueItem, Tags)); + } + + return manifest; + } + + public async Task ExportRedistributableQueueAsync(Data.Models.Redistributable redistributable) + { + var manifest = await Redistributables.ExportAsync(redistributable.Id); + + foreach (var queueItem in _queue) + { + if (queueItem.Type == ExportRecordFlags.Archives) + manifest.Archives.Add(await ExportRecordAsync(queueItem, Archives)); + else if (queueItem.Type == ExportRecordFlags.Scripts) + manifest.Scripts.Add(await ExportRecordAsync(queueItem, Scripts)); + } + + return manifest; + } + + public async Task ExportServerQueueAsync(Data.Models.Server server) + { + var manifest = await Servers.ExportAsync(server.Id); + + foreach (var queueItem in _queue) + { + if (queueItem.Type == ExportRecordFlags.Actions) + manifest.Actions.Add(await ExportRecordAsync(queueItem, Actions)); + else if (queueItem.Type == ExportRecordFlags.Scripts) + manifest.Scripts.Add(await ExportRecordAsync(queueItem, Scripts)); + else if (queueItem.Type == ExportRecordFlags.ServerConsoles) + manifest.ServerConsoles.Add(await ExportRecordAsync(queueItem, ServerConsoles)); + else if (queueItem.Type == ExportRecordFlags.ServerHttpPaths) + await ExportRecordAsync(queueItem, ServerHttpPaths); + } + + return manifest; + } + + private async Task AddSaveToExport(SDK.Models.Manifest.Save save, System.IO.Compression.ZipArchive zip) + { + try + { + var savePath = await saveService.GetSavePathAsync(save.Id); + + if (Path.Exists(savePath)) + { + var saveEntry = zip.CreateEntry($"Saves/{save.Id}"); + + using (var saveEntryStream = saveEntry.Open()) + using (var saveFileStream = new FileStream(savePath, FileMode.Open)) + { + await saveFileStream.CopyToAsync(saveEntryStream); + } + } + } + catch (Exception ex) + { + logger.LogError(ex, $"Could not add save {save.Id} to export file"); + } + } + + private async IAsyncEnumerable GetExportItemInfoAsync(IEnumerable records, + BaseExporter exporter) + { + if (records != null) + foreach (var record in records) + { + if (record != null && record.GetType() == typeof(TEntity)) + yield return await exporter.GetExportInfoAsync(record); + } + } + + private async Task ExportRecordAsync(ExportQueueItem queueItem, BaseExporter exporter) where TEntity : BaseModel + { + var entity = queueItem.Record as TEntity; + try + { + var record = await exporter.ExportAsync(entity.Id); + + queueItem.Processed = true; + + OnRecordProcessed?.Invoke(this, queueItem); + + return record; + } + catch (Exception ex) + { + Errored.Add(queueItem, ex.Message); + OnRecordError?.Invoke(this, queueItem); + + return default; + } + } + + public void Dispose() + { + if (Archive != null) + Archive.Dispose(); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/ActionExporter.cs b/LANCommander.Server.ImportExport/Exporters/ActionExporter.cs new file mode 100644 index 00000000..871fcce5 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/ActionExporter.cs @@ -0,0 +1,20 @@ +using LANCommander.SDK.Enums; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; +using Action = LANCommander.SDK.Models.Manifest.Action; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class ActionExporter( + ActionService actionService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Action record) => + await Task.Run(() => new ExportItemInfo { Id = record.Id, Name = record.Name, Flag = ExportRecordFlags.Actions }); + + public override bool CanExport(Action record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await actionService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/ArchiveExporter.cs b/LANCommander.Server.ImportExport/Exporters/ArchiveExporter.cs new file mode 100644 index 00000000..c6f772e5 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/ArchiveExporter.cs @@ -0,0 +1,70 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Exceptions; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +/// +/// Implements exporting for archive records +/// +/// AutoMapper instance for mapping between models +/// Service for archive operations +public class ArchiveExporter( + IMapper mapper, + ArchiveService archiveService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Archive record) + { + var archivePath = await archiveService.GetArchiveFileLocationAsync(record.ObjectKey); + + var info = new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Archives, + Name = record.Version, + }; + + if (File.Exists(archivePath)) + { + var fileInfo = new FileInfo(archivePath); + + if (fileInfo.Exists) + info.Size = fileInfo.Length; + } + + return info; + } + + public override bool CanExport(Archive record) => ExportContext.DataRecord is Data.Models.Game || ExportContext.DataRecord is Data.Models.Redistributable; + + public override async Task ExportAsync(Guid id) + { + var entity = await archiveService.GetAsync(id); + + try + { + var path = await archiveService.GetArchiveFileLocationAsync(entity); + var fileInfo = new FileInfo(path); + + if (fileInfo.Exists) + { + var archiveEntry = ExportContext.Archive.CreateEntry($"Archives/{entity.Id}"); + + using (var archiveEntryStream = archiveEntry.Open()) + using (var archiveFileStream = new FileStream(path, FileMode.Open)) + { + await archiveFileStream.CopyToAsync(archiveEntryStream); + } + } + + return mapper.Map(entity); + } + catch (Exception ex) + { + throw new ExportSkippedException(entity, "Could not add archive to export file", ex); + } + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/CollectionExporter.cs b/LANCommander.Server.ImportExport/Exporters/CollectionExporter.cs new file mode 100644 index 00000000..521120f8 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/CollectionExporter.cs @@ -0,0 +1,28 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class CollectionExporter( + CollectionService collectionService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Collection record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Collections, + Name = record.Name, + }; + } + + public override bool CanExport(Collection record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await collectionService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/CustomFieldExporter.cs b/LANCommander.Server.ImportExport/Exporters/CustomFieldExporter.cs new file mode 100644 index 00000000..64d47cdd --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/CustomFieldExporter.cs @@ -0,0 +1,28 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class CustomFieldExporter( + GameCustomFieldService gameCustomFieldService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.GameCustomField record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.CustomFields, + Name = record.Name, + }; + } + + public override bool CanExport(GameCustomField record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await gameCustomFieldService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/DeveloperExporter.cs b/LANCommander.Server.ImportExport/Exporters/DeveloperExporter.cs new file mode 100644 index 00000000..719859c9 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/DeveloperExporter.cs @@ -0,0 +1,28 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class DeveloperExporter( + CompanyService companyService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Company record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Developers, + Name = record.Name, + }; + } + + public override bool CanExport(Company record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await companyService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/EngineExporter.cs b/LANCommander.Server.ImportExport/Exporters/EngineExporter.cs new file mode 100644 index 00000000..37e98480 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/EngineExporter.cs @@ -0,0 +1,28 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class EngineExporter( + EngineService engineService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Engine record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Engine, + Name = record.Name, + }; + } + + public override bool CanExport(Engine record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await engineService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/GameExporter.cs b/LANCommander.Server.ImportExport/Exporters/GameExporter.cs new file mode 100644 index 00000000..1414e0c2 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/GameExporter.cs @@ -0,0 +1,46 @@ +using AutoMapper; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class GameExporter( + GameService gameService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Game record) + { + return new ExportItemInfo + { + Id = record.Id, + Name = record.Title, + }; + } + + public override bool CanExport(Game record) => true; + + public override async Task ExportAsync(Guid id) + { + // Retrieve the game again, but only get the minimal amount of data required + var manifest = await gameService.GetAsync(id); + + manifest.Actions ??= new List(); + manifest.Archives ??= new List(); + manifest.Collections ??= new List(); + manifest.CustomFields ??= new List(); + manifest.Developers ??= new List(); + manifest.Genres ??= new List(); + manifest.Keys ??= new List(); + manifest.Media ??= new List(); + manifest.MultiplayerModes ??= new List(); + manifest.Platforms ??= new List(); + manifest.PlaySessions ??= new List(); + manifest.Publishers ??= new List(); + manifest.Saves ??= new List(); + manifest.SavePaths ??= new List(); + manifest.Scripts ??= new List(); + manifest.Tags ??= new List(); + + return manifest; + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/GenreExporter.cs b/LANCommander.Server.ImportExport/Exporters/GenreExporter.cs new file mode 100644 index 00000000..f0239412 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/GenreExporter.cs @@ -0,0 +1,28 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class GenreExporter( + GenreService genreService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Genre record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Genres, + Name = record.Name, + }; + } + + public override bool CanExport(Genre record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await genreService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/KeyExporter.cs b/LANCommander.Server.ImportExport/Exporters/KeyExporter.cs new file mode 100644 index 00000000..00bad4e1 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/KeyExporter.cs @@ -0,0 +1,28 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class KeyExporter( + KeyService keyService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Key record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Keys, + Name = new String('*', record.Value.Length), + }; + } + + public override bool CanExport(Key record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await keyService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/MediaExporter.cs b/LANCommander.Server.ImportExport/Exporters/MediaExporter.cs new file mode 100644 index 00000000..d4c0654a --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/MediaExporter.cs @@ -0,0 +1,65 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Exceptions; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class MediaExporter( + MediaService mediaService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Media record) + { + var mediaPath = await mediaService.GetMediaPathAsync(record.Id); + + var info = new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Media, + Name = String.IsNullOrWhiteSpace(record.Name) ? record.Type.ToString() : $"{record.Type} - {record.Name}", + }; + + if (File.Exists(mediaPath)) + { + var fileInfo = new FileInfo(mediaPath); + + if (fileInfo.Exists) + info.Size = fileInfo.Length; + } + + return info; + } + + public override bool CanExport(Media record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + var entity = await mediaService.GetAsync(id); + + try + { + var path = await mediaService.GetMediaPathAsync(id); + + var fileInfo = new FileInfo(path); + + if (fileInfo.Exists) + { + var mediaEntry = ExportContext.Archive.CreateEntry($"Media/{entity.Id}"); + + using (var mediaEntryStream = mediaEntry.Open()) + using (var mediaFileStream = new FileStream(path, FileMode.Open)) + { + await mediaFileStream.CopyToAsync(mediaEntryStream); + } + } + + return await mediaService.GetAsync(id); + } + catch (Exception ex) + { + throw new ExportSkippedException(entity, "Could not add media to export file", ex); + } + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/MultiplayerModeExporter.cs b/LANCommander.Server.ImportExport/Exporters/MultiplayerModeExporter.cs new file mode 100644 index 00000000..ea1a9bcc --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/MultiplayerModeExporter.cs @@ -0,0 +1,28 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class MultiplayerModeExporter( + MultiplayerModeService multiplayerModeService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.MultiplayerMode record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.MultiplayerModes, + Name = String.IsNullOrWhiteSpace(record.Description) ? record.Type.ToString() : $"{record.Type} - {record.Description}", + }; + } + + public override bool CanExport(MultiplayerMode record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await multiplayerModeService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/PlatformExporter.cs b/LANCommander.Server.ImportExport/Exporters/PlatformExporter.cs new file mode 100644 index 00000000..43b6a45c --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/PlatformExporter.cs @@ -0,0 +1,27 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class PlatformExporter(PlatformService platformService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Platform record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Platforms, + Name = record.Name, + }; + } + + public override bool CanExport(Platform record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await platformService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/PlaySessionExporter.cs b/LANCommander.Server.ImportExport/Exporters/PlaySessionExporter.cs new file mode 100644 index 00000000..d085c416 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/PlaySessionExporter.cs @@ -0,0 +1,27 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class PlaySessionExporter(PlaySessionService playSessionService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.PlaySession record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.PlaySessions, + Name = $"{record.User} - {record.Start}-{record.End}", + }; + } + + public override bool CanExport(PlaySession record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await playSessionService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/PublisherExporter.cs b/LANCommander.Server.ImportExport/Exporters/PublisherExporter.cs new file mode 100644 index 00000000..70943538 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/PublisherExporter.cs @@ -0,0 +1,27 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class PublisherExporter(CompanyService companyService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Company record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Publishers, + Name = record.Name, + }; + } + + public override bool CanExport(Company record) => ExportContext.DataRecord is Data.Models.Company; + + public override async Task ExportAsync(Guid id) + { + return await companyService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/RedistributableExporter.cs b/LANCommander.Server.ImportExport/Exporters/RedistributableExporter.cs new file mode 100644 index 00000000..5922a168 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/RedistributableExporter.cs @@ -0,0 +1,25 @@ +using AutoMapper; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class RedistributableExporter(RedistributableService redistributableService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Redistributable record) + { + return new ExportItemInfo() + { + Id = record.Id, + Name = record.Name, + }; + } + + public override bool CanExport(Redistributable record) => true; + + public override async Task ExportAsync(Guid id) + { + return await redistributableService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/SaveExporter.cs b/LANCommander.Server.ImportExport/Exporters/SaveExporter.cs new file mode 100644 index 00000000..63e8e160 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/SaveExporter.cs @@ -0,0 +1,64 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Exceptions; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class SaveExporter( + IMapper mapper, + GameSaveService gameSaveService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.GameSave record) + { + var savePath = await gameSaveService.GetSavePathAsync(record.Id); + + var info = new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Saves, + Name = $"{record.User} - {record.CreatedOn}", + }; + + if (File.Exists(savePath)) + { + var fileInfo = new FileInfo(savePath); + + if (fileInfo.Exists) + info.Size = fileInfo.Length; + } + + return info; + } + + public override bool CanExport(Save record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + var entity = await gameSaveService.GetAsync(id); + + try + { + var savePath = await gameSaveService.GetSavePathAsync(id); + + if (Path.Exists(savePath)) + { + var saveEntry = ExportContext.Archive.CreateEntry($"Saves/{id}"); + + using (var saveEntryStream = saveEntry.Open()) + using (var saveFileStream = new FileStream(savePath, FileMode.Open)) + { + await saveFileStream.CopyToAsync(saveEntryStream); + } + } + + return mapper.Map(entity); + } + catch (Exception ex) + { + throw new ExportSkippedException(entity, "Could not add save to export file", ex); + } + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/SavePathExporter.cs b/LANCommander.Server.ImportExport/Exporters/SavePathExporter.cs new file mode 100644 index 00000000..2e1571a9 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/SavePathExporter.cs @@ -0,0 +1,27 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class SavePathExporter(SavePathService savePathService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.SavePath record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.SavePaths, + Name = record.Path, + }; + } + + public override bool CanExport(SavePath record) => ExportContext.DataRecord is Data.Models.Game; + + public override async Task ExportAsync(Guid id) + { + return await savePathService.GetAsync(id); + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/Exporters/ScriptExporter.cs b/LANCommander.Server.ImportExport/Exporters/ScriptExporter.cs new file mode 100644 index 00000000..6fc5a2c6 --- /dev/null +++ b/LANCommander.Server.ImportExport/Exporters/ScriptExporter.cs @@ -0,0 +1,60 @@ +using AutoMapper; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models.Manifest; +using LANCommander.Server.ImportExport.Exceptions; +using LANCommander.Server.ImportExport.Models; +using LANCommander.Server.Services; + +namespace LANCommander.Server.ImportExport.Exporters; + +public class ScriptExporter( + IMapper mapper, + ScriptService scriptService) : BaseExporter +{ + public override async Task GetExportInfoAsync(Data.Models.Script record) + { + return new ExportItemInfo + { + Id = record.Id, + Flag = ExportRecordFlags.Scripts, + Name = record.Name, + Size = record.Contents.Length, + }; + } + + public override bool CanExport(Script record) => + ExportContext.DataRecord is Data.Models.Game + || + ExportContext.DataRecord is Data.Models.Redistributable + || + ExportContext.DataRecord is Data.Models.Server; + + public override async Task