First pass of adding tool installation to launcher

This commit is contained in:
Pat Hartl 2026-02-10 18:04:49 -06:00
parent 4474164e9b
commit 46bbf85093
16 changed files with 789 additions and 205 deletions

View file

@ -43,6 +43,7 @@ namespace LANCommander.Launcher.Data.Models
public virtual ICollection<Company>? Developers { get; set; } = new List<Company>();
public virtual ICollection<Platform>? Platforms { get; set; } = new List<Platform>();
public virtual ICollection<Redistributable>? Redistributables { get; set; } = new List<Redistributable>();
public virtual ICollection<Tool>? Tools { get; set; } = new List<Tool>();
public virtual ICollection<Media>? Media { get; set; } = new List<Media>();
public virtual ICollection<Collection> Collections { get; set; } = new List<Collection>();
public virtual ICollection<Game> DependentGames { get; set; } = new List<Game>();

View file

@ -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<Game>? Games { get; set; } = new List<Game>();
}
}

View file

@ -10,8 +10,6 @@ namespace LANCommander.Launcher.Models
public interface IInstallQueueItem
{
Guid Id { get; set; }
Guid[] AddonIds { get; set; }
Dictionary<Guid, string?> AddonVersions { get; set; }
string Title { get; set; }
string Version { get; set; }
string InstallDirectory { get; set; }

View file

@ -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<Guid, string?> 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;
}
}

View file

@ -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<MessageBusService>();
services.AddSingleton<AuthenticationService>();
services.AddSingleton<KeepAliveService>();
@ -58,6 +58,7 @@ namespace LANCommander.Launcher.Services.Extensions
services.AddScoped<PlaySessionService>();
services.AddScoped<ProfileService>();
services.AddScoped<RedistributableService>();
services.AddScoped<ToolService>();
services.AddScoped<SaveService>();
services.AddScoped<TagService>();
services.AddScoped<UpdateService>();

View file

@ -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<ImportContext> _logger;
public ImportContext(IServiceProvider serviceProvider)
{
_collections = serviceProvider.GetRequiredService<CollectionImporter>();
_developers = serviceProvider.GetRequiredService<DeveloperImporter>();
_engines = serviceProvider.GetRequiredService<EngineImporter>();
_games = serviceProvider.GetRequiredService<GameImporter>();
_genres = serviceProvider.GetRequiredService<GenreImporter>();
_media = serviceProvider.GetRequiredService<MediaImporter>();
_multiplayerModes = serviceProvider.GetRequiredService<MultiplayerModeImporter>();
_platforms = serviceProvider.GetRequiredService<PlatformImporter>();
_publishers = serviceProvider.GetRequiredService<PublisherImporter>();
_tags = serviceProvider.GetRequiredService<TagImporter>();
_logger = serviceProvider.GetRequiredService<ILogger<ImportContext>>();
SetupContextOnImporters();
}
private readonly TagImporter _tags;
private readonly ToolImporter _tools;
private readonly ILogger<ImportContext> _logger;
public ImportContext(IServiceProvider serviceProvider)
{
_collections = serviceProvider.GetRequiredService<CollectionImporter>();
_developers = serviceProvider.GetRequiredService<DeveloperImporter>();
_engines = serviceProvider.GetRequiredService<EngineImporter>();
_games = serviceProvider.GetRequiredService<GameImporter>();
_genres = serviceProvider.GetRequiredService<GenreImporter>();
_media = serviceProvider.GetRequiredService<MediaImporter>();
_multiplayerModes = serviceProvider.GetRequiredService<MultiplayerModeImporter>();
_platforms = serviceProvider.GetRequiredService<PlatformImporter>();
_publishers = serviceProvider.GetRequiredService<PublisherImporter>();
_tags = serviceProvider.GetRequiredService<TagImporter>();
_logger = serviceProvider.GetRequiredService<ILogger<ImportContext>>();
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<TRecord>(BaseManifest manifest, IEnumerable<TRecord> records, BaseImporter<TRecord> 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);

View file

@ -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<ToolImporter> logger) : BaseImporter<Tool>
{
public override async Task<ImportItemInfo<Tool>> 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<bool> 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<bool> AddAsync(ImportItemInfo<Tool> 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<bool> UpdateAsync(ImportItemInfo<Tool> 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<Tool>(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<bool> ExistsAsync(ImportItemInfo<Tool> importItemInfo) => await toolService.ExistsAsync(importItemInfo.Record.Id);
}

View file

@ -11,6 +11,7 @@ namespace LANCommander.Launcher.Services
ILogger<ImportService> 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();
}
}
}

View file

@ -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<InstallService> 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)
{

View file

@ -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<ToolService> logger,
DatabaseContext dbContext) : BaseDatabaseService<Tool>(dbContext, logger)
{
}
}

View file

@ -3,6 +3,7 @@
@namespace LANCommander.Launcher.UI
@inherits FeedbackComponent<Models.ListItem, string>
@inject InstallService InstallService
@inject ToolService ToolService
@inject LocalizationService LocalizationService
@inject GameClient GameClient
@inject ISettingsProvider SettingsProvider
@ -24,6 +25,13 @@
<CheckboxButtonGroup @bind-Selected="SelectedAddons" DataSource="Addons" KeySelector="a => a.Id" LabelSelector="a => a.Title" Direction="SpaceDirection.Vertical" />
}
@if (Tools.Any())
{
<Divider Text="@LocalizationService.GetString("Tools")" />
<CheckboxButtonGroup @bind-Selected="SelectedTools" DataSource="Tools" KeySelector="t => t.Id" LabelSelector="t => t.Name" Direction="SpaceDirection.Vertical" />
}
@if (SettingsProvider.CurrentValue.Games.InstallDirectories.Length > 1)
{
@ -65,7 +73,8 @@
List<SDK.Models.Game> Addons = new();
IEnumerable<SDK.Models.Game> SelectedAddons = new List<SDK.Models.Game>();
List<SDK.Models.Tool> Tools = new();
IEnumerable<SDK.Models.Tool> SelectedTools = new List<SDK.Models.Tool>();
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;

View file

@ -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<IEnumerable<Game>>();
}
public async Task<IEnumerable<Tool>> GetToolsAsync(Guid id)
{
return await apiRequestFactory
.Create()
.UseAuthenticationToken()
.UseVersioning()
.UseRoute($"/api/Games/{id}/Tools")
.GetAsync<IEnumerable<Tool>>();
}
public async Task<bool> CheckForUpdateAsync(Guid id, string currentVersion)
{
return await apiRequestFactory

View file

@ -15,14 +15,14 @@ namespace LANCommander.SDK.Services;
public partial class ScriptClient
{
public async Task<bool> Tool_RunDetectInstallScriptAsync(string installDirectory, Guid gameId, Tool tool)
public async Task<bool> Tool_RunDetectInstallScriptAsync(string installDirectory, Guid gameId, Guid toolId)
{
bool result = default;
var gameManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Game>(installDirectory, gameId);
var toolManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Tool>(installDirectory, tool.Id);
var toolManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Tool>(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<int> Tool_RunInstallScriptAsync(string installDirectory, Guid gameId, Tool tool)
public async Task<int> Tool_RunInstallScriptAsync(string installDirectory, Guid toolId)
{
int result = default;
var toolManifest = await ManifestHelper.ReadAsync<Tool>(installDirectory, toolId);
var gameManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Game>(installDirectory, gameId);
var toolManifest = await ManifestHelper.ReadAsync<Tool>(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<int> Tool_RunBeforeStartScriptAsync(string installDirectory, Guid gameId, Tool tool)
public async Task<int> Tool_RunBeforeStartScriptAsync(string installDirectory, Guid toolId)
{
int result = default;
try
{
var gameManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Game>(installDirectory, gameId);
var toolManifest = await ManifestHelper.ReadAsync<Tool>(installDirectory, tool.Id);
var toolManifest = await ManifestHelper.ReadAsync<Tool>(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<int> Tool_RunAfterStopScriptAsync(string installDirectory, Guid gameId, Tool tool)
public async Task<int> Tool_RunAfterStopScriptAsync(string installDirectory, Guid toolId)
{
int result = default;
try
{
var gameManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Game>(installDirectory, gameId);
var toolManifest = await ManifestHelper.ReadAsync<Tool>(installDirectory, tool.Id);
var toolManifest = await ManifestHelper.ReadAsync<Tool>(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);

View file

@ -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<ToolClient> _logger,
ILogger<ToolClient> 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<Tool> GetAsync(Guid id)
{
return await apiRequestFactory
.Create()
.UseAuthenticationToken()
.UseVersioning()
.UseRoute($"/api/Tools/{id}")
.GetAsync<Tool>();
}
public async Task<SDK.Models.Manifest.Tool> 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<InstallResult> 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<ExtractionResult> DownloadAndExtractAsync(Tool tool, Game game)
private async Task<ExtractionResult> 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<ExtractionResult.FileEntry>();
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<ExtractionResult.FileEntry>();
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<bool> CanStreamLatestArchiveAsync(Guid id)
{
try
{
await apiRequestFactory
.Create()
.UseAuthenticationToken()
.UseVersioning()
.UseRoute($"/api/Tools/{id}/Download")
.HeadAsync();
return true;
}
catch
{
return false;
}
}
private async Task<TrackableStream> 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))

View file

@ -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);
}
}

View file

@ -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<Action> Actions { get; set; } = new List<Action>();
public virtual ICollection<Archive> Archives { get; set; } = new List<Archive>();
public virtual ICollection<Script> Scripts { get; set; } = new List<Script>();
public virtual ICollection<Game> Games { get; set; } = new List<Game>();
}
}