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