Some checks failed
LANCommander SDK Release / prep (push) Failing after 14s
LANCommander SDK Release / publish (push) Has been skipped
LANCommander Release / prep (push) Failing after 29s
LANCommander Release / build_server_linux_arm64 (push) Has been skipped
LANCommander Release / build_server_linux_x64 (push) Has been skipped
LANCommander Release / build_server_osx_arm64 (push) Has been skipped
LANCommander Release / build_server_osx_x64 (push) Has been skipped
LANCommander Release / build_server_win_arm64 (push) Has been skipped
LANCommander Release / build_server_win_x64 (push) Has been skipped
LANCommander Release / build_launcher_linux_arm64 (push) Has been skipped
LANCommander Release / build_launcher_linux_x64 (push) Has been skipped
LANCommander Release / build_launcher_osx_arm64 (push) Has been skipped
LANCommander Release / build_launcher_osx_x64 (push) Has been skipped
LANCommander Release / build_launcher_win_arm64 (push) Has been skipped
LANCommander Release / build_launcher_win_x64 (push) Has been skipped
LANCommander Release / build_release (push) Has been skipped
334 lines
14 KiB
C#
334 lines
14 KiB
C#
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;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using LANCommander.SDK.Abstractions;
|
|
using LANCommander.SDK.Exceptions;
|
|
using LANCommander.SDK.Factories;
|
|
|
|
namespace LANCommander.SDK.Services
|
|
{
|
|
public class RedistributableClient(
|
|
ILogger<RedistributableClient> logger,
|
|
ISettingsProvider settingsProvider,
|
|
ApiRequestFactory apiRequestFactory,
|
|
ScriptClient scriptClient,
|
|
ProfileClient profileClient)
|
|
{
|
|
public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e);
|
|
public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress;
|
|
|
|
public delegate void OnArchiveExtractionProgressHandler(long position, long length);
|
|
public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress;
|
|
|
|
public delegate void OnInstallProgressUpdateHandler(InstallProgress e);
|
|
public event OnInstallProgressUpdateHandler OnInstallProgressUpdate;
|
|
|
|
private InstallProgress _installProgress;
|
|
|
|
public async Task<SDK.Models.Manifest.Redistributable> GetManifestAsync(Guid id)
|
|
{
|
|
return await apiRequestFactory
|
|
.Create()
|
|
.UseAuthenticationToken()
|
|
.UseVersioning()
|
|
.UseRoute($"/api/Redistributables/{id}")
|
|
.GetAsync<SDK.Models.Manifest.Redistributable>();
|
|
}
|
|
|
|
public async Task<IEnumerable<Script>> GetScriptsAsync(Guid id)
|
|
{
|
|
return await apiRequestFactory
|
|
.Create()
|
|
.UseAuthenticationToken()
|
|
.UseVersioning()
|
|
.UseRoute($"/api/Redistributables/{id}/Scripts")
|
|
.GetAsync<IEnumerable<Script>>();
|
|
}
|
|
|
|
public async Task WriteScriptsAsync(Game game, Redistributable redistributable)
|
|
{
|
|
var scripts = await GetScriptsAsync(redistributable.Id);
|
|
|
|
if (scripts != null && scripts.Any())
|
|
{
|
|
logger?.LogTrace($"Saving scripts for redistributable {redistributable.Name} ({redistributable.Id}) into {game.InstallDirectory}");
|
|
|
|
foreach (var script in scripts)
|
|
await ScriptHelper.SaveScriptAsync(game, redistributable, script);
|
|
}
|
|
}
|
|
|
|
public async Task<Stream> Stream(Guid id)
|
|
{
|
|
return await apiRequestFactory
|
|
.Create()
|
|
.UseAuthenticationToken()
|
|
.UseVersioning()
|
|
.UseRoute($"/api/Redistributables/{id}/Download")
|
|
.StreamAsync();
|
|
}
|
|
|
|
public async Task InstallAsync(Game game)
|
|
{
|
|
foreach (var redistributable in game.Redistributables)
|
|
{
|
|
await InstallAsync(redistributable, game);
|
|
}
|
|
}
|
|
|
|
public async Task InstallAsync(Redistributable redistributable, Game game, int maxAttempts = 10)
|
|
{
|
|
string extractTempPath = null;
|
|
|
|
_installProgress = new InstallProgress();
|
|
|
|
_installProgress.Status = InstallStatus.Downloading;
|
|
_installProgress.Title = redistributable.Name;
|
|
_installProgress.Progress = 0;
|
|
_installProgress.TransferSpeed = 0;
|
|
_installProgress.TotalBytes = 0;
|
|
_installProgress.BytesTransferred = 0;
|
|
|
|
OnInstallProgressUpdate?.Invoke(_installProgress);
|
|
|
|
try
|
|
{
|
|
logger?.LogTrace("Saving manifest");
|
|
|
|
var manifest = await GetManifestAsync(redistributable.Id);
|
|
|
|
await ManifestHelper.WriteAsync(manifest, game.InstallDirectory);
|
|
|
|
logger?.LogTrace("Saving scripts");
|
|
|
|
await WriteScriptsAsync(game, redistributable);
|
|
|
|
var installed =
|
|
await scriptClient.Redistributable_RunDetectInstallScriptAsync(game.InstallDirectory, game.Id, redistributable.Id);
|
|
|
|
logger?.LogTrace("Redistributable install detection returned {Result}", installed);
|
|
|
|
if (!installed)
|
|
{
|
|
logger?.LogTrace("Redistributable {RedistributableName} not installed", redistributable.Name);
|
|
|
|
if (redistributable.Archives?.Any() ?? false)
|
|
{
|
|
logger?.LogTrace("Archives for redistributable {RedistributableName} exist. Attempting to download...", redistributable.Name);
|
|
|
|
var result = await RetryHelper.RetryOnExceptionAsync(maxAttempts,
|
|
TimeSpan.FromMilliseconds(500), new ExtractionResult(),
|
|
async () =>
|
|
{
|
|
logger?.LogTrace("Attempting to download and extract redistributable");
|
|
|
|
return await DownloadAndExtractAsync(redistributable, game, CancellationToken.None);
|
|
});
|
|
|
|
if (!result.Success && !result.Canceled)
|
|
throw new InstallException("Could not extract the redistributable. Retry the install or check your connection");
|
|
else if (result.Canceled)
|
|
throw new InstallCanceledException("Redistributable install canceled");
|
|
|
|
extractTempPath = result.Directory;
|
|
|
|
logger?.LogTrace("Extraction of redistributable successful. Extracted path is {Path}", extractTempPath);
|
|
logger?.LogTrace("Running install script for redistributable {RedistributableName}", redistributable.Name);
|
|
|
|
await RunPostInstallScripts(game, redistributable);
|
|
}
|
|
else
|
|
{
|
|
logger?.LogTrace("No archives exist for redistributable {RedistributableName}. Running install script anyway...", redistributable.Name);
|
|
|
|
await RunPostInstallScripts(game, redistributable);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger?.LogError(ex, "Redistributable {Redistributable} failed to install", redistributable.Name);
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(extractTempPath))
|
|
Directory.Delete(extractTempPath, true);
|
|
}
|
|
}
|
|
|
|
private async Task RunPostInstallScripts(Game game, Redistributable redistributable)
|
|
{
|
|
if (redistributable.Scripts != null && redistributable.Scripts.Any())
|
|
{
|
|
//GameInstallProgress.Status = GameInstallStatus.RunningScripts;
|
|
|
|
// OnGameInstallProgressUpdate?.Invoke(GameInstallProgress);
|
|
|
|
try
|
|
{
|
|
await scriptClient.Redistributable_RunInstallScriptAsync(game.InstallDirectory, game.Id, redistributable.Id);
|
|
await scriptClient.Redistributable_RunNameChangeScriptAsync(game.InstallDirectory, game.Id, redistributable.Id, await profileClient.GetAliasAsync());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger?.LogError(ex, "Scripts failed to execute for redistributable {RedistributableName} ({GameId})", redistributable.Name, redistributable.Id);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task<ExtractionResult> DownloadAndExtractAsync(Redistributable redistributable, Game game, CancellationToken cancellationToken = default)
|
|
{
|
|
if (redistributable == null)
|
|
{
|
|
logger?.LogTrace("Redistributable failed to download! No redistributable was specified");
|
|
throw new ArgumentNullException(nameof(redistributable));
|
|
}
|
|
|
|
var destination = Path.Combine(GameClient.GetMetadataDirectoryPath(game.InstallDirectory, redistributable.Id), "Files");
|
|
var files = new List<ExtractionResult.FileEntry>();
|
|
|
|
logger?.LogTrace("Downloading and extracting {Redistributable} to path {Destination}", redistributable.Name, destination);
|
|
|
|
try
|
|
{
|
|
Directory.CreateDirectory(destination);
|
|
|
|
using (var redistributableStream = await Stream(redistributable.Id))
|
|
{
|
|
var monitor = new FileTransferMonitor(redistributableStream.Length);
|
|
var seenEntries = new System.Collections.Generic.HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var progress = new Progress<ProgressReport>(report =>
|
|
{
|
|
if (!string.IsNullOrEmpty(report.EntryPath) && seenEntries.Add(report.EntryPath))
|
|
{
|
|
files.Add(new ExtractionResult.FileEntry
|
|
{
|
|
EntryPath = report.EntryPath,
|
|
LocalPath = Path.Combine(destination, report.EntryPath),
|
|
});
|
|
}
|
|
|
|
if (monitor.CanUpdate())
|
|
{
|
|
monitor.Update(redistributableStream.Position);
|
|
|
|
_installProgress.BytesTransferred = monitor.GetBytesTransferred();
|
|
_installProgress.TotalBytes = redistributableStream.Length;
|
|
_installProgress.TransferSpeed = monitor.GetSpeed();
|
|
_installProgress.TimeRemaining = monitor.GetTimeRemaining();
|
|
|
|
OnInstallProgressUpdate?.Invoke(_installProgress);
|
|
}
|
|
|
|
OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs
|
|
{
|
|
Progress = report,
|
|
});
|
|
});
|
|
|
|
await using var reader = await ReaderFactory.OpenAsyncReader(redistributableStream, new ReaderOptions { Progress = progress }, cancellationToken);
|
|
await reader.WriteAllToDirectoryAsync(destination, new ExtractionOptions()
|
|
{
|
|
ExtractFullPath = true,
|
|
Overwrite = true
|
|
}, cancellationToken);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger?.LogError(ex, "Could not extract to path {Destination}", destination);
|
|
|
|
if (Directory.Exists(destination))
|
|
{
|
|
logger?.LogTrace("Cleaning up orphaned files after bad install");
|
|
|
|
Directory.Delete(destination, true);
|
|
}
|
|
|
|
throw new Exception("The redistributable 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("Redistributable {Redistributable} successfully downloaded and extracted to {Destination}", redistributable.Name, destination);
|
|
}
|
|
|
|
return extractionResult;
|
|
}
|
|
|
|
public async Task ImportAsync(string archivePath)
|
|
{
|
|
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
|
|
.Create()
|
|
.UseAuthenticationToken()
|
|
.UseVersioning()
|
|
.UseRoute($"/api/Redistributables/Import/{objectKey}")
|
|
.PostAsync();
|
|
}
|
|
}
|
|
|
|
[Obsolete("Exporter no longer provides \"full\" exports")]
|
|
public async Task ExportAsync(string destinationPath, Guid redistributableId)
|
|
{
|
|
await apiRequestFactory
|
|
.Create()
|
|
.UseAuthenticationToken()
|
|
.UseVersioning()
|
|
.UseRoute($"/Redistributables/{redistributableId}/Export/Full")
|
|
.DownloadAsync(destinationPath);
|
|
}
|
|
|
|
public async Task UploadArchiveAsync(string archivePath, Guid redistributableId, string version, string changelog = "")
|
|
{
|
|
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
|
|
.Create()
|
|
.UseAuthenticationToken()
|
|
.UseVersioning()
|
|
.UseRoute("/api/Redistributables/UploadArchive")
|
|
.AddBody(new UploadArchiveRequest
|
|
{
|
|
Id = redistributableId,
|
|
ObjectKey = objectKey,
|
|
Version = version,
|
|
Changelog = changelog,
|
|
})
|
|
.PostAsync();
|
|
}
|
|
}
|
|
}
|
|
}
|