Merge pull request 'improve-mirror-download-info' (#33) from waffle.lord/SPT-AKI-Installer:improve-mirror-download-info into master

Reviewed-on: CWX/SPT-AKI-Installer#33
This commit is contained in:
IsWaffle 2023-09-21 22:53:55 +00:00
commit d1e56171cc
11 changed files with 209 additions and 103 deletions

View File

@ -26,23 +26,32 @@ public static class DownloadCacheHelper
return DirectorySizeHelper.SizeSuffix(cacheSize); 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 try
{ {
cacheFile.Refresh(); cacheFile.Refresh();
Directory.CreateDirectory(CachePath); Directory.CreateDirectory(CachePath);
if (cacheFile.Exists) if (!cacheFile.Exists || expectedHash == null)
{ return false;
if (expectedHash != null && FileHashHelper.CheckHash(cacheFile, expectedHash))
{
Log.Information($"Using cached file: {cacheFile.Name} - Hash: {expectedHash}");
return true;
}
cacheFile.Delete(); if (FileHashHelper.CheckHash(cacheFile, expectedHash))
cacheFile.Refresh(); {
fileInCache = cacheFile;
return true;
} }
return false; 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 try
{ {
if (outputFile.Exists)
outputFile.Delete();
// Use the provided extension method // Use the provided extension method
using (var file = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None)) using (var file = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None))
await _httpClient.DownloadDataAsync(targetLink, file, progress); await _httpClient.DownloadDataAsync(targetLink, file, progress);
@ -65,90 +87,100 @@ public static class DownloadCacheHelper
if (!outputFile.Exists) 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 outputFile;
{
return Result.FromError("Hash mismatch");
}
return Result.FromSuccess();
} }
catch (Exception ex) 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 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); await downloadStream.CopyToAsync(patcherFileStream);
} }
patcherFileStream.Close(); 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) 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 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) 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)
{ {
Log.Error(ex, $"Error while getting file: {fileName}"); Log.Error(ex, $"Error while getting file: {fileName}");
return null; 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 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}"); Log.Error(ex, $"Error while getting file: {fileName}");
return null; return null;

View File

@ -1,16 +1,20 @@
using CG.Web.MegaApiClient; using Newtonsoft.Json;
using Newtonsoft.Json;
using SPTInstaller.Interfaces; using SPTInstaller.Interfaces;
using SPTInstaller.Models; using SPTInstaller.Models;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using SPTInstaller.Helpers; using SPTInstaller.Helpers;
using SPTInstaller.Models.Mirrors;
using SPTInstaller.Models.Mirrors.Downloaders;
using Serilog;
namespace SPTInstaller.Installer_Tasks; namespace SPTInstaller.Installer_Tasks;
public class DownloadTask : InstallerTaskBase public class DownloadTask : InstallerTaskBase
{ {
private InternalData _data; private InternalData _data;
private List<IMirrorDownloader> _mirrors = new List<IMirrorDownloader>();
private string _expectedPatcherHash = "";
public DownloadTask(InternalData data) : base("Download Files") public DownloadTask(InternalData data) : base("Download Files")
{ {
@ -19,9 +23,9 @@ public class DownloadTask : InstallerTaskBase
private async Task<IResult> BuildMirrorList() 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) if (file == null)
{ {
@ -30,52 +34,43 @@ public class DownloadTask : InstallerTaskBase
var mirrorsList = JsonConvert.DeserializeObject<List<DownloadMirror>>(File.ReadAllText(file.FullName)); var mirrorsList = JsonConvert.DeserializeObject<List<DownloadMirror>>(File.ReadAllText(file.FullName));
if (mirrorsList is List<DownloadMirror> mirrors) if (mirrorsList == null)
{ return Result.FromError("Failed to deserialize mirrors list");
_data.PatcherReleaseMirrors = mirrors;
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) 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);
// 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();
// 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(); 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); foreach (var mirror in _mirrors)
{
SetStatus("Downloading Patcher", mirror.MirrorInfo.Link, progressStyle: ProgressStyle.Indeterminate);
_data.PatcherZipInfo = await mirror.Download(progress);
if (_data.PatcherZipInfo != null) if (_data.PatcherZipInfo != null)
{ {

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

View File

@ -88,7 +88,7 @@ public class InstallerUpdateInfo : ReactiveObject
var progress = new Progress<double>(x => DownloadProgress = (int)x); 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) if (file == null || !file.Exists)
{ {

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using SPTInstaller.Models.Mirrors;
namespace SPTInstaller.Models; namespace SPTInstaller.Models;
@ -44,11 +45,6 @@ public class InternalData
/// </summary> /// </summary>
public string PatcherMirrorsLink { get; set; } public string PatcherMirrorsLink { get; set; }
/// <summary>
/// The release download mirrors for the patcher
/// </summary>
public List<DownloadMirror> PatcherReleaseMirrors { get; set; } = null;
/// <summary> /// <summary>
/// Whether or not a patch is needed to downgrade the client files /// Whether or not a patch is needed to downgrade the client files
/// </summary> /// </summary>

View File

@ -1,4 +1,4 @@
namespace SPTInstaller.Models; namespace SPTInstaller.Models.Mirrors;
public class DownloadMirror public class DownloadMirror
{ {

View File

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

View File

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

View File

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

View File

@ -22,6 +22,9 @@ Start-BitsTransfer -Source $source -Destination $destination -DisplayName "Updat
Remove-Module -Name BitsTransfer Remove-Module -Name BitsTransfer
# remove the new installer from the cache folder after it is copied
Remove-Item -Path $source
Start-Process $destination Start-Process $destination
Write-Host "Done" Write-Host "Done"

View File

@ -9,8 +9,8 @@
<PackageIcon>icon.ico</PackageIcon> <PackageIcon>icon.ico</PackageIcon>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon> <ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<Configurations>Debug;Release;TEST</Configurations> <Configurations>Debug;Release;TEST</Configurations>
<AssemblyVersion>2.13</AssemblyVersion> <AssemblyVersion>2.14</AssemblyVersion>
<FileVersion>2.13</FileVersion> <FileVersion>2.14</FileVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -35,12 +35,12 @@
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.4" /> <PackageReference Include="Avalonia.Diagnostics" Version="11.0.4" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.4" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.4" />
<PackageReference Include="Avalonia.Themes.Fluent" 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="FubarCoder.RestSharp.Portable.HttpClient" Version="4.0.8" />
<PackageReference Include="MegaApiClient" Version="1.10.3" /> <PackageReference Include="MegaApiClient" Version="1.10.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <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" /> <PackageReference Include="System.Reactive" Version="6.0.0" />
</ItemGroup> </ItemGroup>