Merge pull request 'master' (#14) from CWX/SPT-AKI-Installer:master into master
Reviewed-on: waffle.lord/SPT-AKI-Installer#14
This commit is contained in:
commit
84acb45cb7
@ -26,23 +26,32 @@ public static class DownloadCacheHelper
|
||||
return DirectorySizeHelper.SizeSuffix(cacheSize);
|
||||
}
|
||||
|
||||
private static bool CheckCache(FileInfo cacheFile, string expectedHash = null)
|
||||
/// <summary>
|
||||
/// Check if a file in the cache already exists
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to check for</param>
|
||||
/// <param name="expectedHash">The expected hash of the file in the cache</param>
|
||||
/// <param name="cachedFile">The file found in the cache; null if no file is found</param>
|
||||
/// <returns>True if the file is in the cache and its hash matches the expected hash, otherwise false</returns>
|
||||
public static bool CheckCache(string fileName, string expectedHash, out FileInfo cachedFile)
|
||||
=> CheckCache(new FileInfo(Path.Join(CachePath, fileName)), expectedHash, out cachedFile);
|
||||
|
||||
private static bool CheckCache(FileInfo cacheFile, string expectedHash, out FileInfo fileInCache)
|
||||
{
|
||||
fileInCache = cacheFile;
|
||||
|
||||
try
|
||||
{
|
||||
cacheFile.Refresh();
|
||||
Directory.CreateDirectory(CachePath);
|
||||
|
||||
if (cacheFile.Exists)
|
||||
{
|
||||
if (expectedHash != null && FileHashHelper.CheckHash(cacheFile, expectedHash))
|
||||
{
|
||||
Log.Information($"Using cached file: {cacheFile.Name} - Hash: {expectedHash}");
|
||||
return true;
|
||||
}
|
||||
if (!cacheFile.Exists || expectedHash == null)
|
||||
return false;
|
||||
|
||||
cacheFile.Delete();
|
||||
cacheFile.Refresh();
|
||||
if (FileHashHelper.CheckHash(cacheFile, expectedHash))
|
||||
{
|
||||
fileInCache = cacheFile;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -53,10 +62,23 @@ public static class DownloadCacheHelper
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Result> DownloadFile(FileInfo outputFile, string targetLink, IProgress<double> progress, string expectedHash = null)
|
||||
/// <summary>
|
||||
/// Download a file to the cache folder
|
||||
/// </summary>
|
||||
/// <param name="outputFileName">The file name to save the file as</param>
|
||||
/// <param name="targetLink">The url to download the file from</param>
|
||||
/// <param name="progress">A provider for progress updates</param>
|
||||
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
|
||||
/// <remarks>If the file exists, it is deleted before downloading</remarks>
|
||||
public static async Task<FileInfo?> DownloadFileAsync(string outputFileName, string targetLink, IProgress<double> progress)
|
||||
{
|
||||
var outputFile = new FileInfo(Path.Join(CachePath, outputFileName));
|
||||
|
||||
try
|
||||
{
|
||||
if (outputFile.Exists)
|
||||
outputFile.Delete();
|
||||
|
||||
// Use the provided extension method
|
||||
using (var file = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
await _httpClient.DownloadDataAsync(targetLink, file, progress);
|
||||
@ -65,90 +87,100 @@ public static class DownloadCacheHelper
|
||||
|
||||
if (!outputFile.Exists)
|
||||
{
|
||||
return Result.FromError($"Failed to download {outputFile.Name}");
|
||||
Log.Error("Failed to download file from url: {name} :: {url}", outputFileName, targetLink);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expectedHash != null && !FileHashHelper.CheckHash(outputFile, expectedHash))
|
||||
{
|
||||
return Result.FromError("Hash mismatch");
|
||||
}
|
||||
|
||||
return Result.FromSuccess();
|
||||
return outputFile;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.FromError(ex.Message);
|
||||
Log.Error(ex, "Failed to download file from url: {name} :: {url}", outputFileName, targetLink);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Result> ProcessInboundStreamAsync(FileInfo cacheFile, Stream downloadStream, string expectedHash = null)
|
||||
/// <summary>
|
||||
/// Download a file to the cache folder
|
||||
/// </summary>
|
||||
/// <param name="outputFileName">The file name to save the file as</param>
|
||||
/// <param name="downloadStream">The stream the download the file from</param>
|
||||
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
|
||||
/// <remarks>If the file exists, it is deleted before downloading</remarks>
|
||||
public static async Task<FileInfo?> DownloadFileAsync(string outputFileName, Stream downloadStream)
|
||||
{
|
||||
var outputFile = new FileInfo(Path.Join(CachePath, outputFileName));
|
||||
|
||||
try
|
||||
{
|
||||
if (CheckCache(cacheFile, expectedHash)) return Result.FromSuccess();
|
||||
if (outputFile.Exists)
|
||||
outputFile.Delete();
|
||||
|
||||
using var patcherFileStream = cacheFile.Open(FileMode.Create);
|
||||
using var patcherFileStream = outputFile.Open(FileMode.Create);
|
||||
{
|
||||
await downloadStream.CopyToAsync(patcherFileStream);
|
||||
}
|
||||
|
||||
patcherFileStream.Close();
|
||||
|
||||
if (expectedHash != null && !FileHashHelper.CheckHash(cacheFile, expectedHash))
|
||||
if (!outputFile.Exists)
|
||||
{
|
||||
return Result.FromError("Hash mismatch");
|
||||
Log.Error("Failed to download file from stream: {name}", outputFileName);
|
||||
return null;
|
||||
}
|
||||
|
||||
return Result.FromSuccess();
|
||||
return outputFile;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return Result.FromError(ex.Message);
|
||||
Log.Error(ex, "Failed to download file from stream: {fileName}", outputFileName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Result> ProcessInboundFileAsync(FileInfo cacheFile, string targetLink, IProgress<double> progress, string expectedHash = null)
|
||||
/// <summary>
|
||||
/// Get the file from cache or download it
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to check for in the cache</param>
|
||||
/// <param name="targetLink">The url to download from if the file doesn't exist in the cache</param>
|
||||
/// <param name="progress">A provider for progress updates</param>
|
||||
/// <param name="expectedHash">The expected hash of the cached file</param>
|
||||
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
|
||||
/// <remarks>Use <see cref="DownloadFileAsync(string, string, IProgress{double})"/> if you don't have an expected cache file hash</remarks>
|
||||
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, string targetLink, IProgress<double> progress, string expectedHash)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (CheckCache(cacheFile, expectedHash)) return Result.FromSuccess();
|
||||
if (CheckCache(fileName, expectedHash, out var cacheFile))
|
||||
return cacheFile;
|
||||
|
||||
return await DownloadFile(cacheFile, targetLink, progress, expectedHash);
|
||||
return await DownloadFileAsync(fileName, targetLink, progress);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return Result.FromError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, string targetLink, IProgress<double> progress, string expectedHash = null)
|
||||
{
|
||||
var cacheFile = new FileInfo(Path.Join(CachePath, fileName));
|
||||
|
||||
try
|
||||
{
|
||||
var result = await ProcessInboundFileAsync(cacheFile, targetLink, progress, expectedHash);
|
||||
|
||||
return result.Succeeded ? cacheFile : null;
|
||||
}
|
||||
catch(Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Error while getting file: {fileName}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream, string expectedHash = null)
|
||||
/// <summary>
|
||||
/// Get the file from cache or download it
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to check for in the cache</param>
|
||||
/// <param name="fileDownloadStream">The stream to download from if the file doesn't exist in the cache</param>
|
||||
/// <param name="expectedHash">The expected hash of the cached file</param>
|
||||
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
|
||||
/// <remarks>Use <see cref="DownloadFileAsync(string, Stream)"/> if you don't have an expected cache file hash</remarks>
|
||||
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream, string expectedHash)
|
||||
{
|
||||
var cacheFile = new FileInfo(Path.Join(CachePath, fileName));
|
||||
|
||||
try
|
||||
{
|
||||
var result = await ProcessInboundStreamAsync(cacheFile, fileDownloadStream, expectedHash);
|
||||
if (CheckCache(fileName, expectedHash, out var cacheFile))
|
||||
return cacheFile;
|
||||
|
||||
return result.Succeeded ? cacheFile : null;
|
||||
return await DownloadFileAsync(fileName, fileDownloadStream);
|
||||
}
|
||||
catch(Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Error while getting file: {fileName}");
|
||||
return null;
|
||||
|
@ -1,16 +1,20 @@
|
||||
using CG.Web.MegaApiClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
using SPTInstaller.Interfaces;
|
||||
using SPTInstaller.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using SPTInstaller.Helpers;
|
||||
using SPTInstaller.Models.Mirrors;
|
||||
using SPTInstaller.Models.Mirrors.Downloaders;
|
||||
using Serilog;
|
||||
|
||||
namespace SPTInstaller.Installer_Tasks;
|
||||
|
||||
public class DownloadTask : InstallerTaskBase
|
||||
{
|
||||
private InternalData _data;
|
||||
private List<IMirrorDownloader> _mirrors = new List<IMirrorDownloader>();
|
||||
private string _expectedPatcherHash = "";
|
||||
|
||||
public DownloadTask(InternalData data) : base("Download Files")
|
||||
{
|
||||
@ -19,9 +23,9 @@ public class DownloadTask : InstallerTaskBase
|
||||
|
||||
private async Task<IResult> BuildMirrorList()
|
||||
{
|
||||
var progress = new Progress<double>((d) => { SetStatus("Downloading Mirror List", "", (int)Math.Floor(d));});
|
||||
var progress = new Progress<double>((d) => { SetStatus("Downloading Mirror List", "", (int)Math.Floor(d), ProgressStyle.Shown);});
|
||||
|
||||
var file = await DownloadCacheHelper.GetOrDownloadFileAsync("mirrors.json", _data.PatcherMirrorsLink, progress);
|
||||
var file = await DownloadCacheHelper.DownloadFileAsync("mirrors.json", _data.PatcherMirrorsLink, progress);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
@ -30,52 +34,43 @@ public class DownloadTask : InstallerTaskBase
|
||||
|
||||
var mirrorsList = JsonConvert.DeserializeObject<List<DownloadMirror>>(File.ReadAllText(file.FullName));
|
||||
|
||||
if (mirrorsList is List<DownloadMirror> mirrors)
|
||||
{
|
||||
_data.PatcherReleaseMirrors = mirrors;
|
||||
if (mirrorsList == null)
|
||||
return Result.FromError("Failed to deserialize mirrors list");
|
||||
|
||||
return Result.FromSuccess();
|
||||
foreach (var mirror in mirrorsList)
|
||||
{
|
||||
_expectedPatcherHash = mirror.Hash;
|
||||
|
||||
switch (mirror.Link)
|
||||
{
|
||||
case string l when l.StartsWith("https://mega"):
|
||||
_mirrors.Add(new MegaMirrorDownloader(mirror));
|
||||
break;
|
||||
default:
|
||||
_mirrors.Add(new HttpMirrorDownloader(mirror));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Result.FromError("Failed to deserialize mirrors list");
|
||||
return Result.FromSuccess("Mirrors list ready");
|
||||
}
|
||||
|
||||
private async Task<IResult> DownloadPatcherFromMirrors(IProgress<double> progress)
|
||||
{
|
||||
foreach (var mirror in _data.PatcherReleaseMirrors)
|
||||
SetStatus("Downloading Patcher", "Verifying cached patcher ...", progressStyle: ProgressStyle.Indeterminate);
|
||||
|
||||
if (DownloadCacheHelper.CheckCache("patcher.zip", _expectedPatcherHash, out var cacheFile))
|
||||
{
|
||||
SetStatus($"Downloading Patcher", mirror.Link);
|
||||
_data.PatcherZipInfo = cacheFile;
|
||||
Log.Information("Using cached file {fileName} - Hash: {hash}", _data.PatcherZipInfo.Name, _expectedPatcherHash);
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
// mega is a little weird since they use encryption, but thankfully there is a great library for their api :)
|
||||
if (mirror.Link.StartsWith("https://mega"))
|
||||
{
|
||||
var megaClient = new MegaApiClient();
|
||||
await megaClient.LoginAnonymousAsync();
|
||||
foreach (var mirror in _mirrors)
|
||||
{
|
||||
SetStatus("Downloading Patcher", mirror.MirrorInfo.Link, progressStyle: ProgressStyle.Indeterminate);
|
||||
|
||||
// if mega fails to connect, try the next mirror
|
||||
if (!megaClient.IsLoggedIn) continue;
|
||||
|
||||
try
|
||||
{
|
||||
using var megaDownloadStream = await megaClient.DownloadAsync(new Uri(mirror.Link), progress);
|
||||
|
||||
_data.PatcherZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("patcher.zip", megaDownloadStream, mirror.Hash);
|
||||
|
||||
if(_data.PatcherZipInfo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
catch
|
||||
{
|
||||
//most likely a 509 (Bandwidth limit exceeded) due to mega's user quotas.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_data.PatcherZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("patcher.zip", mirror.Link, progress, mirror.Hash);
|
||||
_data.PatcherZipInfo = await mirror.Download(progress);
|
||||
|
||||
if (_data.PatcherZipInfo != null)
|
||||
{
|
||||
|
9
SPTInstaller/Interfaces/IMirrorDownloader.cs
Normal file
9
SPTInstaller/Interfaces/IMirrorDownloader.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using SPTInstaller.Models.Mirrors;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SPTInstaller.Interfaces;
|
||||
public interface IMirrorDownloader
|
||||
{
|
||||
public DownloadMirror MirrorInfo { get; }
|
||||
public Task<FileInfo?> Download(IProgress<double> progress);
|
||||
}
|
@ -88,7 +88,7 @@ public class InstallerUpdateInfo : ReactiveObject
|
||||
|
||||
var progress = new Progress<double>(x => DownloadProgress = (int)x);
|
||||
|
||||
var file = await DownloadCacheHelper.GetOrDownloadFileAsync("SPTInstller.exe", NewInstallerUrl, progress);
|
||||
var file = await DownloadCacheHelper.DownloadFileAsync("SPTInstller.exe", NewInstallerUrl, progress);
|
||||
|
||||
if (file == null || !file.Exists)
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using SPTInstaller.Models.Mirrors;
|
||||
|
||||
namespace SPTInstaller.Models;
|
||||
|
||||
@ -44,11 +45,6 @@ public class InternalData
|
||||
/// </summary>
|
||||
public string PatcherMirrorsLink { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The release download mirrors for the patcher
|
||||
/// </summary>
|
||||
public List<DownloadMirror> PatcherReleaseMirrors { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not a patch is needed to downgrade the client files
|
||||
/// </summary>
|
||||
|
@ -1,4 +1,4 @@
|
||||
namespace SPTInstaller.Models;
|
||||
namespace SPTInstaller.Models.Mirrors;
|
||||
|
||||
public class DownloadMirror
|
||||
{
|
@ -0,0 +1,20 @@
|
||||
using SPTInstaller.Helpers;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SPTInstaller.Models.Mirrors.Downloaders;
|
||||
public class HttpMirrorDownloader : MirrorDownloaderBase
|
||||
{
|
||||
public HttpMirrorDownloader(DownloadMirror mirror) : base(mirror)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<FileInfo?> Download(IProgress<double> progress)
|
||||
{
|
||||
var file = await DownloadCacheHelper.DownloadFileAsync("patcher.zip", MirrorInfo.Link, progress);
|
||||
|
||||
if (file == null)
|
||||
return null;
|
||||
|
||||
return FileHashHelper.CheckHash(file, MirrorInfo.Hash) ? file : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
using CG.Web.MegaApiClient;
|
||||
using SPTInstaller.Helpers;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SPTInstaller.Models.Mirrors.Downloaders;
|
||||
public class MegaMirrorDownloader : MirrorDownloaderBase
|
||||
{
|
||||
public MegaMirrorDownloader(DownloadMirror mirrorInfo) : base(mirrorInfo)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<FileInfo?> Download(IProgress<double> progress)
|
||||
{
|
||||
var megaClient = new MegaApiClient();
|
||||
await megaClient.LoginAnonymousAsync();
|
||||
|
||||
// if mega fails to connect, just return
|
||||
if (!megaClient.IsLoggedIn)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var megaDownloadStream = await megaClient.DownloadAsync(new Uri(MirrorInfo.Link), progress);
|
||||
|
||||
var file = await DownloadCacheHelper.DownloadFileAsync("patcher.zip", megaDownloadStream);
|
||||
|
||||
if (file == null)
|
||||
return null;
|
||||
|
||||
return FileHashHelper.CheckHash(file, MirrorInfo.Hash) ? file : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
//most likely a 509 (Bandwidth limit exceeded) due to mega's user quotas.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using SPTInstaller.Interfaces;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SPTInstaller.Models.Mirrors.Downloaders;
|
||||
public abstract class MirrorDownloaderBase : IMirrorDownloader
|
||||
{
|
||||
public DownloadMirror MirrorInfo { get; private set; }
|
||||
public abstract Task<FileInfo?> Download(IProgress<double> progress);
|
||||
public MirrorDownloaderBase(DownloadMirror mirrorInfo)
|
||||
{
|
||||
MirrorInfo = mirrorInfo;
|
||||
}
|
||||
}
|
@ -22,6 +22,9 @@ Start-BitsTransfer -Source $source -Destination $destination -DisplayName "Updat
|
||||
|
||||
Remove-Module -Name BitsTransfer
|
||||
|
||||
# remove the new installer from the cache folder after it is copied
|
||||
Remove-Item -Path $source
|
||||
|
||||
Start-Process $destination
|
||||
|
||||
Write-Host "Done"
|
@ -9,8 +9,8 @@
|
||||
<PackageIcon>icon.ico</PackageIcon>
|
||||
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
|
||||
<Configurations>Debug;Release;TEST</Configurations>
|
||||
<AssemblyVersion>2.13</AssemblyVersion>
|
||||
<FileVersion>2.13</FileVersion>
|
||||
<AssemblyVersion>2.14</AssemblyVersion>
|
||||
<FileVersion>2.14</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -35,12 +35,12 @@
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.4" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.4" />
|
||||
<PackageReference Include="DialogHost.Avalonia" Version="0.7.6" />
|
||||
<PackageReference Include="DialogHost.Avalonia" Version="0.7.7" />
|
||||
<PackageReference Include="FubarCoder.RestSharp.Portable.HttpClient" Version="4.0.8" />
|
||||
<PackageReference Include="MegaApiClient" Version="1.10.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.34.0" />
|
||||
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user