LANCommander/LANCommander.Server.ImportExport/ImportContext.cs
Pat Hartl 120e14d40c Add Tools section to server app
Allows users to upload additional tools and tie them to games. This can be useful for software like map editors, trainers, etc.
2026-02-08 02:24:53 -06:00

525 lines
No EOL
22 KiB
C#

using LANCommander.SDK;
using LANCommander.SDK.Enums;
using LANCommander.SDK.Helpers;
using LANCommander.Server.Data.Models;
using LANCommander.Server.ImportExport.Importers;
using LANCommander.Server.ImportExport.Models;
using LANCommander.Server.ImportExport.Services;
using LANCommander.Server.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
namespace LANCommander.Server.ImportExport;
public class ImportContext : IDisposable
{
private Guid? Id { get; set; }
public object Manifest { get; private set; }
public BaseModel DataRecord { get; private set; }
public StorageLocation ArchiveStorageLocation { get; set; }
public ZipArchive Archive { get; private set; }
public IImportItemInfo CurrentItem { get; set; }
public int Processed;
public int Total;
private Queue<IImportItemInfo> Queue { get; } = new();
private IEnumerable<Guid> SelectedRecordIds { get; set; } = [];
public AsyncEventHandler<ImportStatusUpdate> OnImportStarted { get; set; } = new();
public AsyncEventHandler<ImportStatusUpdate> OnImportStatusUpdate { get; set; } = new();
public AsyncEventHandler<ImportStatusUpdate> OnImportComplete { get; set; } = new();
public AsyncEventHandler<ImportStatusUpdate> OnImportError = new();
private readonly ImportService _importService;
private readonly StorageLocationService _storageLocationService;
private readonly ILogger<ImportContext> _logger;
#region Importers
private readonly ActionImporter _actions;
private readonly ArchiveImporter _archives;
private readonly CollectionImporter _collections;
private readonly CustomFieldImporter _customFields;
private readonly DeveloperImporter _developers;
private readonly EngineImporter _engines;
private readonly GameImporter _games;
private readonly GenreImporter _genres;
private readonly KeyImporter _keys;
private readonly MediaImporter _media;
private readonly MultiplayerModeImporter _multiplayerModes;
private readonly PlatformImporter _platforms;
private readonly PlaySessionImporter _playSessions;
private readonly PublisherImporter _publishers;
private readonly RedistributableImporter _redistributables;
private readonly SaveImporter _saves;
private readonly SavePathImporter _savePaths;
private readonly ScriptImporter _scripts;
private readonly ServerConsoleImporter _serverConsoles;
private readonly ServerHttpPathImporter _serverHttpPaths;
private readonly ServerImporter _servers;
private readonly TagImporter _tags;
private readonly ToolImporter _tools;
#endregion
public ImportContext(IServiceProvider serviceProvider)
{
_actions = serviceProvider.GetRequiredService<ActionImporter>();
_archives = serviceProvider.GetRequiredService<ArchiveImporter>();
_collections = serviceProvider.GetRequiredService<CollectionImporter>();
_customFields = serviceProvider.GetRequiredService<CustomFieldImporter>();
_developers = serviceProvider.GetRequiredService<DeveloperImporter>();
_engines = serviceProvider.GetRequiredService<EngineImporter>();
_games = serviceProvider.GetRequiredService<GameImporter>();
_genres = serviceProvider.GetRequiredService<GenreImporter>();
_keys = serviceProvider.GetRequiredService<KeyImporter>();
_media = serviceProvider.GetRequiredService<MediaImporter>();
_multiplayerModes = serviceProvider.GetRequiredService<MultiplayerModeImporter>();
_platforms = serviceProvider.GetRequiredService<PlatformImporter>();
_playSessions = serviceProvider.GetRequiredService<PlaySessionImporter>();
_publishers = serviceProvider.GetRequiredService<PublisherImporter>();
_redistributables = serviceProvider.GetRequiredService<RedistributableImporter>();
_saves = serviceProvider.GetRequiredService<SaveImporter>();
_savePaths = serviceProvider.GetRequiredService<SavePathImporter>();
_scripts = serviceProvider.GetRequiredService<ScriptImporter>();
_serverConsoles = serviceProvider.GetRequiredService<ServerConsoleImporter>();
_serverHttpPaths = serviceProvider.GetRequiredService<ServerHttpPathImporter>();
_servers = serviceProvider.GetRequiredService<ServerImporter>();
_tags = serviceProvider.GetRequiredService<TagImporter>();
_tools = serviceProvider.GetRequiredService<ToolImporter>();
_importService = serviceProvider.GetRequiredService<ImportService>();
_storageLocationService = serviceProvider.GetRequiredService<StorageLocationService>();
_logger = serviceProvider.GetRequiredService<ILogger<ImportContext>>();
}
internal bool InQueue<TRecord>(TRecord record, BaseImporter<TRecord> importer)
where TRecord : class =>
Queue.Any(qi => qi.Key == importer.GetKey(record));
public void SetId(Guid id) => Id = id;
#region Initialize Import
public async Task<IEnumerable<IImportItemInfo>> InitializeImportAsync(string archivePath)
{
_actions.UseContext(this);
_archives.UseContext(this);
_collections.UseContext(this);
_customFields.UseContext(this);
_developers.UseContext(this);
_engines.UseContext(this);
_games.UseContext(this);
_genres.UseContext(this);
_keys.UseContext(this);
_media.UseContext(this);
_multiplayerModes.UseContext(this);
_platforms.UseContext(this);
_playSessions.UseContext(this);
_publishers.UseContext(this);
_redistributables.UseContext(this);
_saves.UseContext(this);
_savePaths.UseContext(this);
_scripts.UseContext(this);
_serverConsoles.UseContext(this);
_serverHttpPaths.UseContext(this);
_servers.UseContext(this);
_tags.UseContext(this);
_tools.UseContext(this);
Archive = ZipArchive.Open(archivePath);
var manifestEntry = Archive.Entries.FirstOrDefault(e => e.Key == ManifestHelper.ManifestFilename);
// Legacy purposes
if (manifestEntry == null)
manifestEntry = Archive.Entries.FirstOrDefault(e => e.Key == "_manifest.yml");
if (manifestEntry == null)
throw new InvalidOperationException("Invalid import file, cannot load manifest");
using (var reader = new StreamReader(manifestEntry.OpenEntryStream()))
{
var manifestContents = await reader.ReadToEndAsync();
var manifestMeta = ManifestHelper.Deserialize<SDK.Models.Manifest.BaseManifest>(manifestContents);
// Check for legacy manifest
if (manifestMeta.IsLegacyManifest())
{
if (ManifestHelper.TryDeserialize<Legacy.Models.GameManifest>(manifestContents, out var legacyGameManifest))
return await InitializeLegacyGameImportAsync(legacyGameManifest);
}
if (ManifestHelper.TryDeserialize<SDK.Models.Manifest.Game>(manifestContents, out var gameManifest))
return await InitializeGameImportAsync(gameManifest);
if (ManifestHelper.TryDeserialize<SDK.Models.Manifest.Redistributable>(manifestContents, out var redistributableManifest))
return await InitializeRedistributableImportAsync(redistributableManifest);
if (ManifestHelper.TryDeserialize<SDK.Models.Manifest.Server>(manifestContents, out var serverManifest))
return await InitializeServerImportAsync(serverManifest);
if (ManifestHelper.TryDeserialize<SDK.Models.Manifest.Tool>(manifestContents, out var toolManifest))
return await InitializeToolImportAsync(toolManifest);
throw new InvalidOperationException("Unknown manifest file");
}
}
private async Task<IEnumerable<IImportItemInfo>> InitializeLegacyGameImportAsync(
Legacy.Models.GameManifest gameManifest)
{
var manifest = gameManifest.UpdateManifest();
if (gameManifest.Scripts != null)
foreach (var script in gameManifest.Scripts)
{
_scripts.AddAsset(new ImportAssetArchiveEntry
{
Name = script.Name,
RecordId = script.Id,
Path = $"Scripts/{script.Id}",
});
}
if (gameManifest.Media != null)
foreach (var media in gameManifest.Media)
{
_media.AddAsset(new ImportAssetArchiveEntry
{
RecordId = media.Id,
Name = media.Type.ToString(),
Path = $"Media/{media.FileId}",
});
}
if (gameManifest.Archives != null)
foreach (var archive in gameManifest.Archives)
{
_archives.AddAsset(new ImportAssetArchiveEntry
{
RecordId = archive.Id,
Name = archive.Version,
Path = $"Archives/{archive.ObjectKey}",
});
}
return await InitializeGameImportAsync(manifest);
}
private async Task<IEnumerable<IImportItemInfo>> InitializeGameImportAsync(SDK.Models.Manifest.Game gameManifest)
{
Manifest = gameManifest;
var importItemInfo = new List<IImportItemInfo>();
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Actions, _actions).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Archives, _archives).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Collections, _collections).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.CustomFields, _customFields).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Developers, _developers).ToListAsync());
if (gameManifest.Engine != null)
importItemInfo.AddRange(await GetImportItemInfoAsync([gameManifest.Engine], _engines).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Genres, _genres).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Keys, _keys).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Media, _media).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.MultiplayerModes, _multiplayerModes).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Platforms, _platforms).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.PlaySessions, _playSessions).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Publishers, _publishers).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Saves, _saves).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.SavePaths, _savePaths).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Scripts, _scripts).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Tags, _tags).ToListAsync());
return importItemInfo;
}
private async Task<IEnumerable<IImportItemInfo>> InitializeRedistributableImportAsync(SDK.Models.Manifest.Redistributable redistributableManifest)
{
Manifest = redistributableManifest;
var importItemInfo = new List<IImportItemInfo>();
importItemInfo.AddRange(await GetImportItemInfoAsync(redistributableManifest.Archives, _archives).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(redistributableManifest.Scripts, _scripts).ToListAsync());
return importItemInfo;
}
private async Task<IEnumerable<IImportItemInfo>> InitializeServerImportAsync(SDK.Models.Manifest.Server serverManifest)
{
Manifest = serverManifest;
var importItemInfo = new List<IImportItemInfo>();
importItemInfo.AddRange(await GetImportItemInfoAsync(serverManifest.Actions, _actions).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(serverManifest.Scripts, _scripts).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(serverManifest.ServerConsoles, _serverConsoles).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(serverManifest.HttpPaths, _serverHttpPaths).ToListAsync());
return importItemInfo;
}
private async Task<IEnumerable<IImportItemInfo>> InitializeToolImportAsync(SDK.Models.Manifest.Tool toolManifest)
{
Manifest = toolManifest;
var importItemInfo = new List<IImportItemInfo>();
importItemInfo.AddRange(await GetImportItemInfoAsync(toolManifest.Archives, _archives).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(toolManifest.Scripts, _scripts).ToListAsync());
return importItemInfo;
}
#endregion
public async Task PrepareImportQueueAsync(IEnumerable<Guid> selectedRecordIds, Guid storageLocationId)
{
SelectedRecordIds = selectedRecordIds;
ArchiveStorageLocation = await _storageLocationService.GetAsync(storageLocationId);
if (Manifest is SDK.Models.Manifest.Game gameManifest)
await AddAsync(gameManifest);
if (Manifest is SDK.Models.Manifest.Redistributable redistributableManifest)
await AddAsync(redistributableManifest);
if (Manifest is SDK.Models.Manifest.Server serverManifest)
await AddAsync(serverManifest);
if (Manifest is SDK.Models.Manifest.Tool toolManifest)
await AddAsync(toolManifest);
}
public async Task AddAsync(SDK.Models.Manifest.Game game)
{
await AddAsync(game.Actions, _actions);
await AddAsync(game.Archives, _archives);
await AddAsync(game.Collections, _collections);
await AddAsync(game.CustomFields, _customFields);
await AddAsync(game.Developers, _developers);
await AddAsync(game.Engine, _engines);
await AddAsync(game.Genres, _genres);
await AddAsync(game.Keys, _keys);
await AddAsync(game.Media, _media);
await AddAsync(game.MultiplayerModes, _multiplayerModes);
await AddAsync(game.Platforms, _platforms);
await AddAsync(game.PlaySessions, _playSessions);
await AddAsync(game.Publishers, _publishers);
await AddAsync(game.Saves, _saves);
await AddAsync(game.SavePaths, _savePaths);
await AddAsync(game.Scripts, _scripts);
await AddAsync(game.Tags, _tags);
await AddAsync(game, _games);
}
public async Task AddAsync(SDK.Models.Manifest.Redistributable redistributable)
{
await AddAsync(redistributable.Archives, _archives);
await AddAsync(redistributable.Scripts, _scripts);
await AddAsync(redistributable, _redistributables);
}
public async Task AddAsync(SDK.Models.Manifest.Server server)
{
await AddAsync(server.Actions, _actions);
await AddAsync(server.Scripts, _scripts);
await AddAsync(server.HttpPaths, _serverHttpPaths);
await AddAsync(server.ServerConsoles, _serverConsoles);
await AddAsync(server, _servers);
}
public async Task AddAsync(SDK.Models.Manifest.Tool tool)
{
await AddAsync(tool.Archives, _archives);
await AddAsync(tool.Scripts, _scripts);
await AddAsync(tool, _tools);
}
private async Task AddAsync<TRecord>(IEnumerable<TRecord> records, BaseImporter<TRecord> importer)
where TRecord : class
{
foreach (var record in records)
await AddAsync(record, importer);
}
private async Task AddAsync<TRecord>(TRecord? record, BaseImporter<TRecord> importer)
where TRecord : class
{
if (record != null && !InQueue(record, importer) && await importer.CanImportAsync(record))
{
var importInfo = await importer.GetImportInfoAsync(record);
importInfo.Key = importer.GetKey(record);
Queue.Enqueue(importInfo);
}
}
public async Task ImportQueueAsync()
{
Processed = 0;
Total = Queue.Count;
await OnImportStarted?.InvokeAsync(new ImportStatusUpdate
{
Index = Processed,
Total = Total,
})!;
int deferred = 0;
while (Queue.Count > 0)
{
var queueItem = Queue.Dequeue();
await OnImportStatusUpdate?.InvokeAsync(new ImportStatusUpdate
{
CurrentItem = queueItem,
Index = Processed,
Total = Total,
})!;
var success = await TryImportAsync(queueItem);
if (success)
{
Processed++;
deferred = 0;
continue;
}
Queue.Enqueue(queueItem);
deferred++;
if (deferred >= Queue.Count)
throw new InvalidOperationException("Import deadlocked: remaining jobs cannot be satisfied.");
}
await OnImportComplete?.InvokeAsync(new ImportStatusUpdate
{
Index = Total,
Total = Total,
})!;
_importService.RemoveContext(Id.Value);
}
private async Task<bool> TryImportAsync(IImportItemInfo queueItem)
{
try
{
switch (queueItem.Type)
{
case ImportExportRecordType.Action:
return await _actions.ImportAsync(queueItem);
case ImportExportRecordType.Archive:
return await _archives.ImportAsync(queueItem);
case ImportExportRecordType.Collection:
return await _collections.ImportAsync(queueItem);
case ImportExportRecordType.CustomField:
return await _customFields.ImportAsync(queueItem);
case ImportExportRecordType.Developer:
return await _developers.ImportAsync(queueItem);
case ImportExportRecordType.Engine:
return await _engines.ImportAsync(queueItem);
case ImportExportRecordType.Game:
return await _games.ImportAsync(queueItem);
case ImportExportRecordType.Genre:
return await _genres.ImportAsync(queueItem);
case ImportExportRecordType.Key:
return await _keys.ImportAsync(queueItem);
case ImportExportRecordType.Media:
return await _media.ImportAsync(queueItem);
case ImportExportRecordType.MultiplayerMode:
return await _multiplayerModes.ImportAsync(queueItem);
case ImportExportRecordType.Platform:
return await _platforms.ImportAsync(queueItem);
case ImportExportRecordType.PlaySession:
return await _playSessions.ImportAsync(queueItem);
case ImportExportRecordType.Publisher:
return await _publishers.ImportAsync(queueItem);
case ImportExportRecordType.Redistributable:
return await _redistributables.ImportAsync(queueItem);
case ImportExportRecordType.Save:
return await _saves.ImportAsync(queueItem);
case ImportExportRecordType.SavePath:
return await _savePaths.ImportAsync(queueItem);
case ImportExportRecordType.Script:
return await _scripts.ImportAsync(queueItem);
case ImportExportRecordType.Server:
return await _servers.ImportAsync(queueItem);
case ImportExportRecordType.ServerConsole:
return await _serverConsoles.ImportAsync(queueItem);
case ImportExportRecordType.ServerHttpPath:
return await _serverHttpPaths.ImportAsync(queueItem);
case ImportExportRecordType.Tag:
return await _tags.ImportAsync(queueItem);
case ImportExportRecordType.Tool:
return await _tools.ImportAsync(queueItem);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error importing record {RecordName}", queueItem.Name);
await OnImportError?.InvokeAsync(new ImportStatusUpdate
{
CurrentItem = CurrentItem,
Index = Processed,
Total = Total,
Error = ex.Message,
})!;
}
return false;
}
private async IAsyncEnumerable<ImportItemInfo<TRecord>> GetImportItemInfoAsync<TRecord>(IEnumerable<TRecord> records,
BaseImporter<TRecord> importer) where TRecord : class
{
if (records != null)
foreach (var record in records)
{
if (record != null && record.GetType() == typeof(TRecord))
{
var importInfo = await importer.GetImportInfoAsync(record);
importInfo.Key = importer.GetKey(record);
yield return importInfo;
}
}
}
public void Dispose()
{
if (Archive != null)
Archive.Dispose();
}
}