diff --git a/LANCommander.Launcher.Data/Models/Game.cs b/LANCommander.Launcher.Data/Models/Game.cs index b5539c78..08ecc983 100644 --- a/LANCommander.Launcher.Data/Models/Game.cs +++ b/LANCommander.Launcher.Data/Models/Game.cs @@ -43,6 +43,7 @@ namespace LANCommander.Launcher.Data.Models public virtual ICollection? Developers { get; set; } = new List(); public virtual ICollection? Platforms { get; set; } = new List(); public virtual ICollection? Redistributables { get; set; } = new List(); + public virtual ICollection? Tools { get; set; } = new List(); public virtual ICollection? Media { get; set; } = new List(); public virtual ICollection Collections { get; set; } = new List(); public virtual ICollection DependentGames { get; set; } = new List(); diff --git a/LANCommander.Launcher.Data/Models/Tool.cs b/LANCommander.Launcher.Data/Models/Tool.cs new file mode 100644 index 00000000..6b46bd0d --- /dev/null +++ b/LANCommander.Launcher.Data/Models/Tool.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace LANCommander.Launcher.Data.Models +{ + [Table("Tools")] + public class Tool : BaseModel + { + public string Name { get; set; } + public string? Description { get; set; } + public string? Notes { get; set; } + + public bool Installed { get; set; } + public string? InstallDirectory { get; set; } + public string? InstalledVersion { get; set; } + public DateTime? InstalledOn { get; set; } + public string? LatestVersion { get; set; } + + public virtual ICollection? Games { get; set; } = new List(); + } +} diff --git a/LANCommander.Launcher.Models/IInstallQueueItem.cs b/LANCommander.Launcher.Models/IInstallQueueItem.cs index 5677b7a0..3cdbec74 100644 --- a/LANCommander.Launcher.Models/IInstallQueueItem.cs +++ b/LANCommander.Launcher.Models/IInstallQueueItem.cs @@ -10,8 +10,6 @@ namespace LANCommander.Launcher.Models public interface IInstallQueueItem { Guid Id { get; set; } - Guid[] AddonIds { get; set; } - Dictionary AddonVersions { get; set; } string Title { get; set; } string Version { get; set; } string InstallDirectory { get; set; } diff --git a/LANCommander.Launcher.Models/InstallQueueTool.cs b/LANCommander.Launcher.Models/InstallQueueTool.cs new file mode 100644 index 00000000..c9894b7a --- /dev/null +++ b/LANCommander.Launcher.Models/InstallQueueTool.cs @@ -0,0 +1,65 @@ +using LANCommander.SDK.Enums; + +namespace LANCommander.Launcher.Models; + +public class InstallQueueTool : IInstallQueueItem +{ + public Guid Id { get; set; } + public Guid[] AddonIds { get; set; } + public Dictionary AddonVersions { get; set; } + public string Title { get; set; } + public string Version { get; set; } + public string InstallDirectory { get; set; } + public Guid CoverId { get; set; } + public Guid IconId { get; set; } + public DateTime QueuedOn { get; set; } + public DateTime? CompletedOn { get; set; } + public bool IsUpdate { get; set; } + + public bool State { + get + { + switch (Status) + { + case InstallStatus.Starting: + case InstallStatus.Moving: + case InstallStatus.Downloading: + case InstallStatus.InstallingRedistributables: + case InstallStatus.InstallingMods: + case InstallStatus.InstallingExpansions: + case InstallStatus.InstallingAddons: + case InstallStatus.RunningScripts: + case InstallStatus.DownloadingSaves: + return true; + + default: + return false; + } + } + } + + public InstallStatus Status { get; set; } + public SDK.Models.Tool Tool { get; set; } + + public float Progress { + get + { + return BytesDownloaded / (float)TotalBytes; + } + set { } + } + public double TransferSpeed { get; set; } + public long BytesDownloaded { get; set; } + public long TotalBytes { get; set; } + public CancellationTokenSource CancellationToken { get; set; } = new(); + + public InstallQueueTool(SDK.Models.Tool tool) + { + Tool = tool; + Id = tool.Id; + Title = tool.Name; + Version = tool.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault()?.Version ?? ""; + QueuedOn = DateTime.Now; + Status = InstallStatus.Queued; + } +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs b/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs index 77c5e0d6..e2bb2f51 100644 --- a/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs +++ b/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs @@ -26,10 +26,10 @@ namespace LANCommander.Launcher.Services.Extensions }); #region Register Client - var options = new LANCommanderOptions(); - - configure?.Invoke(options); - + var options = new LANCommanderOptions(); + + configure?.Invoke(options); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -58,6 +58,7 @@ namespace LANCommander.Launcher.Services.Extensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/LANCommander.Launcher.Services/Import/ImportContext.cs b/LANCommander.Launcher.Services/Import/ImportContext.cs index b14aa8fb..fe0389a0 100644 --- a/LANCommander.Launcher.Services/Import/ImportContext.cs +++ b/LANCommander.Launcher.Services/Import/ImportContext.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; namespace LANCommander.Launcher.Services.Import; -public class ImportContext +public class ImportContext { private int Processed; private int Total; @@ -27,27 +27,28 @@ public class ImportContext private readonly MultiplayerModeImporter _multiplayerModes; private readonly PlatformImporter _platforms; private readonly PublisherImporter _publishers; - private readonly TagImporter _tags; - - private readonly ILogger _logger; - - public ImportContext(IServiceProvider serviceProvider) - { - _collections = serviceProvider.GetRequiredService(); - _developers = serviceProvider.GetRequiredService(); - _engines = serviceProvider.GetRequiredService(); - _games = serviceProvider.GetRequiredService(); - _genres = serviceProvider.GetRequiredService(); - _media = serviceProvider.GetRequiredService(); - _multiplayerModes = serviceProvider.GetRequiredService(); - _platforms = serviceProvider.GetRequiredService(); - _publishers = serviceProvider.GetRequiredService(); - _tags = serviceProvider.GetRequiredService(); - _logger = serviceProvider.GetRequiredService>(); - - SetupContextOnImporters(); - } - + private readonly TagImporter _tags; + private readonly ToolImporter _tools; + + private readonly ILogger _logger; + + public ImportContext(IServiceProvider serviceProvider) + { + _collections = serviceProvider.GetRequiredService(); + _developers = serviceProvider.GetRequiredService(); + _engines = serviceProvider.GetRequiredService(); + _games = serviceProvider.GetRequiredService(); + _genres = serviceProvider.GetRequiredService(); + _media = serviceProvider.GetRequiredService(); + _multiplayerModes = serviceProvider.GetRequiredService(); + _platforms = serviceProvider.GetRequiredService(); + _publishers = serviceProvider.GetRequiredService(); + _tags = serviceProvider.GetRequiredService(); + _logger = serviceProvider.GetRequiredService>(); + + SetupContextOnImporters(); + } + public async Task AddAsync(Game game) { await AddAsync(game, game.Collections, _collections); @@ -62,6 +63,11 @@ public class ImportContext await AddAsync(game, game, _games); } + public async Task AddAsync(Tool tool) + { + await AddAsync(tool, tool, _tools); + } + private async Task AddAsync(BaseManifest manifest, IEnumerable records, BaseImporter importer) where TRecord : class { @@ -76,8 +82,8 @@ public class ImportContext { var importInfo = await importer.GetImportInfoAsync(record, manifest); - importInfo.Key = importer.GetKey(record); - + importInfo.Key = importer.GetKey(record); + _logger.LogInformation("Queuing item {ItemName} for import with key {Key}", importInfo.Name, importInfo.Key); Queue.Enqueue(importInfo); diff --git a/LANCommander.Launcher.Services/Import/Importers/ToolImporter.cs b/LANCommander.Launcher.Services/Import/Importers/ToolImporter.cs new file mode 100644 index 00000000..e7cefcb2 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/ToolImporter.cs @@ -0,0 +1,104 @@ +using LANCommander.Launcher.Services.Exceptions; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.Models.Manifest; +using LANCommander.SDK.Services; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public class ToolImporter( + ToolService toolService, + LibraryService libraryService, + ILogger logger) : BaseImporter +{ + public override async Task> GetImportInfoAsync(Tool record, BaseManifest manifest) => + new() + { + Key = GetKey(record), + Name = record.Name, + Type = nameof(Game), + Record = record, + }; + + public override string GetKey(Tool record) => $"{nameof(Tool)}/{record.Id}"; + + public override async Task CanImportAsync(Tool record) + { + var existing = await toolService.GetAsync(record.Id); + + if (existing == null) + return true; + + return + record.UpdatedOn > existing.ImportedOn + || + record.Actions.Any(a => a.UpdatedOn > existing.ImportedOn || a.CreatedOn > existing.ImportedOn); + } + + public override async Task AddAsync(ImportItemInfo importItemInfo) + { + try + { + var tool = new Data.Models.Tool + { + Id = importItemInfo.Record.Id, + Name = importItemInfo.Record.Name, + Description = importItemInfo.Record.Description, + Notes = importItemInfo.Record.Notes, + CreatedOn = importItemInfo.Record.CreatedOn, + UpdatedOn = importItemInfo.Record.UpdatedOn, + ImportedOn = DateTime.UtcNow, + }; + + await toolService.AddAsync(tool); + await UpdateRelationships(importItemInfo.Record); + + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "Could not add tool | {Key}", GetKey(importItemInfo.Record)); + return false; + } + } + + public override async Task UpdateAsync(ImportItemInfo importItemInfo) + { + var existing = await toolService.GetAsync(importItemInfo.Record.Id); + + try + { + existing.Name = importItemInfo.Record.Name; + existing.Description = importItemInfo.Record.Description; + existing.Notes = importItemInfo.Record.Notes; + existing.CreatedOn = importItemInfo.Record.CreatedOn; + existing.ImportedOn = DateTime.UtcNow; + // existing.LatestVersion = importItemInfo.Record.Version; + + await toolService.UpdateAsync(existing); + await UpdateRelationships(importItemInfo.Record); + + if (await libraryService.IsInstalledAsync(existing.Id) && existing.LatestVersion == existing.InstalledVersion) + await ManifestHelper.WriteAsync(importItemInfo.Record, existing.InstallDirectory); + + return true; + } + catch (Exception ex) + { + throw new ImportSkippedException(importItemInfo.Record, "An unknown error occurred while trying to update tool", ex); + } + } + + private async Task UpdateRelationships(Tool manifest) + { + var tool = await toolService.GetAsync(manifest.Id); + + await toolService.SyncRelatedCollectionAsync( + tool, + t => t.Games, + manifest.Games, + r => c => c.Title == r.Title); + } + + public override async Task ExistsAsync(ImportItemInfo importItemInfo) => await toolService.ExistsAsync(importItemInfo.Record.Id); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/ImportService.cs b/LANCommander.Launcher.Services/ImportService.cs index 02e728f3..51498b1a 100644 --- a/LANCommander.Launcher.Services/ImportService.cs +++ b/LANCommander.Launcher.Services/ImportService.cs @@ -11,6 +11,7 @@ namespace LANCommander.Launcher.Services ILogger logger, ImportContextFactory importContextFactory, GameClient gameClient, + ToolClient toolClient, LibraryClient libraryClient) : BaseService(logger) { private ImportProgress _importProgress = new(); @@ -65,5 +66,15 @@ namespace LANCommander.Launcher.Services await importContext.AddAsync(manifest); await importContext.ImportQueueAsync(); } + + public async Task ImportToolAsync(Guid toolId) + { + var importContext = importContextFactory.Create(); + + var manifest = await toolClient.GetManifestAsync(toolId); + + await importContext.AddAsync(manifest); + await importContext.ImportQueueAsync(); + } } } diff --git a/LANCommander.Launcher.Services/InstallService.cs b/LANCommander.Launcher.Services/InstallService.cs index 224c8471..ce545fec 100644 --- a/LANCommander.Launcher.Services/InstallService.cs +++ b/LANCommander.Launcher.Services/InstallService.cs @@ -13,8 +13,11 @@ namespace LANCommander.Launcher.Services public class InstallService : BaseService { private readonly GameService _gameService; + private readonly ToolService _toolService; + private readonly ImportService _importService; private readonly GameClient _gameClient; private readonly RedistributableClient _redistributableClient; + private readonly ToolClient _toolClient; private readonly MediaClient _mediaClient; private Stopwatch Stopwatch { get; set; } @@ -37,13 +40,19 @@ namespace LANCommander.Launcher.Services public InstallService( ILogger logger, GameService gameService, + ToolService toolService, + ImportService importService, GameClient gameClient, RedistributableClient redistributableClient, + ToolClient toolClient, MediaClient mediaClient) : base(logger) { _gameService = gameService; + _toolService = toolService; + _importService = importService; _gameClient = gameClient; _redistributableClient = redistributableClient; + _toolClient = toolClient; _mediaClient = mediaClient; Stopwatch = new Stopwatch(); @@ -154,6 +163,54 @@ namespace LANCommander.Launcher.Services OnQueueChanged?.Invoke(); } } + + public async Task Add(SDK.Models.Tool tool, string installDirectory = "") + { + var toolInfo = await _toolClient.GetAsync(tool.Id); + + // TODO: Throw exception (and gracefully handle) when gameInfo == null + // Game probably couldn't be found or deserialized from server + + Logger?.LogTrace("Adding game {ToolName} to the queue", toolInfo.Name); + + try + { + var toolCompletedQueueItems = Queue.Where(i => i.Status == InstallStatus.Complete && i.Id == tool.Id).ToList(); + + foreach (var queueItem in toolCompletedQueueItems) + { + Queue.Remove(queueItem); + } + + OnQueueChanged?.Invoke(); + } + catch (Exception ex) + { + + } + + if (!Queue.Any(i => i.Id == tool.Id && i.Status == InstallStatus.Queued)) + { + var queueItem = new InstallQueueTool(toolInfo); + + queueItem.InstallDirectory = installDirectory; + + if (Queue.Any(i => i.State)) + Queue.Add(queueItem); + else + { + Logger?.LogTrace("Download queue is empty, starting the tool download immediately"); + + queueItem.Status = InstallStatus.Starting; + + Queue.Add(queueItem); + + await Next(); + } + + OnQueueChanged?.Invoke(); + } + } public void Remove(Guid id) { @@ -197,18 +254,27 @@ namespace LANCommander.Launcher.Services if (currentItem == null) return; + if (currentItem is InstallQueueGame gameQueueItem) + await Next(gameQueueItem); + + if (currentItem is InstallQueueTool toolQueueItem) + await Next(toolQueueItem); + } + + private async Task Next(InstallQueueGame queueItem) + { Game localGame = null; SDK.Models.Game remoteGame = null; try { - localGame = await _gameService.GetAsync(currentItem.Id); - remoteGame = await _gameClient.GetAsync(currentItem.Id); + localGame = await _gameService.GetAsync(queueItem.Id); + remoteGame = await _gameClient.GetAsync(queueItem.Id); if (localGame == null) { Logger?.LogError("Game does not exist in local database, skipping"); - Remove(currentItem); + Remove(queueItem); OnQueueChanged?.Invoke(); return; } @@ -217,7 +283,7 @@ namespace LANCommander.Launcher.Services { Logger?.LogError("Game info could not be retrieved from the server"); - currentItem.Status = InstallStatus.Failed; + queueItem.Status = InstallStatus.Failed; OnQueueChanged?.Invoke(); return; } @@ -228,31 +294,31 @@ namespace LANCommander.Launcher.Services await _gameClient.UpdateGameInstallationAsync(localGame.InstallDirectory, remoteGame); // Probably doing a modification of some sort - if (localGame.InstallDirectory.StartsWith(currentItem.InstallDirectory)) + if (localGame.InstallDirectory.StartsWith(queueItem.InstallDirectory)) { var allAddons = remoteGame.DependentGames.ToArray(); - var removeAddons = allAddons.Except(currentItem.AddonIds ?? []).ToArray(); - var addAddons = allAddons.Intersect(currentItem.AddonIds ?? []).ToArray(); + var removeAddons = allAddons.Except(queueItem.AddonIds ?? []).ToArray(); + var addAddons = allAddons.Intersect(queueItem.AddonIds ?? []).ToArray(); var uninstallResult = await _gameClient.UninstallAddonsAsync(localGame.InstallDirectory, localGame.Id, removeAddons); var installResult = await _gameClient.InstallAddonsAsync(localGame.InstallDirectory, localGame.Id, addAddons); await _gameClient.RestoreFilesAsync(localGame.InstallDirectory, localGame.Id, uninstallResult.FileList, installResult.FileList); - UpdateGameState(currentItem, localGame, localGame.InstallDirectory); + UpdateGameState(queueItem, localGame, localGame.InstallDirectory); await _gameService.UpdateAsync(localGame); - currentItem.Status = InstallStatus.Complete; + queueItem.Status = InstallStatus.Complete; OnQueueChanged?.Invoke(); OnInstallComplete?.Invoke(localGame); } else { - await Move(currentItem, localGame, remoteGame); + await Move(queueItem, localGame, remoteGame); } } else { - await Install(currentItem, localGame, remoteGame); + await Install(queueItem, localGame, remoteGame); } } catch (Exception ex) @@ -261,7 +327,51 @@ namespace LANCommander.Launcher.Services } } - public async Task Install(IInstallQueueItem currentItem, Game localGame, SDK.Models.Game remoteGame) + private async Task Next(InstallQueueTool queueItem) + { + Tool localTool = null; + SDK.Models.Tool remoteTool = null; + + try + { + localTool = await _toolService.GetAsync(queueItem.Id); + remoteTool = await _toolClient.GetAsync(queueItem.Id); + + if (remoteTool == null) + { + Logger?.LogError("Tool info could not be retrieved from the server"); + + queueItem.Status = InstallStatus.Failed; + OnQueueChanged?.Invoke(); + return; + } + + if (localTool == null) + { + Logger?.LogError("Tool does not exist in local database, importing"); + + await _importService.ImportToolAsync(queueItem.Id); + + await Next(queueItem); + + return; + } + + if (localTool.Installed) + { + // Modify + } + else + { + await Install(queueItem, localTool, remoteTool); + } + } + catch + { + } + } + + public async Task Install(InstallQueueGame currentItem, Game localGame, SDK.Models.Game remoteGame) { using (var operation = Logger.BeginOperation("Installing game {GameTitle} ({GameId})", localGame.Title, localGame.Id)) { @@ -352,7 +462,65 @@ namespace LANCommander.Launcher.Services await Next(); } - private static void UpdateGameState(IInstallQueueItem currentItem, Game localGame, string installDirectory) + public async Task Install(InstallQueueTool currentItem, Tool localTool, SDK.Models.Tool remoteTool) + { + using (var operation = Logger.BeginOperation("Installing tool {ToolName} ({ToolId})", localTool.Name, localTool.Id)) + { + currentItem.Status = InstallStatus.Downloading; + OnQueueChanged?.Invoke(); + + try + { + var result = await _toolClient.InstallAsync(remoteTool, currentItem.InstallDirectory); + + UpdateToolState(currentItem, localTool, result.InstallDirectory); + } + catch (InstallCanceledException ex) + { + Logger?.LogError("Install canceled, removing from queue"); + Queue.Remove(currentItem); + return; + } + catch (InstallException ex) + { + Logger?.LogError(ex, "An error occurred during install, removing from queue"); + Queue.Remove(currentItem); + return; + } + catch (Exception ex) + { + Logger.LogError(ex, "An unknown error occurred during install, removing from queue"); + Queue.Remove(currentItem); + return; + } + + currentItem.CompletedOn = DateTime.Now; + currentItem.Status = InstallStatus.Complete; + currentItem.Progress = 1; + currentItem.BytesDownloaded = currentItem.TotalBytes; + + try + { + await _toolService.UpdateAsync(localTool); + } + catch (Exception ex) + { + Logger?.LogError(ex, "An unknown error occurred while trying to write changes to the database after install of tool {ToolName} ({ToolId})", localTool.Name, localTool.Id); + } + + OnQueueChanged?.Invoke(); + + Logger?.LogTrace("Install of tool {ToolName} ({ToolId}) complete!", localTool.Name, localTool.Id); + + // OnInstallComplete?.Invoke(localTool); + + operation.Complete(); + } + + await Next(); + } + + private static void UpdateGameState(InstallQueueGame currentItem, Game localGame, string installDirectory) { localGame.InstallDirectory = installDirectory; localGame.Installed = true; @@ -380,6 +548,14 @@ namespace LANCommander.Launcher.Services } } } + + private static void UpdateToolState(InstallQueueTool currentItem, Tool localTool, string installDirectory) + { + localTool.InstallDirectory = installDirectory; + localTool.Installed = true; + localTool.InstalledVersion = currentItem.Version; + localTool.InstalledOn ??= DateTime.Now; + } public async Task Move(IInstallQueueItem currentItem, Game localGame, SDK.Models.Game remoteGame) { diff --git a/LANCommander.Launcher.Services/ToolService.cs b/LANCommander.Launcher.Services/ToolService.cs new file mode 100644 index 00000000..c3057a4e --- /dev/null +++ b/LANCommander.Launcher.Services/ToolService.cs @@ -0,0 +1,12 @@ +using LANCommander.Launcher.Data; +using LANCommander.Launcher.Data.Models; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services +{ + public class ToolService( + ILogger logger, + DatabaseContext dbContext) : BaseDatabaseService(dbContext, logger) + { + } +} diff --git a/LANCommander.Launcher/UI/Components/InstallDialog/InstallDialog.razor b/LANCommander.Launcher/UI/Components/InstallDialog/InstallDialog.razor index 4fe6ffc9..02b36a5e 100644 --- a/LANCommander.Launcher/UI/Components/InstallDialog/InstallDialog.razor +++ b/LANCommander.Launcher/UI/Components/InstallDialog/InstallDialog.razor @@ -3,6 +3,7 @@ @namespace LANCommander.Launcher.UI @inherits FeedbackComponent @inject InstallService InstallService +@inject ToolService ToolService @inject LocalizationService LocalizationService @inject GameClient GameClient @inject ISettingsProvider SettingsProvider @@ -24,6 +25,13 @@ } + + @if (Tools.Any()) + { + + + + } @if (SettingsProvider.CurrentValue.Games.InstallDirectories.Length > 1) { @@ -65,7 +73,8 @@ List Addons = new(); IEnumerable SelectedAddons = new List(); - + List Tools = new(); + IEnumerable SelectedTools = new List(); protected override async Task OnInitializedAsync() { @@ -78,10 +87,41 @@ SelectedDirectory = SettingsProvider.CurrentValue.Games.InstallDirectories.First(); RemoteGame = await GameClient.GetAsync(Options.Key); - Addons = (await GameClient.GetAddonsAsync(Options.Key)).OrderByTitle(g => g.SortTitle ?? g.Title).ToList(); + + await LoadAddons(); + await LoadTools(); var localAddons = Game?.DependentGames.Where(g => g.Installed).Select(addon => addon.Id).ToArray() ?? []; SelectedAddons = Addons?.Where(addon => localAddons.Contains(addon.Id)).ToList() ?? []; + + var localTools = Game?.Tools.Where(t => t.Installed).Select(tool => tool.Id).ToArray() ?? []; + SelectedTools = Tools?.Where(tool => localTools.Contains(tool.Id)).ToList() ?? []; + } + + async Task LoadAddons() + { + try + { + Addons = (await GameClient.GetAddonsAsync(Options.Key)).OrderByTitle(g => g.SortTitle ?? g.Title).ToList(); + } + catch (Exception ex) + { + // TODO: Add logging + Addons = []; + } + } + + async Task LoadTools() + { + try + { + Tools = (await GameClient.GetToolsAsync(Options.Key)).OrderByTitle(t => t.Name).ToList(); + } + catch (Exception ex) + { + // TODO: Add logging + Tools = []; + } } async Task Close() @@ -93,7 +133,13 @@ { var game = Options.DataItem as Game; - InstallService.Add(game, SelectedDirectory, SelectedAddons.ToArray()); + await InstallService.Add(game, SelectedDirectory, SelectedAddons.ToArray()); + + foreach (var tool in SelectedTools) + { + + await InstallService.Add(tool, SelectedDirectory); + } await CloseFeedbackAsync(); } @@ -121,6 +167,8 @@ size += RemoteGame.Archives.OrderByDescending(a => a.CreatedOn).First().CompressedSize; size += SelectedAddons.Sum(a => a.Archives.OrderByDescending(arc => arc.CreatedOn).First().CompressedSize); + + size += SelectedTools.Sum(t => t.Archives.OrderByDescending(arc => arc.CreatedOn).First().CompressedSize); } return size; @@ -135,6 +183,8 @@ size += RemoteGame.Archives.OrderByDescending(a => a.CreatedOn).First().UncompressedSize; size += SelectedAddons.Sum(a => a.Archives.OrderByDescending(arc => arc.CreatedOn).First().UncompressedSize); + + size += SelectedTools.Sum(t => t.Archives.OrderByDescending(arc => arc.CreatedOn).First().UncompressedSize); } return size; diff --git a/LANCommander.SDK/Clients/GameClient.cs b/LANCommander.SDK/Clients/GameClient.cs index dcb67fc7..20a7c6ef 100644 --- a/LANCommander.SDK/Clients/GameClient.cs +++ b/LANCommander.SDK/Clients/GameClient.cs @@ -71,7 +71,8 @@ namespace LANCommander.SDK.Services SaveClient saveClient, ScriptClient scriptClient, ProfileClient profileClient, - LobbyClient lobbyClient) + LobbyClient lobbyClient, + ToolClient toolClient) { public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e); public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress; @@ -236,6 +237,16 @@ namespace LANCommander.SDK.Services .GetAsync>(); } + public async Task> GetToolsAsync(Guid id) + { + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Games/{id}/Tools") + .GetAsync>(); + } + public async Task CheckForUpdateAsync(Guid id, string currentVersion) { return await apiRequestFactory diff --git a/LANCommander.SDK/Clients/ScriptClient.Tools.cs b/LANCommander.SDK/Clients/ScriptClient.Tools.cs index 458a7ff5..4ac06623 100644 --- a/LANCommander.SDK/Clients/ScriptClient.Tools.cs +++ b/LANCommander.SDK/Clients/ScriptClient.Tools.cs @@ -15,14 +15,14 @@ namespace LANCommander.SDK.Services; public partial class ScriptClient { - public async Task Tool_RunDetectInstallScriptAsync(string installDirectory, Guid gameId, Tool tool) + public async Task Tool_RunDetectInstallScriptAsync(string installDirectory, Guid gameId, Guid toolId) { bool result = default; var gameManifest = await ManifestHelper.ReadAsync(installDirectory, gameId); - var toolManifest = await ManifestHelper.ReadAsync(installDirectory, tool.Id); + var toolManifest = await ManifestHelper.ReadAsync(installDirectory, toolId); - var path = ScriptHelper.GetScriptFilePath(installDirectory, tool.Id, Enums.ScriptType.DetectInstall); + var path = ScriptHelper.GetScriptFilePath(installDirectory, toolId, Enums.ScriptType.DetectInstall); try { @@ -42,7 +42,7 @@ public partial class ScriptClient { op.Enrich("InstallDirectory", installDirectory) .Enrich("GameManifestPath", ManifestHelper.GetPath(installDirectory, gameId)) - .Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, tool.Id)) + .Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, toolId)) .Enrich("ScriptPath", path) .Enrich("GameTitle", gameManifest.Title) .Enrich("GameId", gameManifest.Id) @@ -62,7 +62,7 @@ public partial class ScriptClient } } - script.UseWorkingDirectory(Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, tool.Id))); + script.UseWorkingDirectory(Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, toolId))); script.UseFile(path); if (Debug) @@ -102,14 +102,13 @@ public partial class ScriptClient return result; } - public async Task Tool_RunInstallScriptAsync(string installDirectory, Guid gameId, Tool tool) + public async Task Tool_RunInstallScriptAsync(string installDirectory, Guid toolId) { int result = default; + + var toolManifest = await ManifestHelper.ReadAsync(installDirectory, toolId); - var gameManifest = await ManifestHelper.ReadAsync(installDirectory, gameId); - var toolManifest = await ManifestHelper.ReadAsync(installDirectory, tool.Id); - - var path = ScriptHelper.GetScriptFilePath(installDirectory, tool.Id, Enums.ScriptType.Install); + var path = ScriptHelper.GetScriptFilePath(installDirectory, toolId, Enums.ScriptType.Install); try { @@ -120,7 +119,6 @@ public partial class ScriptClient var script = powerShellScriptFactory.Create(Enums.ScriptType.Install); script.AddVariable("InstallDirectory", installDirectory); - script.AddVariable("GameManifest", gameManifest); script.AddVariable("ToolManifest", toolManifest); script.AddVariable("DefaultInstallDirectory", settingsProvider.CurrentValue.Games.InstallDirectories.FirstOrDefault()); script.AddVariable("ServerAddress", connectionClient.GetServerAddress()); @@ -129,11 +127,8 @@ public partial class ScriptClient { op .Enrich("InstallDirectory", installDirectory) - .Enrich("GameManifestPath", ManifestHelper.GetPath(installDirectory, gameId)) - .Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, tool.Id)) + .Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, toolId)) .Enrich("ScriptPath", path) - .Enrich("GameTitle", gameManifest.Title) - .Enrich("GameId", gameManifest.Id) .Enrich("ToolName", toolManifest.Name) .Enrich("ToolId", toolManifest.Id); } @@ -142,15 +137,7 @@ public partial class ScriptClient logger?.LogError(ex, "Could not enrich logs"); } - if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any()) - { - foreach (var customField in gameManifest.CustomFields) - { - script.AddVariable(customField.Name, customField.Value); - } - } - - var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, tool.Id), "Files"); + var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, toolId), "Files"); script.UseWorkingDirectory(extractionPath); script.UseFile(path); @@ -175,41 +162,33 @@ public partial class ScriptClient return result; } - public async Task Tool_RunBeforeStartScriptAsync(string installDirectory, Guid gameId, Tool tool) + public async Task Tool_RunBeforeStartScriptAsync(string installDirectory, Guid toolId) { int result = default; try { - var gameManifest = await ManifestHelper.ReadAsync(installDirectory, gameId); - var toolManifest = await ManifestHelper.ReadAsync(installDirectory, tool.Id); + var toolManifest = await ManifestHelper.ReadAsync(installDirectory, toolId); - var path = ScriptHelper.GetScriptFilePath(installDirectory, tool.Id, Enums.ScriptType.BeforeStart); + var path = ScriptHelper.GetScriptFilePath(installDirectory, toolId, Enums.ScriptType.BeforeStart); using (var op = logger.BeginOperation("Executing before start script")) { if (File.Exists(path)) { var script = powerShellScriptFactory.Create(Enums.ScriptType.BeforeStart); - var playerAlias = await GameClient.GetPlayerAliasAsync(installDirectory, gameId); script.AddVariable("InstallDirectory", installDirectory); - script.AddVariable("GameManifest", gameManifest); script.AddVariable("ToolManifest", toolManifest); script.AddVariable("DefaultInstallDirectory", settingsProvider.CurrentValue.Games.InstallDirectories.FirstOrDefault()); script.AddVariable("ServerAddress", connectionClient.GetServerAddress()); - script.AddVariable("PlayerAlias", playerAlias); try { op .Enrich("InstallDirectory", installDirectory) - .Enrich("GameManifestPath", ManifestHelper.GetPath(installDirectory, gameId)) - .Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, tool.Id)) + .Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, toolId)) .Enrich("ScriptPath", path) - .Enrich("PlayerAlias", playerAlias) - .Enrich("GameTitle", gameManifest.Title) - .Enrich("GameId", gameManifest.Id) .Enrich("ToolName", toolManifest.Name) .Enrich("ToolId", toolManifest.Id); } @@ -218,15 +197,7 @@ public partial class ScriptClient logger?.LogError(ex, "Could not enrich logs"); } - if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any()) - { - foreach (var customField in gameManifest.CustomFields) - { - script.AddVariable(customField.Name, customField.Value); - } - } - - var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, tool.Id), "Files"); + var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, toolId), "Files"); script.UseWorkingDirectory(extractionPath); script.UseFile(path); @@ -255,16 +226,15 @@ public partial class ScriptClient return result; } - public async Task Tool_RunAfterStopScriptAsync(string installDirectory, Guid gameId, Tool tool) + public async Task Tool_RunAfterStopScriptAsync(string installDirectory, Guid toolId) { int result = default; try { - var gameManifest = await ManifestHelper.ReadAsync(installDirectory, gameId); - var toolManifest = await ManifestHelper.ReadAsync(installDirectory, tool.Id); + var toolManifest = await ManifestHelper.ReadAsync(installDirectory, toolId); - var path = ScriptHelper.GetScriptFilePath(installDirectory, tool.Id, Enums.ScriptType.AfterStop); + var path = ScriptHelper.GetScriptFilePath(installDirectory, toolId, Enums.ScriptType.AfterStop); using (var op = logger.BeginOperation("Executing after stop script")) { @@ -273,21 +243,16 @@ public partial class ScriptClient var script = powerShellScriptFactory.Create(Enums.ScriptType.AfterStop); script.AddVariable("InstallDirectory", installDirectory); - script.AddVariable("GameManifest", gameManifest); script.AddVariable("ToolManifest", toolManifest); script.AddVariable("DefaultInstallDirectory", settingsProvider.CurrentValue.Games.InstallDirectories.FirstOrDefault()); script.AddVariable("ServerAddress", connectionClient.GetServerAddress()); - script.AddVariable("PlayerAlias", await GameClient.GetPlayerAliasAsync(installDirectory, gameId)); try { op .Enrich("InstallDirectory", installDirectory) - .Enrich("GameManifestPath", ManifestHelper.GetPath(installDirectory, gameId)) - .Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, tool.Id)) + .Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, toolId)) .Enrich("ScriptPath", path) - .Enrich("GameTitle", gameManifest.Title) - .Enrich("GameId", gameManifest.Id) .Enrich("ToolName", toolManifest.Name) .Enrich("ToolId", toolManifest.Id); } @@ -296,15 +261,7 @@ public partial class ScriptClient logger?.LogError(ex, "Could not enrich logs"); } - if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any()) - { - foreach (var customField in gameManifest.CustomFields) - { - script.AddVariable(customField.Name, customField.Value); - } - } - - var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, tool.Id), "Files"); + var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, toolId), "Files"); script.UseWorkingDirectory(extractionPath); script.UseFile(path); diff --git a/LANCommander.SDK/Clients/ToolClient.cs b/LANCommander.SDK/Clients/ToolClient.cs index c97dc676..a7edec0b 100644 --- a/LANCommander.SDK/Clients/ToolClient.cs +++ b/LANCommander.SDK/Clients/ToolClient.cs @@ -8,7 +8,10 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; +using System.Threading; using System.Threading.Tasks; +using Force.Crc32; using LANCommander.SDK.Abstractions; using LANCommander.SDK.Exceptions; using LANCommander.SDK.Factories; @@ -16,7 +19,7 @@ using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { public class ToolClient( - ILogger _logger, + ILogger logger, ISettingsProvider settingsProvider, ApiRequestFactory apiRequestFactory, ScriptClient scriptClient, @@ -25,13 +28,26 @@ namespace LANCommander.SDK.Services public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e); public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress; - public delegate void OnArchiveExtractionProgressHandler(long position, long length); + public delegate void OnArchiveExtractionProgressHandler(long position, long length, Tool tool); public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress; public delegate void OnInstallProgressUpdateHandler(InstallProgress e); public event OnInstallProgressUpdateHandler OnInstallProgressUpdate; + + private TrackableStream _transferStream; + private IReader _reader; private InstallProgress _installProgress; + + public async Task GetAsync(Guid id) + { + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Tools/{id}") + .GetAsync(); + } public async Task GetManifestAsync(Guid id) { @@ -57,13 +73,15 @@ namespace LANCommander.SDK.Services { foreach (var tool in game.Tools) { - await InstallAsync(tool, game); + await InstallAsync(tool, game.InstallDirectory); } } - public async Task InstallAsync(Tool tool, Game game, int maxAttempts = 10) + public async Task InstallAsync(Tool tool, string installDirectory, int maxAttempts = 10) { string extractTempPath = null; + + var installResult = new InstallResult(); _installProgress = new InstallProgress(); @@ -78,73 +96,99 @@ namespace LANCommander.SDK.Services try { - _logger?.LogTrace("Saving manifest"); + logger?.LogTrace("Saving manifest"); var manifest = await GetManifestAsync(tool.Id); - await ManifestHelper.WriteAsync(manifest, game.InstallDirectory); + await ManifestHelper.WriteAsync(manifest, installDirectory); - _logger?.LogTrace("Saving scripts"); + logger?.LogTrace("Saving scripts"); foreach (var script in tool.Scripts) + await ScriptHelper.SaveScriptAsync(tool, script.Type, installDirectory); + + if (tool.Archives?.Any() ?? false) { - await ScriptHelper.SaveScriptAsync(game, tool, script.Type); + logger?.LogTrace("Archives for tool {ToolName} exist. Attempting to download...", tool.Name); + + var result = await RetryHelper.RetryOnExceptionAsync(maxAttempts, + TimeSpan.FromMilliseconds(500), new ExtractionResult(), + async () => + { + logger?.LogTrace("Attempting to download and extract tool"); + + return await Task.Run(async () => await DownloadAndExtractAsync(tool, installDirectory)); + }); + + if (!result.Success && !result.Canceled) + throw new InstallException("Could not extract the tool. Retry the install or check your connection"); + else if (result.Canceled) + throw new InstallCanceledException("Tool install canceled"); + + extractTempPath = result.Directory; + + logger?.LogTrace("Extraction of tool successful. Extracted path is {Path}", extractTempPath); + logger?.LogTrace("Running install script for tool {ToolName}", tool.Name); + + await RunPostInstallScripts(installDirectory, tool); } - - var installed = - await scriptClient.RunDetectInstallScriptAsync(game.InstallDirectory, game.Id, tool.Id); - - _logger?.LogTrace("Tool install detection returned {Result}", installed); - - if (!installed) + + if (tool.Archives?.Any() ?? false) { - _logger?.LogTrace("Tool {ToolName} not installed", tool.Name); + logger?.LogTrace("Archives for tool {ToolName} exist. Attempting to download...", tool.Name); + + var result = await RetryHelper.RetryOnExceptionAsync(maxAttempts, + TimeSpan.FromMilliseconds(500), new ExtractionResult(), + async () => + { + logger?.LogTrace("Attempting to download and extract tool"); + + return await Task.Run(async () => await DownloadAndExtractAsync(tool, installDirectory)); + }); - if (tool.Archives?.Any() ?? false) - { - _logger?.LogTrace("Archives for tool {ToolName} exist. Attempting to download...", tool.Name); + if (!result.Success && !result.Canceled) + throw new InstallException("Could not extract the tool. Retry the install or check your connection"); + else if (result.Canceled) + throw new InstallCanceledException("Tool install canceled"); - var result = await RetryHelper.RetryOnExceptionAsync(maxAttempts, - TimeSpan.FromMilliseconds(500), new ExtractionResult(), - async () => - { - _logger?.LogTrace("Attempting to download and extract tool"); + extractTempPath = result.Directory; - return await Task.Run(async () => await DownloadAndExtractAsync(tool, game)); - }); - - if (!result.Success && !result.Canceled) - throw new InstallException("Could not extract the tool. Retry the install or check your connection"); - else if (result.Canceled) - throw new InstallCanceledException("Tool install canceled"); + installResult.InstallDirectory = result.Directory; - extractTempPath = result.Directory; - - _logger?.LogTrace("Extraction of tool successful. Extracted path is {Path}", extractTempPath); - _logger?.LogTrace("Running install script for tool {ToolName}", tool.Name); + // TODO: Verification for tool files? + var toolFiles = result?.Files?.Where(x => !x.EntryPath.EndsWith("/")).Select(x => + new GameInstallationFileListEntry.FileEntry + { + EntryPath = x.EntryPath, + LocalPath = x.LocalPath, + }); + + logger?.LogTrace("Extraction of tool successful. Extracted path is {Path}", extractTempPath); + logger?.LogTrace("Running install script for tool {ToolName}", tool.Name); - await RunPostInstallScripts(game, tool); - } - else - { - _logger?.LogTrace("No archives exist for tool {ToolName}. Running install script anyway...", tool.Name); + await RunPostInstallScripts(installDirectory, tool); + } + else + { + logger?.LogTrace("No archives exist for tool {ToolName}. Running install script anyway...", tool.Name); - await RunPostInstallScripts(game, tool); - } + await RunPostInstallScripts(installDirectory, tool); } } catch (Exception ex) { - _logger?.LogError(ex, "Tool {Tool} failed to install", tool.Name); + logger?.LogError(ex, "Tool {Tool} failed to install", tool.Name); } finally { if (Directory.Exists(extractTempPath)) Directory.Delete(extractTempPath, true); } + + return installResult; } - private async Task RunPostInstallScripts(Game game, Tool tool) + private async Task RunPostInstallScripts(string installDirectory, Tool tool) { if (tool.Scripts != null && tool.Scripts.Any()) { @@ -154,104 +198,230 @@ namespace LANCommander.SDK.Services try { - await scriptClient.Redistributable_RunInstallScriptAsync(installDirectory, game.Id, tool.Id); - await scriptClient.Redistributable_RunNameChangeScriptAsync(game.InstallDirectory, game.Id, tool.Id, await profileClient.GetAliasAsync()); + await scriptClient.Tool_RunInstallScriptAsync(installDirectory, tool.Id); } catch (Exception ex) { - _logger?.LogError(ex, "Scripts failed to execute for tool {ToolName} ({GameId})", tool.Name, tool.Id); + logger?.LogError(ex, "Scripts failed to execute for tool {ToolName} ({GameId})", tool.Name, tool.Id); } } } - private async Task DownloadAndExtractAsync(Tool tool, Game game) + private async Task DownloadAndExtractAsync(Tool tool, string destination, CancellationToken cancellationToken = default) { if (tool == null) { - _logger?.LogTrace("Tool failed to download! No tool was specified"); + logger?.LogTrace("Tool failed to download! No tool was specified"); throw new ArgumentNullException(nameof(tool)); } - var destination = Path.Combine(GameClient.GetMetadataDirectoryPath(game.InstallDirectory, tool.Id), "Files"); - var files = new List(); + logger?.LogTrace("Downloading and extracting {Tool} to path {Destination}", tool.Name, destination); - _logger?.LogTrace("Downloading and extracting {Tool} to path {Destination}", tool.Name, destination); + var extractionResult = new ExtractionResult + { + Canceled = false, + }; + + if (!await CanStreamLatestArchiveAsync(tool.Id)) + { + extractionResult.Success = false; + extractionResult.Canceled = true; + + return extractionResult; + } + + var fileManifest = new StringBuilder(); + var files = new List(); try { Directory.CreateDirectory(destination); - using (var toolStream = await Stream(tool.Id)) - using (var reader = ReaderFactory.Open(toolStream)) - using (var monitor = new FileTransferMonitor(toolStream.Length)) + var stream = await StreamLatestArchiveAsync(tool.Id); + + _reader = ReaderFactory.Open(stream); + + using (var monitor = new FileTransferMonitor(stream.Length)) { - /*(toolStream.OnProgress += (pos, len) => + _reader.EntryExtractionProgress += (sender, e) => { + if (cancellationToken.IsCancellationRequested) + { + _reader.Cancel(); + + _installProgress.Status = InstallStatus.Canceled; + + OnInstallProgressUpdate?.Invoke(_installProgress); + + return; + } + if (monitor.CanUpdate()) { - monitor.Update(pos); + monitor.Update(stream.Position); _installProgress.BytesTransferred = monitor.GetBytesTransferred(); - _installProgress.TotalBytes = len; + _installProgress.TotalBytes = stream.Length; _installProgress.TransferSpeed = monitor.GetSpeed(); _installProgress.TimeRemaining = monitor.GetTimeRemaining(); - + OnInstallProgressUpdate?.Invoke(_installProgress); } - };*/ - - reader.EntryExtractionProgress += (sender, e) => - { - files.Add(new ExtractionResult.FileEntry - { - EntryPath = e.Item.Key, - LocalPath = Path.Combine(destination, e.Item.Key), - }); OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs { Entry = e.Item, Progress = e.ReaderProgress, + Game = null, }); }; - - reader.WriteAllToDirectory(destination, new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); } + + while (_reader.MoveToNextEntry()) + { + if (_reader.Cancelled) + break; + + try + { + var localFile = Path.Combine(destination, _reader.Entry.Key); + + uint crc = 0; + + if (File.Exists(localFile)) + { + using (FileStream fs = File.Open(localFile, FileMode.Open)) + { + var buffer = new byte[65536]; + + while (true) + { + var count = fs.Read(buffer, 0, buffer.Length); + + if (count == 0) + break; + + crc = Crc32Algorithm.Append(crc, buffer, 0, count); + } + } + } + + fileManifest.AppendLine($"{_reader.Entry.Key} | {_reader.Entry.Crc.ToString("X")}"); + files.Add(new ExtractionResult.FileEntry + { + EntryPath = _reader.Entry.Key, + LocalPath = localFile, + }); + + if (crc == 0 || crc != _reader.Entry.Crc) + _reader.WriteEntryToDirectory(destination, new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true, + PreserveFileTime = true + }); + else // Skip to next entry + try + { + _reader.OpenEntryStream().Dispose(); + } + catch + { + logger?.LogError("Could not skip to next entry in archive"); + } + } + catch (IOException ex) + { + var errorCode = ex.HResult & 0xFFFF; + + if (errorCode == 87) + throw ex; + else + logger?.LogTrace("Not replacing existing file/folder on disk: {Message}", ex.Message); + + // Skip to next entry + _reader.OpenEntryStream().Dispose(); + } + } + + _reader.Dispose(); + await stream.DisposeAsync(); } - catch (Exception ex) + catch (ReaderCancelledException ex) { - _logger?.LogError(ex, "Could not extract to path {Destination}", destination); + logger?.LogTrace(ex, "User cancelled the download"); + + extractionResult.Canceled = true; if (Directory.Exists(destination)) { - _logger?.LogTrace("Cleaning up orphaned files after bad install"); + logger?.LogTrace("Cleaning up ophaned files after cancelled install"); + + Directory.Delete(destination, true); + } + } + catch (Exception ex) + { + logger?.LogError(ex, "Could not extract to path {Destination}", destination); + + if (Directory.Exists(destination)) + { + logger?.LogTrace("Cleaning up orphaned install files after bad install"); Directory.Delete(destination, true); } - throw new Exception("The tool archive could not be extracted, is it corrupted? Please try again"); + throw new Exception("The game archive could not be extracted, is it corrupted? Please try again"); } - var extractionResult = new ExtractionResult - { - Canceled = false - }; - if (!extractionResult.Canceled) { extractionResult.Success = true; extractionResult.Directory = destination; extractionResult.Files = files; - _logger?.LogTrace("Tool {Tool} successfully downloaded and extracted to {Destination}", tool.Name, destination); + + var fileListDestination = Path.Combine(destination, ".lancommander", tool.Id.ToString(), "FileList.txt"); + + if (!Directory.Exists(Path.GetDirectoryName(fileListDestination))) + Directory.CreateDirectory(Path.GetDirectoryName(fileListDestination)); + + File.WriteAllText(fileListDestination, fileManifest.ToString()); + + logger?.LogTrace("Tool {Tool} successfully downloaded and extracted to {Destination}", tool.Name, destination); } return extractionResult; } + private async Task CanStreamLatestArchiveAsync(Guid id) + { + try + { + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Tools/{id}/Download") + .HeadAsync(); + + return true; + } + catch + { + return false; + } + } + + private async Task StreamLatestArchiveAsync(Guid id) + { + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Tools/{id}/Download") + .StreamAsync(); + } + public async Task ImportAsync(string archivePath) { using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read)) diff --git a/LANCommander.SDK/Helpers/ScriptHelper.cs b/LANCommander.SDK/Helpers/ScriptHelper.cs index 259e8461..8f1afb21 100644 --- a/LANCommander.SDK/Helpers/ScriptHelper.cs +++ b/LANCommander.SDK/Helpers/ScriptHelper.cs @@ -82,23 +82,23 @@ namespace LANCommander.SDK.Helpers } } - public static async Task SaveScriptAsync(Game game, Tool tool, ScriptType type) + public static async Task SaveScriptAsync(Tool tool, ScriptType type, string installDirectory) { var scriptContents = GetScriptContents(tool, type); if (!String.IsNullOrWhiteSpace(scriptContents)) { - var fileName = GetScriptFilePath(game.InstallDirectory, tool.Id, type); + var filename = GetScriptFilePath(installDirectory, tool.Id, type); - if (!Directory.Exists(Path.GetDirectoryName(fileName))) - Directory.CreateDirectory(Path.GetDirectoryName(fileName)); - - if (File.Exists(fileName)) - File.Delete(fileName); - - Logger?.LogTrace("Writing {ScriptType} script to {Destination}", type, fileName); + if (!Directory.Exists(Path.GetDirectoryName(filename))) + Directory.CreateDirectory(Path.GetDirectoryName(filename)); - await File.WriteAllTextAsync(fileName, scriptContents); + if (File.Exists(filename)) + File.Delete(filename); + + Logger?.LogTrace("Writing {ScriptType} script to {Destination}", type, filename); + + await File.WriteAllTextAsync(filename, scriptContents); } } diff --git a/LANCommander.SDK/Models/Manifest/Tool.cs b/LANCommander.SDK/Models/Manifest/Tool.cs index 22d72301..148a6e76 100644 --- a/LANCommander.SDK/Models/Manifest/Tool.cs +++ b/LANCommander.SDK/Models/Manifest/Tool.cs @@ -10,7 +10,9 @@ namespace LANCommander.SDK.Models.Manifest public string Description { get; set; } public string Notes { get; set; } public DateTime ReleasedOn { get; set; } + public virtual ICollection Actions { get; set; } = new List(); public virtual ICollection Archives { get; set; } = new List(); public virtual ICollection