2026-02-08 02:24:53 -06:00
|
|
|
|
using LANCommander.SDK.Enums;
|
|
|
|
|
|
using LANCommander.SDK.Helpers;
|
|
|
|
|
|
using LANCommander.SDK.Models;
|
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using SharpCompress.Common;
|
|
|
|
|
|
using SharpCompress.Readers;
|
|
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
2026-02-10 18:04:49 -06:00
|
|
|
|
using System.Text;
|
|
|
|
|
|
using System.Threading;
|
2026-02-08 02:24:53 -06:00
|
|
|
|
using System.Threading.Tasks;
|
2026-02-10 18:04:49 -06:00
|
|
|
|
using Force.Crc32;
|
2026-02-08 02:24:53 -06:00
|
|
|
|
using LANCommander.SDK.Abstractions;
|
|
|
|
|
|
using LANCommander.SDK.Exceptions;
|
|
|
|
|
|
using LANCommander.SDK.Factories;
|
2026-04-17 22:24:47 -05:00
|
|
|
|
using Action = System.Action;
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
|
|
|
|
|
namespace LANCommander.SDK.Services
|
|
|
|
|
|
{
|
|
|
|
|
|
public class ToolClient(
|
2026-02-10 18:04:49 -06:00
|
|
|
|
ILogger<ToolClient> logger,
|
2026-02-08 02:24:53 -06:00
|
|
|
|
ISettingsProvider settingsProvider,
|
|
|
|
|
|
ApiRequestFactory apiRequestFactory,
|
2026-03-11 19:36:24 -05:00
|
|
|
|
ScriptClient scriptClient)
|
2026-02-08 02:24:53 -06:00
|
|
|
|
{
|
|
|
|
|
|
public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e);
|
|
|
|
|
|
public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress;
|
|
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
public delegate void OnArchiveExtractionProgressHandler(long position, long length, Tool tool);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress;
|
|
|
|
|
|
|
|
|
|
|
|
public delegate void OnInstallProgressUpdateHandler(InstallProgress e);
|
|
|
|
|
|
public event OnInstallProgressUpdateHandler OnInstallProgressUpdate;
|
2026-03-11 19:36:24 -05:00
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
private IAsyncReader _reader;
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
|
|
|
|
|
private InstallProgress _installProgress;
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
|
|
|
|
|
public async Task<Tool> GetAsync(Guid id)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await apiRequestFactory
|
|
|
|
|
|
.Create()
|
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
|
.UseVersioning()
|
|
|
|
|
|
.UseRoute($"/api/Tools/{id}")
|
|
|
|
|
|
.GetAsync<Tool>();
|
|
|
|
|
|
}
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
|
|
|
|
|
public async Task<SDK.Models.Manifest.Tool> GetManifestAsync(Guid id)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await apiRequestFactory
|
|
|
|
|
|
.Create()
|
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
|
.UseVersioning()
|
2026-06-21 21:16:36 -05:00
|
|
|
|
.UseRoute($"/api/Tools/{id}/Manifest")
|
2026-02-08 02:24:53 -06:00
|
|
|
|
.GetAsync<SDK.Models.Manifest.Tool>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 19:36:24 -05:00
|
|
|
|
public async Task<IEnumerable<Script>> GetScriptsAsync(Guid id)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await apiRequestFactory
|
|
|
|
|
|
.Create()
|
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
|
.UseVersioning()
|
|
|
|
|
|
.UseRoute($"/api/Tool/{id}/Scripts")
|
|
|
|
|
|
.GetAsync<IEnumerable<Script>>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task WriteScriptsAsync(Tool tool, string installDirectory)
|
|
|
|
|
|
{
|
|
|
|
|
|
var scripts = await GetScriptsAsync(tool.Id);
|
|
|
|
|
|
|
|
|
|
|
|
if (scripts != null && scripts.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
logger?.LogTrace($"Saving scripts for tool {tool.Name} ({tool.Id}) into {installDirectory}");
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var script in scripts)
|
2026-03-11 20:12:09 -05:00
|
|
|
|
await ScriptHelper.SaveScriptAsync(tool, script, installDirectory);
|
2026-03-11 19:36:24 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 02:24:53 -06:00
|
|
|
|
public async Task<Stream> Stream(Guid id)
|
|
|
|
|
|
{
|
|
|
|
|
|
return await apiRequestFactory
|
|
|
|
|
|
.Create()
|
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
|
.UseVersioning()
|
|
|
|
|
|
.UseRoute($"/api/Tool/{id}/Download")
|
|
|
|
|
|
.StreamAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 22:24:47 -05:00
|
|
|
|
public delegate void OnTaskProgressHandler(InstallTaskProgress progress);
|
|
|
|
|
|
public event OnTaskProgressHandler OnTaskProgress;
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<InstallPlan> GenerateInstallPlanAsync(Tool tool, string installDirectory)
|
|
|
|
|
|
{
|
|
|
|
|
|
var plan = new InstallPlan();
|
|
|
|
|
|
|
|
|
|
|
|
var toolItem = new InstallPlanItem
|
|
|
|
|
|
{
|
|
|
|
|
|
EntityId = tool.Id,
|
|
|
|
|
|
Title = tool.Name,
|
|
|
|
|
|
Type = InstallPlanItemType.Tool,
|
|
|
|
|
|
InstallDirectory = installDirectory,
|
|
|
|
|
|
Order = 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
int taskOrder = 0;
|
|
|
|
|
|
|
|
|
|
|
|
toolItem.Tasks.Add(new InstallTaskDefinition
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = InstallTaskType.DownloadAndExtract,
|
|
|
|
|
|
Title = $"Download {tool.Name}",
|
|
|
|
|
|
Order = taskOrder++,
|
|
|
|
|
|
TargetId = tool.Id,
|
|
|
|
|
|
TargetName = tool.Name,
|
|
|
|
|
|
IsCritical = true,
|
|
|
|
|
|
ReportsProgress = true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-21 21:16:36 -05:00
|
|
|
|
toolItem.Tasks.Add(new InstallTaskDefinition
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = InstallTaskType.WriteManifest,
|
|
|
|
|
|
Title = "Write manifest",
|
|
|
|
|
|
Order = taskOrder++,
|
|
|
|
|
|
TargetId = tool.Id,
|
|
|
|
|
|
TargetName = tool.Name,
|
|
|
|
|
|
IsCritical = true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-17 22:24:47 -05:00
|
|
|
|
if (tool.Scripts != null && tool.Scripts.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
toolItem.Tasks.Add(new InstallTaskDefinition
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = InstallTaskType.RunInstallScript,
|
|
|
|
|
|
Title = "Run install script",
|
|
|
|
|
|
Order = taskOrder++,
|
|
|
|
|
|
TargetId = tool.Id,
|
|
|
|
|
|
TargetName = tool.Name,
|
|
|
|
|
|
IsCritical = false,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
plan.Items.Add(toolItem);
|
|
|
|
|
|
|
|
|
|
|
|
return plan;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<InstallResult> ExecuteInstallPlanItemAsync(InstallPlanItem planItem, CancellationToken cancellationToken = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
var tool = await GetAsync(planItem.EntityId);
|
|
|
|
|
|
var installResult = new InstallResult();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var taskDef in planItem.Tasks.OrderBy(t => t.Order))
|
|
|
|
|
|
{
|
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
|
|
var taskProgress = new InstallTaskProgress
|
|
|
|
|
|
{
|
|
|
|
|
|
QueueItemId = planItem.EntityId,
|
|
|
|
|
|
TaskId = taskDef.Id,
|
|
|
|
|
|
TaskType = taskDef.Type,
|
|
|
|
|
|
TaskTitle = taskDef.Title,
|
|
|
|
|
|
TaskStatus = InstallTaskStatus.Running,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
OnTaskProgress?.Invoke(taskProgress);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (taskDef.Type)
|
|
|
|
|
|
{
|
|
|
|
|
|
case InstallTaskType.DownloadAndExtract:
|
|
|
|
|
|
var result = await RetryHelper.RetryOnExceptionAsync(10,
|
|
|
|
|
|
TimeSpan.FromMilliseconds(500), new ExtractionResult(),
|
|
|
|
|
|
async () => await Task.Run(async () => await DownloadAndExtractAsync(tool, planItem.InstallDirectory, cancellationToken)));
|
|
|
|
|
|
|
|
|
|
|
|
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 was canceled");
|
|
|
|
|
|
|
|
|
|
|
|
installResult.InstallDirectory = result.Directory;
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2026-06-21 21:16:36 -05:00
|
|
|
|
case InstallTaskType.WriteManifest:
|
|
|
|
|
|
var manifest = await GetManifestAsync(tool.Id);
|
|
|
|
|
|
await ManifestHelper.WriteAsync(manifest, planItem.InstallDirectory);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2026-04-17 22:24:47 -05:00
|
|
|
|
case InstallTaskType.RunInstallScript:
|
|
|
|
|
|
await scriptClient.Tool_RunInstallScriptAsync(planItem.InstallDirectory, tool.Id);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
taskProgress.TaskStatus = InstallTaskStatus.Completed;
|
|
|
|
|
|
taskProgress.Progress = 1.0f;
|
|
|
|
|
|
OnTaskProgress?.Invoke(taskProgress);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (InstallCanceledException)
|
|
|
|
|
|
{
|
|
|
|
|
|
taskProgress.TaskStatus = InstallTaskStatus.Canceled;
|
|
|
|
|
|
OnTaskProgress?.Invoke(taskProgress);
|
|
|
|
|
|
throw;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex) when (!taskDef.IsCritical)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger?.LogError(ex, "Non-critical task {TaskTitle} failed for tool {ToolName}", taskDef.Title, tool.Name);
|
|
|
|
|
|
taskProgress.TaskStatus = InstallTaskStatus.Failed;
|
|
|
|
|
|
taskProgress.ErrorMessage = ex.Message;
|
|
|
|
|
|
OnTaskProgress?.Invoke(taskProgress);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return installResult;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 21:16:36 -05:00
|
|
|
|
public async Task UninstallAsync(string installDirectory, Guid toolId)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger?.LogTrace("Uninstalling tool {ToolId} from {InstallDirectory}", toolId, installDirectory);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await scriptClient.Tool_RunUninstallScriptAsync(installDirectory, toolId);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger?.LogWarning(ex, "Could not run uninstall script for tool {ToolId}", toolId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var fileListPath = GameClient.GetMetadataFilePath(installDirectory, toolId, "FileList.txt");
|
|
|
|
|
|
|
|
|
|
|
|
if (File.Exists(fileListPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
var fileList = await File.ReadAllLinesAsync(fileListPath);
|
|
|
|
|
|
var files = fileList.Select(l => l.Split('|').FirstOrDefault()?.Trim());
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var file in files.Where(f => !string.IsNullOrWhiteSpace(f) && !f.EndsWith("/")))
|
|
|
|
|
|
{
|
|
|
|
|
|
var localPath = Path.Combine(installDirectory, file);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (File.Exists(localPath))
|
|
|
|
|
|
File.Delete(localPath);
|
|
|
|
|
|
|
|
|
|
|
|
logger?.LogTrace("Deleted tool file {LocalPath}", localPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger?.LogWarning(ex, "Could not remove tool file {LocalPath}", localPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var metadataPath = GameClient.GetMetadataDirectoryPath(installDirectory, toolId);
|
|
|
|
|
|
|
|
|
|
|
|
if (Directory.Exists(metadataPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.Delete(metadataPath, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger?.LogWarning(ex, "Could not remove tool metadata directory {MetadataPath}", metadataPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 02:24:53 -06:00
|
|
|
|
public async Task InstallAsync(Game game)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var tool in game.Tools)
|
|
|
|
|
|
{
|
2026-02-10 18:04:49 -06:00
|
|
|
|
await InstallAsync(tool, game.InstallDirectory);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
public async Task<InstallResult> InstallAsync(Tool tool, string installDirectory, int maxAttempts = 10)
|
2026-02-08 02:24:53 -06:00
|
|
|
|
{
|
|
|
|
|
|
string extractTempPath = null;
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
|
|
|
|
|
var installResult = new InstallResult();
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-03-11 19:36:24 -05:00
|
|
|
|
_installProgress = new InstallProgress
|
|
|
|
|
|
{
|
|
|
|
|
|
Status = InstallStatus.Downloading,
|
|
|
|
|
|
Title = tool.Name,
|
|
|
|
|
|
Progress = 0,
|
|
|
|
|
|
TransferSpeed = 0,
|
|
|
|
|
|
TotalBytes = 0,
|
|
|
|
|
|
BytesTransferred = 0
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-08 02:24:53 -06:00
|
|
|
|
OnInstallProgressUpdate?.Invoke(_installProgress);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-02-10 18:04:49 -06:00
|
|
|
|
logger?.LogTrace("Saving manifest");
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
|
|
|
|
|
var manifest = await GetManifestAsync(tool.Id);
|
|
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
await ManifestHelper.WriteAsync(manifest, installDirectory);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
logger?.LogTrace("Saving scripts");
|
2026-03-11 19:36:24 -05:00
|
|
|
|
|
|
|
|
|
|
await WriteScriptsAsync(tool, installDirectory);
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
|
|
|
|
|
if (tool.Archives?.Any() ?? false)
|
2026-02-08 02:24:53 -06:00
|
|
|
|
{
|
2026-02-10 18:04:49 -06:00
|
|
|
|
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");
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
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");
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
extractTempPath = result.Directory;
|
|
|
|
|
|
|
|
|
|
|
|
logger?.LogTrace("Extraction of tool successful. Extracted path is {Path}", extractTempPath);
|
|
|
|
|
|
logger?.LogTrace("Running install script for tool {ToolName}", tool.Name);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
await RunPostInstallScripts(installDirectory, tool);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (tool.Archives?.Any() ?? false)
|
2026-02-08 02:24:53 -06:00
|
|
|
|
{
|
2026-02-10 18:04:49 -06:00
|
|
|
|
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));
|
|
|
|
|
|
});
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
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");
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
extractTempPath = result.Directory;
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
installResult.InstallDirectory = result.Directory;
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
// 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);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
await RunPostInstallScripts(installDirectory, tool);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
logger?.LogTrace("No archives exist for tool {ToolName}. Running install script anyway...", tool.Name);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
await RunPostInstallScripts(installDirectory, tool);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-02-10 18:04:49 -06:00
|
|
|
|
logger?.LogError(ex, "Tool {Tool} failed to install", tool.Name);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Directory.Exists(extractTempPath))
|
|
|
|
|
|
Directory.Delete(extractTempPath, true);
|
|
|
|
|
|
}
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
|
|
|
|
|
return installResult;
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
private async Task RunPostInstallScripts(string installDirectory, Tool tool)
|
2026-02-08 02:24:53 -06:00
|
|
|
|
{
|
|
|
|
|
|
if (tool.Scripts != null && tool.Scripts.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
//GameInstallProgress.Status = GameInstallStatus.RunningScripts;
|
|
|
|
|
|
|
|
|
|
|
|
// OnGameInstallProgressUpdate?.Invoke(GameInstallProgress);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-02-10 18:04:49 -06:00
|
|
|
|
await scriptClient.Tool_RunInstallScriptAsync(installDirectory, tool.Id);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-02-10 18:04:49 -06:00
|
|
|
|
logger?.LogError(ex, "Scripts failed to execute for tool {ToolName} ({GameId})", tool.Name, tool.Id);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
private async Task<ExtractionResult> DownloadAndExtractAsync(Tool tool, string destination, CancellationToken cancellationToken = default)
|
2026-02-08 02:24:53 -06:00
|
|
|
|
{
|
|
|
|
|
|
if (tool == null)
|
|
|
|
|
|
{
|
2026-02-10 18:04:49 -06:00
|
|
|
|
logger?.LogTrace("Tool failed to download! No tool was specified");
|
2026-02-08 02:24:53 -06:00
|
|
|
|
throw new ArgumentNullException(nameof(tool));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
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;
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
return extractionResult;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var fileManifest = new StringBuilder();
|
|
|
|
|
|
var files = new List<ExtractionResult.FileEntry>();
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.CreateDirectory(destination);
|
|
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
var stream = await StreamLatestArchiveAsync(tool.Id);
|
|
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
var monitor = new FileTransferMonitor(stream.Length);
|
|
|
|
|
|
var progress = new Progress<ProgressReport>(report =>
|
2026-02-08 02:24:53 -06:00
|
|
|
|
{
|
2026-03-09 22:12:01 -05:00
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
2026-02-08 02:24:53 -06:00
|
|
|
|
{
|
2026-03-09 22:12:01 -05:00
|
|
|
|
_reader?.Cancel();
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
_installProgress.Status = InstallStatus.Canceled;
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
OnInstallProgressUpdate?.Invoke(_installProgress);
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
if (monitor.CanUpdate())
|
|
|
|
|
|
{
|
|
|
|
|
|
monitor.Update(stream.Position);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
_installProgress.BytesTransferred = monitor.GetBytesTransferred();
|
|
|
|
|
|
_installProgress.TotalBytes = stream.Length;
|
|
|
|
|
|
_installProgress.TransferSpeed = monitor.GetSpeed();
|
|
|
|
|
|
_installProgress.TimeRemaining = monitor.GetTimeRemaining();
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
OnInstallProgressUpdate?.Invoke(_installProgress);
|
|
|
|
|
|
}
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs
|
|
|
|
|
|
{
|
|
|
|
|
|
Progress = report,
|
|
|
|
|
|
Game = null,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
_reader = await ReaderFactory.OpenAsyncReader(stream, new ReaderOptions { Progress = progress }, cancellationToken);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
while (await _reader.MoveToNextEntryAsync(cancellationToken))
|
2026-02-10 18:04:49 -06:00
|
|
|
|
{
|
|
|
|
|
|
if (_reader.Cancelled)
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
2026-02-08 02:24:53 -06:00
|
|
|
|
{
|
2026-02-10 18:04:49 -06:00
|
|
|
|
var localFile = Path.Combine(destination, _reader.Entry.Key);
|
|
|
|
|
|
|
|
|
|
|
|
uint crc = 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (File.Exists(localFile))
|
|
|
|
|
|
{
|
2026-03-11 19:36:24 -05:00
|
|
|
|
await using FileStream fs = File.Open(localFile, FileMode.Open);
|
|
|
|
|
|
var buffer = new byte[65536];
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
2026-03-11 19:36:24 -05:00
|
|
|
|
while (true)
|
|
|
|
|
|
{
|
|
|
|
|
|
var count = await fs.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
2026-03-11 19:36:24 -05:00
|
|
|
|
if (count == 0)
|
|
|
|
|
|
break;
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
2026-03-11 19:36:24 -05:00
|
|
|
|
crc = Crc32Algorithm.Append(crc, buffer, 0, count);
|
2026-02-10 18:04:49 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-03-09 22:12:01 -05:00
|
|
|
|
await _reader.WriteEntryToDirectoryAsync(destination, new ExtractionOptions()
|
2026-02-10 18:04:49 -06:00
|
|
|
|
{
|
|
|
|
|
|
ExtractFullPath = true,
|
|
|
|
|
|
Overwrite = true,
|
|
|
|
|
|
PreserveFileTime = true
|
2026-03-09 22:12:01 -05:00
|
|
|
|
}, cancellationToken);
|
2026-02-10 18:04:49 -06:00
|
|
|
|
else // Skip to next entry
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-03-09 22:12:01 -05:00
|
|
|
|
await using var es = await _reader.OpenEntryStreamAsync(cancellationToken);
|
2026-02-10 18:04:49 -06:00
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
2026-03-09 22:12:01 -05:00
|
|
|
|
logger?.LogError("Could not skip to next entry in archive: {EntryKey}", _reader.Entry.Key);
|
2026-02-10 18:04:49 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (IOException ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
var errorCode = ex.HResult & 0xFFFF;
|
|
|
|
|
|
|
|
|
|
|
|
if (errorCode == 87)
|
|
|
|
|
|
throw ex;
|
|
|
|
|
|
else
|
2026-03-09 22:12:01 -05:00
|
|
|
|
logger?.LogTrace("Not replacing existing file/folder on disk: {EntryKey} - {Message}", _reader.Entry.Key, ex.Message);
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
|
|
|
|
|
// Skip to next entry
|
2026-03-09 22:12:01 -05:00
|
|
|
|
await using var es = await _reader.OpenEntryStreamAsync(cancellationToken);
|
2026-02-10 18:04:49 -06:00
|
|
|
|
}
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
2026-03-09 22:12:01 -05:00
|
|
|
|
await _reader.DisposeAsync();
|
2026-02-10 18:04:49 -06:00
|
|
|
|
await stream.DisposeAsync();
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
2026-02-10 18:04:49 -06:00
|
|
|
|
catch (ReaderCancelledException ex)
|
2026-02-08 02:24:53 -06:00
|
|
|
|
{
|
2026-02-10 18:04:49 -06:00
|
|
|
|
logger?.LogTrace(ex, "User cancelled the download");
|
|
|
|
|
|
|
|
|
|
|
|
extractionResult.Canceled = true;
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
|
|
|
|
|
if (Directory.Exists(destination))
|
|
|
|
|
|
{
|
2026-03-11 19:36:24 -05:00
|
|
|
|
logger?.LogTrace("Cleaning up orphaned files after cancelled install");
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
|
|
|
|
|
Directory.Delete(destination, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-10 18:04:49 -06:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger?.LogError(ex, "Could not extract to path {Destination}", destination);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
if (Directory.Exists(destination))
|
|
|
|
|
|
{
|
|
|
|
|
|
logger?.LogTrace("Cleaning up orphaned install files after bad install");
|
|
|
|
|
|
|
|
|
|
|
|
Directory.Delete(destination, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw new Exception("The game archive could not be extracted, is it corrupted? Please try again");
|
|
|
|
|
|
}
|
2026-02-08 02:24:53 -06:00
|
|
|
|
|
|
|
|
|
|
if (!extractionResult.Canceled)
|
|
|
|
|
|
{
|
|
|
|
|
|
extractionResult.Success = true;
|
|
|
|
|
|
extractionResult.Directory = destination;
|
|
|
|
|
|
extractionResult.Files = files;
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
|
|
|
|
|
var fileListDestination = Path.Combine(destination, ".lancommander", tool.Id.ToString(), "FileList.txt");
|
|
|
|
|
|
|
|
|
|
|
|
if (!Directory.Exists(Path.GetDirectoryName(fileListDestination)))
|
|
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(fileListDestination));
|
|
|
|
|
|
|
2026-03-11 19:36:24 -05:00
|
|
|
|
await File.WriteAllTextAsync(fileListDestination, fileManifest.ToString(), cancellationToken);
|
2026-02-10 18:04:49 -06:00
|
|
|
|
|
|
|
|
|
|
logger?.LogTrace("Tool {Tool} successfully downloaded and extracted to {Destination}", tool.Name, destination);
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return extractionResult;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 18:04:49 -06:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 02:24:53 -06:00
|
|
|
|
public async Task ImportAsync(string archivePath)
|
|
|
|
|
|
{
|
2026-03-11 19:36:24 -05:00
|
|
|
|
await using var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read);
|
|
|
|
|
|
|
|
|
|
|
|
var objectKey = await apiRequestFactory
|
|
|
|
|
|
.Create()
|
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
|
.UseVersioning()
|
|
|
|
|
|
.UploadInChunksAsync(settingsProvider.CurrentValue.Archives.UploadChunkSize, fs);
|
|
|
|
|
|
|
|
|
|
|
|
if (objectKey != Guid.Empty)
|
|
|
|
|
|
await apiRequestFactory
|
2026-02-08 02:24:53 -06:00
|
|
|
|
.Create()
|
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
|
.UseVersioning()
|
2026-03-11 19:36:24 -05:00
|
|
|
|
.UseRoute($"/api/Tools/Import/{objectKey}")
|
|
|
|
|
|
.PostAsync();
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Obsolete("Exporter no longer provides \"full\" exports")]
|
|
|
|
|
|
public async Task ExportAsync(string destinationPath, Guid toolId)
|
|
|
|
|
|
{
|
|
|
|
|
|
await apiRequestFactory
|
|
|
|
|
|
.Create()
|
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
|
.UseVersioning()
|
|
|
|
|
|
.UseRoute($"/Tools/{toolId}/Export/Full")
|
|
|
|
|
|
.DownloadAsync(destinationPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task UploadArchiveAsync(string archivePath, Guid toolId, string version, string changelog = "")
|
|
|
|
|
|
{
|
2026-03-11 19:36:24 -05:00
|
|
|
|
await using var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read);
|
|
|
|
|
|
|
|
|
|
|
|
var objectKey = await apiRequestFactory
|
|
|
|
|
|
.Create()
|
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
|
.UseVersioning()
|
|
|
|
|
|
.UploadInChunksAsync(settingsProvider.CurrentValue.Archives.UploadChunkSize, fs);
|
|
|
|
|
|
|
|
|
|
|
|
if (objectKey != Guid.Empty)
|
|
|
|
|
|
await apiRequestFactory
|
2026-02-08 02:24:53 -06:00
|
|
|
|
.Create()
|
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
|
.UseVersioning()
|
2026-03-11 19:36:24 -05:00
|
|
|
|
.UseRoute("/api/Tools/UploadArchive")
|
|
|
|
|
|
.AddBody(new UploadArchiveRequest
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = toolId,
|
|
|
|
|
|
ObjectKey = objectKey,
|
|
|
|
|
|
Version = version,
|
|
|
|
|
|
Changelog = changelog,
|
|
|
|
|
|
})
|
|
|
|
|
|
.PostAsync();
|
2026-02-08 02:24:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|