Merge pull request 'impr/r2-hits' (#80) from waffle.lord/SPT-AKI-Installer:impr/r2-hits into master

Reviewed-on: CWX/SPT-AKI-Installer#80
This commit is contained in:
IsWaffle 2024-05-04 20:21:25 +00:00
commit 90be28a1f0
8 changed files with 159 additions and 114 deletions

View File

@ -8,7 +8,8 @@ namespace SPTInstaller.Helpers;
public static class DownloadCacheHelper public static class DownloadCacheHelper
{ {
private static HttpClient _httpClient = new() { Timeout = TimeSpan.FromMinutes(15) }; private static HttpClient _httpClient = new() { Timeout = TimeSpan.FromMinutes(15) };
public static TimeSpan SuggestedTtl = TimeSpan.FromHours(1);
public static string CachePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), public static string CachePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"spt-installer/cache"); "spt-installer/cache");
@ -50,10 +51,10 @@ public static class DownloadCacheHelper
/// <param name="expectedHash">The expected hash of the file in the cache</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> /// <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> /// <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) public static bool CheckCacheHash(string fileName, string expectedHash, out FileInfo cachedFile)
=> CheckCache(new FileInfo(Path.Join(CachePath, fileName)), expectedHash, out cachedFile); => CheckCacheHash(new FileInfo(Path.Join(CachePath, fileName)), expectedHash, out cachedFile);
private static bool CheckCache(FileInfo cacheFile, string expectedHash, out FileInfo fileInCache) private static bool CheckCacheHash(FileInfo cacheFile, string expectedHash, out FileInfo fileInCache)
{ {
fileInCache = cacheFile; fileInCache = cacheFile;
@ -85,6 +86,44 @@ public static class DownloadCacheHelper
return false; return false;
} }
} }
/// <summary>
/// Gets a file in the cache based on a time-to-live from its last modified time
/// </summary>
/// <param name="fileName">The name of the file to look for in the cache</param>
/// <param name="ttl">The time-to-live to check against</param>
/// <param name="cachedFile">The file found in the cache if it exists</param>
/// <returns>Returns true if the file was found in the cache, otherwise false</returns>
public static bool CheckCacheTTL(string fileName, TimeSpan ttl, out FileInfo cachedFile) =>
CheckCacheTTL(new FileInfo(Path.Join(CachePath, fileName)), ttl, out cachedFile);
private static bool CheckCacheTTL(FileInfo cacheFile, TimeSpan ttl, out FileInfo fileInCache)
{
fileInCache = cacheFile;
try
{
cacheFile.Refresh();
Directory.CreateDirectory(CachePath);
if (!cacheFile.Exists)
{
Log.Information($"{cacheFile.Name} {(cacheFile.Exists ? "is in cache" : "NOT in cache")}");
return false;
}
var validTimeToLive = cacheFile.LastWriteTime.Add(ttl) > DateTime.Now;
Log.Information($"{cacheFile.Name} TTL is {(validTimeToLive ? "OK" : "INVALID")}");
return validTimeToLive;
}
catch (Exception ex)
{
Log.Error(ex, "Something went wrong during hashing");
return false;
}
}
/// <summary> /// <summary>
/// Download a file to the cache folder /// Download a file to the cache folder
@ -166,6 +205,33 @@ public static class DownloadCacheHelper
return null; return null;
} }
} }
/// <summary>
/// Get or download a file using a time to live
/// </summary>
/// <param name="fileName">The file to get from cache</param>
/// <param name="targetLink">The link to use for the download</param>
/// <param name="progress">A progress object for reporting download progress</param>
/// <param name="timeToLive">The time-to-live to check against in the cache</param>
/// <returns></returns>
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, string targetLink,
IProgress<double> progress, TimeSpan timeToLive)
{
try
{
if (CheckCacheTTL(fileName, timeToLive, out FileInfo cachedFile))
{
return cachedFile;
}
return await DownloadFileAsync(fileName, targetLink, progress);
}
catch (Exception ex)
{
Log.Error(ex, $"Error while getting file: {fileName}");
return null;
}
}
/// <summary> /// <summary>
/// Get the file from cache or download it /// Get the file from cache or download it
@ -181,7 +247,7 @@ public static class DownloadCacheHelper
{ {
try try
{ {
if (CheckCache(fileName, expectedHash, out var cacheFile)) if (CheckCacheHash(fileName, expectedHash, out var cacheFile))
return cacheFile; return cacheFile;
return await DownloadFileAsync(fileName, targetLink, progress); return await DownloadFileAsync(fileName, targetLink, progress);
@ -206,7 +272,7 @@ public static class DownloadCacheHelper
{ {
try try
{ {
if (CheckCache(fileName, expectedHash, out var cacheFile)) if (CheckCacheHash(fileName, expectedHash, out var cacheFile))
return cacheFile; return cacheFile;
return await DownloadFileAsync(fileName, fileDownloadStream); return await DownloadFileAsync(fileName, fileDownloadStream);

View File

@ -9,98 +9,6 @@ namespace SPTInstaller.Helpers;
public static class FileHelper public static class FileHelper
{ {
private static Result IterateDirectories(DirectoryInfo sourceDir, DirectoryInfo targetDir, string[] exclusions)
{
try
{
foreach (var dir in sourceDir.GetDirectories("*", SearchOption.AllDirectories))
{
var exclude = false;
foreach (var exclusion in exclusions)
{
var currentDirRelativePath = dir.FullName.Replace(sourceDir.FullName, "");
if (currentDirRelativePath.StartsWith(exclusion) || currentDirRelativePath == exclusion)
{
exclude = true;
Log.Debug(
$"EXCLUSION FOUND :: DIR\nExclusion: '{exclusion}'\nPath: '{currentDirRelativePath}'");
break;
}
}
if (exclude)
continue;
Directory.CreateDirectory(dir.FullName.Replace(sourceDir.FullName, targetDir.FullName));
}
return Result.FromSuccess();
}
catch (Exception ex)
{
Log.Error(ex, "Error while creating directories");
return Result.FromError(ex.Message);
}
}
private static Result IterateFiles(DirectoryInfo sourceDir, DirectoryInfo targetDir, string[] exclusions,
Action<string, int> updateCallback = null)
{
try
{
int totalFiles = sourceDir.GetFiles("*.*", SearchOption.AllDirectories).Length;
int processedFiles = 0;
foreach (var file in sourceDir.GetFiles("*.*", SearchOption.AllDirectories))
{
var exclude = false;
updateCallback?.Invoke(file.Name, (int)Math.Floor(((double)processedFiles / totalFiles) * 100));
foreach (var exclusion in exclusions)
{
var currentFileRelativePath = file.FullName.Replace(sourceDir.FullName, "");
if (currentFileRelativePath.StartsWith(exclusion) || currentFileRelativePath == exclusion)
{
exclude = true;
Log.Debug(
$"EXCLUSION FOUND :: FILE\nExclusion: '{exclusion}'\nPath: '{currentFileRelativePath}'");
break;
}
if (currentFileRelativePath.EndsWith(".bak"))
{
exclude = true;
Log.Debug($"EXCLUDING BAK FILE :: {currentFileRelativePath}");
break;
}
}
if (exclude)
continue;
var targetFile = file.FullName.Replace(sourceDir.FullName, targetDir.FullName);
Log.Debug(
$"COPY\nSourceDir: '{sourceDir.FullName}'\nTargetDir: '{targetDir.FullName}'\nNewPath: '{targetFile}'");
File.Copy(file.FullName, targetFile, true);
processedFiles++;
}
return Result.FromSuccess();
}
catch (Exception ex)
{
Log.Error(ex, "Error while copying files");
return Result.FromError(ex.Message);
}
}
public static string GetRedactedPath(string path) public static string GetRedactedPath(string path)
{ {
var nameMatched = Regex.Match(path, @".:\\[uU]sers\\(?<NAME>[^\\]+)"); var nameMatched = Regex.Match(path, @".:\\[uU]sers\\(?<NAME>[^\\]+)");
@ -123,13 +31,57 @@ public static class FileHelper
{ {
try try
{ {
var iterateDirectoriesResult = IterateDirectories(sourceDir, targetDir, exclusions ??= new string[0]); var allFiles = sourceDir.GetFiles("*", SearchOption.AllDirectories);
var fileCopies = new List<CopyInfo>();
int count = 0;
if (!iterateDirectoriesResult.Succeeded) return iterateDirectoriesResult; // filter files before starting copy
foreach (var file in allFiles)
{
count++;
updateCallback?.Invoke("getting list of files to copy", (int)Math.Floor((double)count / allFiles.Length * 100));
var currentFileRelativePath = file.FullName.Replace(sourceDir.FullName, "");
if (exclusions != null)
{
// check exclusions
foreach (var exclusion in exclusions)
{
if (currentFileRelativePath.StartsWith(exclusion) || currentFileRelativePath == exclusion)
{
Log.Debug(
$"EXCLUSION FOUND :: FILE\nExclusion: '{exclusion}'\nPath: '{currentFileRelativePath}'");
break;
}
}
}
// don't copy .bak files
if (currentFileRelativePath.EndsWith(".bak"))
{
Log.Debug($"EXCLUDING BAK FILE :: {currentFileRelativePath}");
break;
}
fileCopies.Add(new CopyInfo(file.FullName, file.FullName.Replace(sourceDir.FullName, targetDir.FullName)));
}
count = 0;
var iterateFilesResult = IterateFiles(sourceDir, targetDir, exclusions ??= new string[0], updateCallback); // process copy info for files that need to be copied
foreach (var copyInfo in fileCopies)
if (!iterateFilesResult.Succeeded) return iterateDirectoriesResult; {
count++;
updateCallback?.Invoke(copyInfo.FileName, (int)Math.Floor((double)count / fileCopies.Count * 100));
var result = copyInfo.Copy();
if (!result.Succeeded)
{
return result;
}
}
return Result.FromSuccess(); return Result.FromSuccess();
} }

View File

@ -45,7 +45,7 @@ public class DownloadTask : InstallerTaskBase
{ {
SetStatus("Downloading Patcher", "Verifying cached patcher ...", progressStyle: ProgressStyle.Indeterminate); SetStatus("Downloading Patcher", "Verifying cached patcher ...", progressStyle: ProgressStyle.Indeterminate);
if (DownloadCacheHelper.CheckCache("patcher", _expectedPatcherHash, out var cacheFile)) if (DownloadCacheHelper.CheckCacheHash("patcher", _expectedPatcherHash, out var cacheFile))
{ {
_data.PatcherZipInfo = cacheFile; _data.PatcherZipInfo = cacheFile;
Log.Information("Using cached file {fileName} - Hash: {hash}", _data.PatcherZipInfo.Name, Log.Information("Using cached file {fileName} - Hash: {hash}", _data.PatcherZipInfo.Name,

View File

@ -25,8 +25,9 @@ public class ReleaseCheckTask : InstallerTaskBase
var progress = new Progress<double>((d) => { SetStatus(null, null, (int)Math.Floor(d)); }); var progress = new Progress<double>((d) => { SetStatus(null, null, (int)Math.Floor(d)); });
var akiReleaseInfoFile = var akiReleaseInfoFile =
await DownloadCacheHelper.DownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl, await DownloadCacheHelper.GetOrDownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress); progress, DownloadCacheHelper.SuggestedTtl);
if (akiReleaseInfoFile == null) if (akiReleaseInfoFile == null)
{ {
return Result.FromError("Failed to download release metadata"); return Result.FromError("Failed to download release metadata");
@ -38,8 +39,8 @@ public class ReleaseCheckTask : InstallerTaskBase
SetStatus("Checking for Patches", "", null, ProgressStyle.Indeterminate); SetStatus("Checking for Patches", "", null, ProgressStyle.Indeterminate);
var akiPatchMirrorsFile = var akiPatchMirrorsFile =
await DownloadCacheHelper.DownloadFileAsync("mirrors.json", DownloadCacheHelper.PatchMirrorUrl, await DownloadCacheHelper.GetOrDownloadFileAsync("mirrors.json", DownloadCacheHelper.PatchMirrorUrl,
progress); progress, DownloadCacheHelper.SuggestedTtl);
if (akiPatchMirrorsFile == null) if (akiPatchMirrorsFile == null)
{ {

View File

@ -0,0 +1,24 @@
using Serilog;
using SPTInstaller.Helpers;
namespace SPTInstaller.Models;
class CopyInfo(string sourcePath, string targetPath)
{
public string FileName => $"{Path.GetFileName(sourcePath)}";
public Result Copy()
{
try
{
var directory = Path.GetDirectoryName(targetPath);
Directory.CreateDirectory(directory);
Log.Debug($"COPY\nSource: {FileHelper.GetRedactedPath(sourcePath)}\nTarget: {FileHelper.GetRedactedPath(targetPath)}");
File.Copy(sourcePath, targetPath);
return Result.FromSuccess();
}
catch (Exception ex)
{
return Result.FromError(ex.Message);
}
}
}

View File

@ -140,8 +140,8 @@ public class InstallerUpdateInfo : ReactiveObject
try try
{ {
var installerInfoFile = var installerInfoFile =
await DownloadCacheHelper.DownloadFileAsync("installer.json", DownloadCacheHelper.InstallerInfoUrl, await DownloadCacheHelper.GetOrDownloadFileAsync("installer.json", DownloadCacheHelper.InstallerInfoUrl, null
null); , DownloadCacheHelper.SuggestedTtl);
if (installerInfoFile == null) if (installerInfoFile == null)
{ {

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.62</AssemblyVersion> <AssemblyVersion>2.63</AssemblyVersion>
<FileVersion>2.62</FileVersion> <FileVersion>2.63</FileVersion>
<Company>SPT-AKI</Company> <Company>SPT-AKI</Company>
</PropertyGroup> </PropertyGroup>

View File

@ -286,9 +286,11 @@ public class PreChecksViewModel : ViewModelBase
InstallButtonCheckState = StatusSpinner.SpinnerState.Running; InstallButtonCheckState = StatusSpinner.SpinnerState.Running;
var progress = new Progress<double>((d) => { }); var progress = new Progress<double>((d) => { });
var akiReleaseInfoFile = var akiReleaseInfoFile =
await DownloadCacheHelper.DownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl, await DownloadCacheHelper.GetOrDownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress); progress, DownloadCacheHelper.SuggestedTtl);
if (akiReleaseInfoFile == null) if (akiReleaseInfoFile == null)
{ {
InstallButtonText = "Could not get SPT release metadata"; InstallButtonText = "Could not get SPT release metadata";