-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable caching of downloading MelonLoader archive #38
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,40 @@ | ||
using System.IO.Compression; | ||
using System.Security.Cryptography; | ||
|
||
namespace MelonLoader.Installer; | ||
|
||
public static class InstallerUtils | ||
{ | ||
public static HttpClient Http { get; } | ||
private static readonly SHA512 Hasher = SHA512.Create(); | ||
|
||
static InstallerUtils() | ||
{ | ||
Http = new(); | ||
Http.DefaultRequestHeaders.Add("User-Agent", $"MelonLoader Installer v{Program.VersionName}"); | ||
} | ||
|
||
public static async Task<string?> DownloadFileAsync(string url, Stream destination, InstallProgressEventHandler? onProgress) | ||
private static async Task<string?> FetchFile(string url, Stream destination, bool useCache, InstallProgressEventHandler? onProgress) | ||
{ | ||
// Cache preparation | ||
var parentDirectory = Path.GetFileName(Path.GetDirectoryName(url)) ?? ""; | ||
var fileCache = Path.Combine(Config.CacheDir, "Cache", parentDirectory, Path.GetFileName(url)); | ||
|
||
// Cache hits | ||
if (useCache && File.Exists(fileCache)) | ||
{ | ||
try | ||
{ | ||
await using var fsOut = File.OpenRead(fileCache); | ||
await fsOut.CopyToAsync(destination); | ||
return null; | ||
} | ||
catch | ||
{ | ||
return $"Failed to read the cache file {fileCache}"; | ||
} | ||
} | ||
|
||
HttpResponseMessage response; | ||
try | ||
{ | ||
|
@@ -33,35 +54,77 @@ static InstallerUtils() | |
return response.ReasonPhrase; | ||
} | ||
|
||
using var content = await response.Content.ReadAsStreamAsync(); | ||
await using var content = await response.Content.ReadAsStreamAsync(); | ||
|
||
var length = response.Content.Headers.ContentLength ?? 0; | ||
|
||
if (length > 0) | ||
{ | ||
destination.SetLength(length); | ||
var position = 0; | ||
var buffer = new byte[1024 * 16]; | ||
while (position < destination.Length - 1) | ||
{ | ||
var read = await content.ReadAsync(buffer, 0, buffer.Length); | ||
await destination.WriteAsync(buffer, 0, read); | ||
|
||
position += read; | ||
|
||
onProgress?.Invoke(position / (double)(destination.Length - 1), null); | ||
} | ||
} | ||
else | ||
{ | ||
await content.CopyToAsync(destination); | ||
return null; | ||
} | ||
|
||
var position = 0; | ||
var buffer = new byte[1024 * 16]; | ||
while (position < destination.Length - 1) | ||
|
||
// Save cache | ||
if (useCache) | ||
{ | ||
var read = await content.ReadAsync(buffer, 0, buffer.Length); | ||
await destination.WriteAsync(buffer, 0, read); | ||
|
||
position += read; | ||
|
||
onProgress?.Invoke(position / (double)(destination.Length - 1), null); | ||
try | ||
{ | ||
Directory.CreateDirectory(Path.Combine(Config.CacheDir, "Cache", parentDirectory)); | ||
await using var fsIn = File.OpenWrite(fileCache); | ||
destination.Seek(0, SeekOrigin.Begin); | ||
await destination.CopyToAsync(fsIn); | ||
} | ||
catch | ||
{ | ||
// Failed to save cache | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public static async Task<string?> DownloadFileAsync(string url, Stream destination, bool useCache, InstallProgressEventHandler? onProgress) | ||
{ | ||
// Get archive | ||
var result = await FetchFile(url, destination, useCache, onProgress); | ||
if (result != null) | ||
return $"Failed to fetch file from {url}: {result}"; | ||
|
||
destination.Seek(0, SeekOrigin.Begin); | ||
|
||
// Get checksum | ||
var checksumUrl = url.Replace(".zip", ".sha512"); | ||
using var checksumStr = new MemoryStream(); | ||
result = await FetchFile(checksumUrl, checksumStr, useCache, onProgress); | ||
|
||
// Checksum fetch failed, skip verification | ||
if (result != null) return null; | ||
|
||
var checksumDownload = System.Text.Encoding.UTF8.GetString(checksumStr.ToArray()); | ||
var checksumCompute = Convert.ToHexString(await Hasher.ComputeHashAsync(destination)); | ||
|
||
// Verification successful | ||
if (checksumCompute == checksumDownload) return null; | ||
// Verification failed, remove corrupted files | ||
var parentDirectory = Path.GetFileName(Path.GetDirectoryName(url)) ?? ""; | ||
File.Delete(Path.Combine(Config.CacheDir, "Cache", parentDirectory, Path.GetFileName(url))); | ||
File.Delete(Path.Combine(Config.CacheDir, "Cache", parentDirectory, Path.GetFileName(checksumUrl))); | ||
return "Fetched corrupted file (checksum mismatch)"; | ||
} | ||
Comment on lines
+99
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assumes that the downloaded file is a zip and that a hash is present. The purpose of this function is to download any file the caller may request. The checksum seems unnecessary, as the only possibility of a "corruption" would be a returned error message (which should be indicated by the response status code). |
||
|
||
public static string? Extract(Stream archiveStream, string destination, InstallProgressEventHandler? onProgress) | ||
{ | ||
Directory.CreateDirectory(destination); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,12 +4,12 @@ | |
|
||
namespace MelonLoader.Installer.ViewModels; | ||
|
||
public class GameModel(string path, string name, bool is32Bit, bool isLinux, GameLauncher? launcher, Bitmap? icon, SemVersion? mlVersion, bool isProtected) : ViewModelBase | ||
public class GameModel(string path, string name, Architecture architecture, GameLauncher? launcher, Bitmap? icon, SemVersion? mlVersion, bool isProtected) : ViewModelBase | ||
{ | ||
public string Path => path; | ||
public string Name => name; | ||
public bool Is32Bit => is32Bit; | ||
public bool IsLinux => isLinux; | ||
public Architecture Arch => architecture; | ||
public bool IsLinux => architecture == Architecture.LinuxX64; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
public GameLauncher? Launcher => launcher; | ||
public Bitmap? Icon => icon; | ||
public string? MLVersionText => mlVersion != null ? 'v' + mlVersion.ToString() : null; | ||
|
@@ -47,8 +47,8 @@ public bool ValidateGame() | |
return false; | ||
} | ||
|
||
var newMlVersion = Installer.MLVersion.GetMelonLoaderVersion(Dir, out var ml86, out var mlLinux); | ||
if (newMlVersion != null && (ml86 != Is32Bit || mlLinux != IsLinux)) | ||
var newMlVersion = Installer.MLVersion.GetMelonLoaderVersion(Dir, out var arch); | ||
if (newMlVersion != null && arch != Arch) | ||
newMlVersion = null; | ||
|
||
if (newMlVersion == MLVersion) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assumes the file name is unique for every download, which already breaks everything. ML versions do not have unique file names per version, so only one version will be cached.