using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
namespace SPTInstaller.Helpers;
public static class DownloadCacheHelper
{
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),
"spt-installer/cache");
public static string ReleaseMirrorUrl = "https://spt-releases.modd.in/release.json";
public static string PatchMirrorUrl = "https://slugma.waffle-lord.net/mirrors.json";
public static string InstallerUrl = "https://ligma.waffle-lord.net/SPTInstaller.exe";
public static string InstallerInfoUrl = "https://ligma.waffle-lord.net/installer.json";
public static string GetCacheSizeText()
{
if (!Directory.Exists(CachePath))
{
var message = "No cache folder";
Log.Information(message);
return message;
}
var cacheDir = new DirectoryInfo(CachePath);
var cacheSize = DirectorySizeHelper.GetSizeOfDirectory(cacheDir);
if (cacheSize == -1)
{
var message = "An error occurred while getting the cache size :(";
Log.Error(message);
return message;
}
if (cacheSize == 0)
return "Empty";
return DirectorySizeHelper.SizeSuffix(cacheSize);
}
///
/// Check if a file in the cache already exists
///
/// The name of the file to check for
/// The expected hash of the file in the cache
/// The file found in the cache; null if no file is found
/// True if the file is in the cache and its hash matches the expected hash, otherwise false
public static bool CheckCacheHash(string fileName, string expectedHash, out FileInfo cachedFile)
=> CheckCacheHash(new FileInfo(Path.Join(CachePath, fileName)), expectedHash, out cachedFile);
private static bool CheckCacheHash(FileInfo cacheFile, string expectedHash, out FileInfo fileInCache)
{
fileInCache = cacheFile;
try
{
cacheFile.Refresh();
Directory.CreateDirectory(CachePath);
if (!cacheFile.Exists || expectedHash == null)
{
Log.Information($"{cacheFile.Name} {(cacheFile.Exists ? "is in cache" : "NOT in cache")}");
Log.Information($"Expected hash: {(expectedHash == null ? "not provided" : expectedHash)}");
return false;
}
if (FileHashHelper.CheckHash(cacheFile, expectedHash))
{
fileInCache = cacheFile;
Log.Information("Hashes MATCH");
return true;
}
Log.Warning("Hashes DO NOT MATCH");
return false;
}
catch (Exception ex)
{
Log.Error(ex, "Something went wrong during hashing");
return false;
}
}
///
/// Gets a file in the cache based on a time-to-live from its last modified time
///
/// The name of the file to look for in the cache
/// The time-to-live to check against
/// The file found in the cache if it exists
/// Returns true if the file was found in the cache, otherwise false
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;
}
}
///
/// Download a file to the cache folder
///
/// The file name to save the file as
/// The url to download the file from
/// A provider for progress updates
/// A object of the cached file
/// If the file exists, it is deleted before downloading
public static async Task DownloadFileAsync(string outputFileName, string targetLink,
IProgress progress)
{
Directory.CreateDirectory(CachePath);
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))
{
if (!await _httpClient.DownloadDataAsync(targetLink, file, progress))
{
Log.Error($"Download failed: {targetLink}");
outputFile.Refresh();
if (outputFile.Exists)
{
outputFile.Delete();
return null;
}
}
}
outputFile.Refresh();
if (!outputFile.Exists)
{
Log.Error("Failed to download file from url: {name} :: {url}", outputFileName, targetLink);
return null;
}
return outputFile;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to download file from url: {name} :: {url}", outputFileName, targetLink);
return null;
}
}
///
/// Download a file to the cache folder
///
/// The file name to save the file as
/// The stream the download the file from
/// A object of the cached file
/// If the file exists, it is deleted before downloading
public static async Task DownloadFileAsync(string outputFileName, Stream downloadStream)
{
Directory.CreateDirectory(CachePath);
var outputFile = new FileInfo(Path.Join(CachePath, outputFileName));
try
{
if (outputFile.Exists)
outputFile.Delete();
using var patcherFileStream = outputFile.Open(FileMode.Create);
{
await downloadStream.CopyToAsync(patcherFileStream);
}
patcherFileStream.Close();
outputFile.Refresh();
if (!outputFile.Exists)
{
Log.Error("Failed to download file from stream: {name}", outputFileName);
return null;
}
return outputFile;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to download file from stream: {fileName}", outputFileName);
return null;
}
}
///
/// Get or download a file using a time to live
///
/// The file to get from cache
/// The link to use for the download
/// A progress object for reporting download progress
/// The time-to-live to check against in the cache
///
public static async Task GetOrDownloadFileAsync(string fileName, string targetLink,
IProgress progress, TimeSpan timeToLive)
{
try
{
if (CheckCacheTTL(fileName, timeToLive, out FileInfo cachedFile))
{
return cachedFile;
}
Log.Information($"Downloading File: {targetLink}");
return await DownloadFileAsync(fileName, targetLink, progress);
}
catch (Exception ex)
{
Log.Error(ex, $"Error while getting file: {fileName}");
return null;
}
}
///
/// Get the file from cache or download it
///
/// The name of the file to check for in the cache
/// The url to download from if the file doesn't exist in the cache
/// A provider for progress updates
/// The expected hash of the cached file
/// A object of the cached file
/// Use if you don't have an expected cache file hash
public static async Task GetOrDownloadFileAsync(string fileName, string targetLink,
IProgress progress, string expectedHash)
{
try
{
if (CheckCacheHash(fileName, expectedHash, out var cacheFile))
return cacheFile;
Log.Information($"Downloading File: {targetLink}");
return await DownloadFileAsync(fileName, targetLink, progress);
}
catch (Exception ex)
{
Log.Error(ex, $"Error while getting file: {fileName}");
return null;
}
}
///
/// Get the file from cache or download it
///
/// The name of the file to check for in the cache
/// The stream to download from if the file doesn't exist in the cache
/// The expected hash of the cached file
/// A object of the cached file
/// Use if you don't have an expected cache file hash
public static async Task GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream,
string expectedHash)
{
try
{
if (CheckCacheHash(fileName, expectedHash, out var cacheFile))
return cacheFile;
return await DownloadFileAsync(fileName, fileDownloadStream);
}
catch (Exception ex)
{
Log.Error(ex, $"Error while getting file: {fileName}");
return null;
}
}
}