Merge pull request 'add basic download caching' (#10) from waffle.lord/SPT-AKI-Installer:master into master

Reviewed-on: CWX/SPT-AKI-Installer#10
This commit is contained in:
CWX 2023-03-05 20:00:47 +00:00
commit 113a518599
5 changed files with 178 additions and 96 deletions

View File

@ -20,22 +20,20 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
private async Task<GenericResult> BuildMirrorList()
{
var mirrorListInfo = new FileInfo(Path.Join(_data.TargetInstallPath, "mirrors.json"));
SetStatus("Downloading Mirror List", false);
var progress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
var downloadResult = await DownloadHelper.DownloadFile(mirrorListInfo, _data.PatcherMirrorsLink, progress);
var file = await DownloadCacheHelper.GetOrDownloadFileAsync("mirrors.json", _data.PatcherMirrorsLink, progress);
if (!downloadResult.Succeeded)
if (file == null)
{
return downloadResult;
return GenericResult.FromError("Failed to download mirror list");
}
var blah = JsonConvert.DeserializeObject<List<DownloadMirror>>(File.ReadAllText(mirrorListInfo.FullName));
var mirrorsList = JsonConvert.DeserializeObject<List<DownloadMirror>>(File.ReadAllText(file.FullName));
if (blah is List<DownloadMirror> mirrors)
if (mirrorsList is List<DownloadMirror> mirrors)
{
_data.PatcherReleaseMirrors = mirrors;
@ -63,30 +61,26 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
try
{
using var megaDownloadStream = await megaClient.DownloadAsync(new Uri(mirror.Link), progress);
using var patcherFileStream = _data.PatcherZipInfo.Open(FileMode.Create);
{
await megaDownloadStream.CopyToAsync(patcherFileStream);
}
patcherFileStream.Close();
_data.PatcherZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("patcher.zip", megaDownloadStream, mirror.Hash);
if(!DownloadHelper.FileHashCheck(_data.PatcherZipInfo, mirror.Hash))
if(_data.PatcherZipInfo == null)
{
return GenericResult.FromError("Hash mismatch");
continue;
}
return GenericResult.FromSuccess();
}
catch (Exception)
catch
{
//most likely a 509 (Bandwidth limit exceeded) due to mega's user quotas.
continue;
}
}
var result = await DownloadHelper.DownloadFile(_data.PatcherZipInfo, mirror.Link, progress, mirror.Hash);
_data.PatcherZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("patcher.zip", mirror.Link, progress, mirror.Hash);
if (result.Succeeded)
if (_data.PatcherZipInfo != null)
{
return GenericResult.FromSuccess();
}
@ -97,13 +91,8 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
public override async Task<GenericResult> RunAsync()
{
_data.PatcherZipInfo = new FileInfo(Path.Join(_data.TargetInstallPath, "patcher.zip"));
_data.AkiZipInfo = new FileInfo(Path.Join(_data.TargetInstallPath, "sptaki.zip"));
if (_data.PatchNeeded)
{
if (_data.PatcherZipInfo.Exists) _data.PatcherZipInfo.Delete();
var buildResult = await BuildMirrorList();
if (!buildResult.Succeeded)
@ -122,19 +111,17 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
}
}
if (_data.AkiZipInfo.Exists) _data.AkiZipInfo.Delete();
SetStatus("Downloading SPT-AKI", false);
Progress = 0;
var akiProgress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
var releaseDownloadResult = await DownloadHelper.DownloadFile(_data.AkiZipInfo, _data.AkiReleaseDownloadLink, akiProgress);
_data.AkiZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki.zip", _data.AkiReleaseDownloadLink, akiProgress);
if (!releaseDownloadResult.Succeeded)
if (_data.AkiZipInfo == null)
{
return releaseDownloadResult;
return GenericResult.FromError("Failed to download spt-aki");
}
return GenericResult.FromSuccess();

View File

@ -31,8 +31,6 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
var extractPatcherProgress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
var extractPatcherResult = ZipHelper.Decompress(_data.PatcherZipInfo, patcherOutputDir, extractPatcherProgress);
if (!extractPatcherResult.Succeeded)
@ -65,7 +63,6 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
{
return patchingResult;
}
}
@ -92,9 +89,6 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
patcherOutputDir.Delete(true);
patcherEXE.Delete();
}
_data.PatcherZipInfo.Delete();
_data.AkiZipInfo.Delete();
return GenericResult.FromSuccess("SPT is Setup. Happy Playing!");
}

View File

@ -0,0 +1,140 @@
using HttpClientProgress;
using SPT_AKI_Installer.Aki.Core.Model;
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Helper
{
public static class DownloadCacheHelper
{
private static HttpClient _httpClient = new HttpClient() { Timeout = TimeSpan.FromHours(1) };
private static string _cachePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "spt-installer/cache");
private static async Task<GenericResult> DownloadFile(FileInfo outputFile, string targetLink, IProgress<double> progress, string expectedHash = null)
{
try
{
// Use the provided extension method
using (var file = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None))
await _httpClient.DownloadDataAsync(targetLink, file, progress);
outputFile.Refresh();
if (!outputFile.Exists)
{
return GenericResult.FromError($"Failed to download {outputFile.Name}");
}
if (expectedHash != null && !FileHashHelper.CheckHash(outputFile, expectedHash))
{
return GenericResult.FromError("Hash mismatch");
}
return GenericResult.FromSuccess();
}
catch (Exception ex)
{
return GenericResult.FromError(ex.Message);
}
}
private static async Task<GenericResult> ProcessInboundStreamAsync(FileInfo cacheFile, Stream downloadStream, string expectedHash = null)
{
try
{
cacheFile.Refresh();
Directory.CreateDirectory(_cachePath);
if (cacheFile.Exists)
{
if (expectedHash != null && FileHashHelper.CheckHash(cacheFile, expectedHash))
{
return GenericResult.FromSuccess();
}
cacheFile.Delete();
cacheFile.Refresh();
}
using var patcherFileStream = cacheFile.Open(FileMode.Create);
{
await downloadStream.CopyToAsync(patcherFileStream);
}
patcherFileStream.Close();
if (expectedHash != null && !FileHashHelper.CheckHash(cacheFile, expectedHash))
{
return GenericResult.FromError("Hash mismatch");
}
return GenericResult.FromSuccess();
}
catch(Exception ex)
{
return GenericResult.FromError(ex.Message);
}
}
private static async Task<GenericResult> ProcessInboundFileAsync(FileInfo cacheFile, string targetLink, IProgress<double> progress, string expectedHash = null)
{
try
{
cacheFile.Refresh();
Directory.CreateDirectory(_cachePath);
if (cacheFile.Exists)
{
if (expectedHash != null && FileHashHelper.CheckHash(cacheFile, expectedHash))
{
return GenericResult.FromSuccess();
}
cacheFile.Delete();
cacheFile.Refresh();
}
return await DownloadFile(cacheFile, targetLink, progress, expectedHash);
}
catch(Exception ex)
{
return GenericResult.FromError(ex.Message);
}
}
public static async Task<FileInfo> GetOrDownloadFileAsync(string fileName, string targetLink, IProgress<double> progress, string expectedHash = null)
{
FileInfo cacheFile = new FileInfo(Path.Join(_cachePath, fileName));
try
{
var result = await ProcessInboundFileAsync(cacheFile, targetLink, progress, expectedHash);
return result.Succeeded ? cacheFile : null;
}
catch
{
return null;
}
}
public static async Task<FileInfo> GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream, string expectedHash = null)
{
FileInfo cacheFile = new FileInfo(Path.Join(_cachePath, fileName));
try
{
var result = await ProcessInboundStreamAsync(cacheFile, fileDownloadStream, expectedHash);
return result.Succeeded ? cacheFile : null;
}
catch
{
return null;
}
}
}
}

View File

@ -1,63 +0,0 @@
using HttpClientProgress;
using SPT_AKI_Installer.Aki.Core.Model;
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Helper
{
public static class DownloadHelper
{
private static HttpClient _httpClient = new HttpClient() { Timeout = TimeSpan.FromHours(1) };
public static bool FileHashCheck(FileInfo file, string expectedHash)
{
using (MD5 md5Service = MD5.Create())
using (var sourceStream = file.OpenRead())
{
byte[] sourceHash = md5Service.ComputeHash(sourceStream);
byte[] expectedHashBytes = Convert.FromBase64String(expectedHash);
bool matched = Enumerable.SequenceEqual(sourceHash, expectedHashBytes);
return matched;
}
}
public static async Task<GenericResult> DownloadFile(FileInfo outputFile, string targetLink, IProgress<double> progress, string expectedHash = null)
{
try
{
outputFile.Refresh();
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);
outputFile.Refresh();
if (!outputFile.Exists)
{
return GenericResult.FromError($"Failed to download {outputFile.Name}");
}
if (expectedHash != null && !FileHashCheck(outputFile, expectedHash))
{
return GenericResult.FromError("Hash mismatch");
}
return GenericResult.FromSuccess();
}
catch (Exception ex)
{
return GenericResult.FromError(ex.Message);
}
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
namespace SPT_AKI_Installer.Aki.Helper
{
public static class FileHashHelper
{
public static bool CheckHash(FileInfo file, string expectedHash)
{
using (MD5 md5Service = MD5.Create())
using (var sourceStream = file.OpenRead())
{
byte[] sourceHash = md5Service.ComputeHash(sourceStream);
byte[] expectedHashBytes = Convert.FromBase64String(expectedHash);
bool matched = Enumerable.SequenceEqual(sourceHash, expectedHashBytes);
return matched;
}
}
}
}