private void _Process(ProcessState state, PatchInfo[] toUpdate, CancellationToken ct = default) { using (var md5 = MD5.Create()) using (var client = new AquaHttpClient()) { const int streamingFileSize = 10 * 1024 * 1024; // 10MB byte[] bufferBytes = new byte[streamingFileSize]; var entries = new List <PatchCacheEntry>(); bool ProcessSingleFile() { ct.ThrowIfCancellationRequested(); var index = Interlocked.Increment(ref state.AtomicIndex) - 1; if (index >= toUpdate.Length) { state.UpdateBuckets.Enqueue(entries); return(true); } if (entries.Count > 50) { state.UpdateBuckets.Enqueue(entries); entries = new List <PatchCacheEntry>(); } var patch = toUpdate[index]; var path = Path.Combine(_InstallConfiguration.PSO2BinDirectory, patch.Name.Substring(0, patch.Name.Length - 4)); if (File.Exists(path)) { using (var fs = File.OpenRead(path)) { // streaming the file into the hash is considerably slower than // reading into a byte array first. It does however avoid possible // out of memory errors with giant files. // As such we only stream the file if its bigger than the // specified file size byte[] hashBytes; if (fs.Length > streamingFileSize) { hashBytes = md5.ComputeHash(fs); } else { fs.Read(bufferBytes, 0, (int)fs.Length); hashBytes = md5.ComputeHash(bufferBytes, 0, (int)fs.Length); } var hashString = string.Concat(hashBytes.Select(b => b.ToString("X2"))); if (hashString == patch.Hash) { entries.Add(new PatchCacheEntry() { Name = patch.Name, Hash = patch.Hash, LastWriteTime = new FileInfo(path).LastWriteTimeUtc.ToFileTimeUtc() }); return(false); } } } try { // perhaps we should look for a synchronous version of the network requests, or we // should restructure this whole phase to be async only using thread pool work // for the CPU intensive tasks Directory.CreateDirectory(Path.GetDirectoryName(path)); var task = Task.Run(async() => { using (var response = await client.GetAsync(patch.DownloadPath, HttpCompletionOption.ResponseHeadersRead, ct)) { response.EnsureSuccessStatusCode(); using (var responseStream = await response.Content.ReadAsStreamAsync()) using (var fs = File.Create(path, 4096, FileOptions.Asynchronous)) await responseStream.CopyToAsync(fs, 4096, ct); } }); task.GetAwaiter().GetResult(); } catch (Exception ex) { App.Logger.Error(nameof(VerifyFilesPhase), $"Error downloading file: \"{patch.DownloadPath}\"", ex); throw; } entries.Add(new PatchCacheEntry() { Name = patch.Name, Hash = patch.Hash, LastWriteTime = new FileInfo(path).LastWriteTimeUtc.ToFileTimeUtc() }); return(false); } while (true) { if (ProcessSingleFile()) { break; } Interlocked.Increment(ref state.AtomicCompletedCount); } } }
public static async Task <List <PatchInfo> > FetchPatchInfosAsync(InstallConfiguration installConfiguration, DownloadConfiguration downloadConfiguration, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var infos = new List <PatchInfo>(); // patchlist App.Logger.Info(nameof(PatchInfo), "Downloading patch list"); using (var client = new AquaHttpClient()) using (var response = await client.GetAsync(downloadConfiguration.PatchesPatchList, ct)) using (var reader = new StringReader(await response.Content.ReadAsStringAsync())) { App.Logger.Info(nameof(PatchInfo), "Parsing patch list"); while (true) { ct.ThrowIfCancellationRequested(); var line = await reader.ReadLineAsync(); if (line == null) { break; } var parts = line.Split(); if (parts.Length < 4) { throw new Exception($"Patch list line contained less than four parts: \"{line}\""); } var name = parts[0]; var hash = parts[1]; var type = parts[3]; if (Path.GetFileNameWithoutExtension(name) == Path.GetFileName(installConfiguration.CensorFile)) { continue; } Uri root; switch (type) { case "p": root = downloadConfiguration.RootPatches; break; case "m": root = downloadConfiguration.RootMaster; break; default: throw new Exception($"Patch list line contained unknown root type \"{type}\""); } infos.Add(new PatchInfo { Name = name, Hash = hash, DownloadPath = new Uri(root, name) }); } } // launcherlist App.Logger.Info(nameof(PatchInfo), "Downloading launcher list"); using (var client = new AquaHttpClient()) using (var response = await client.GetAsync(downloadConfiguration.PatchesLauncherList, ct)) using (var reader = new StringReader(await response.Content.ReadAsStringAsync())) { App.Logger.Info(nameof(PatchInfo), "Parsing launcher list"); while (true) { ct.ThrowIfCancellationRequested(); var line = await reader.ReadLineAsync(); if (line == null) { break; } var parts = line.Split(); if (parts.Length < 3) { throw new Exception($"Launcher list line contained less than three parts: \"{line}\""); } var name = parts[0]; // parts[1] is file size var hash = parts[2]; infos.Add(new PatchInfo { Name = name, Hash = hash, DownloadPath = new Uri(downloadConfiguration.RootPatches, name) }); } } return(infos); }