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);
                    }
                }
        }
Exemple #2
0
        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);
        }