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() private async Task<GenericResult> BuildMirrorList()
{ {
var mirrorListInfo = new FileInfo(Path.Join(_data.TargetInstallPath, "mirrors.json"));
SetStatus("Downloading Mirror List", false); SetStatus("Downloading Mirror List", false);
var progress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); }); 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; _data.PatcherReleaseMirrors = mirrors;
@ -63,30 +61,26 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
try try
{ {
using var megaDownloadStream = await megaClient.DownloadAsync(new Uri(mirror.Link), progress); 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(); return GenericResult.FromSuccess();
} }
catch (Exception) catch
{ {
//most likely a 509 (Bandwidth limit exceeded) due to mega's user quotas. //most likely a 509 (Bandwidth limit exceeded) due to mega's user quotas.
continue; 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(); return GenericResult.FromSuccess();
} }
@ -97,13 +91,8 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
public override async Task<GenericResult> RunAsync() 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.PatchNeeded)
{ {
if (_data.PatcherZipInfo.Exists) _data.PatcherZipInfo.Delete();
var buildResult = await BuildMirrorList(); var buildResult = await BuildMirrorList();
if (!buildResult.Succeeded) 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); SetStatus("Downloading SPT-AKI", false);
Progress = 0; Progress = 0;
var akiProgress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); }); 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(); 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 extractPatcherProgress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
var extractPatcherResult = ZipHelper.Decompress(_data.PatcherZipInfo, patcherOutputDir, extractPatcherProgress); var extractPatcherResult = ZipHelper.Decompress(_data.PatcherZipInfo, patcherOutputDir, extractPatcherProgress);
if (!extractPatcherResult.Succeeded) if (!extractPatcherResult.Succeeded)
@ -65,7 +63,6 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
{ {
return patchingResult; return patchingResult;
} }
} }
@ -92,9 +89,6 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
patcherOutputDir.Delete(true); patcherOutputDir.Delete(true);
patcherEXE.Delete(); patcherEXE.Delete();
} }
_data.PatcherZipInfo.Delete();
_data.AkiZipInfo.Delete();
return GenericResult.FromSuccess("SPT is Setup. Happy Playing!"); 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;
}
}
}
}