wabbajack/Wabbajack.Networking.Http/SingleThreadedDownloader.cs
Luca a87f8dac7f
New Features from External Contributors [Merged to Internal Test Branch so Tests can run!] (#2325)
* added enderalse GOGID

* Fix readme opening twice when loading last modlist

* Edit Wabbajack CLI button text

* Cancel running downloads when shutting down application

* Add resume support for IHttpDownloader

* Add resume support for manual downloads

* Update CHANGELOG.md

* Improve game selection to only show games with results combined with the amount of lists

* Undo accidental removal of loading settings

* Add more tooltips and improve existing ones

* Update CHANGELOG.md

* Main test external pull readme fix (#2335)

* Fix SelectedGameType crashing Wabbajack when no settings are present yet, fix readme being clickable when not specified resulting in crash

* Add readme fix to CHANGELOG, fix typo

* Add readme button fix to changelog

---------

Co-authored-by: UrbanCMC <UrbanCMC@web.de>
Co-authored-by: Angad <angadmisra28@gmail.com>
Co-authored-by: trawzified <55751269+tr4wzified@users.noreply.github.com>
Co-authored-by: Timothy Baldridge <tbaldridge@gmail.com>
2023-05-07 14:32:18 -06:00

144 lines
4.9 KiB
C#

using System;
using System.Buffers;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
namespace Wabbajack.Networking.Http;
public class SingleThreadedDownloader : IHttpDownloader
{
private readonly HttpClient _client;
private readonly ILogger<SingleThreadedDownloader> _logger;
public SingleThreadedDownloader(ILogger<SingleThreadedDownloader> logger, HttpClient client)
{
_logger = logger;
_client = client;
}
public async Task<Hash> Download(HttpRequestMessage message, AbsolutePath outputPath, IJob job,
CancellationToken token)
{
var downloader = new ResumableDownloader(message, outputPath, job);
return await downloader.Download(token);
// using var response = await _client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, token);
// if (!response.IsSuccessStatusCode)
// throw new HttpException(response);
//
// if (job.Size == 0)
// job.Size = response.Content.Headers.ContentLength ?? 0;
//
// /* Need to make this mulitthreaded to be much use
// if ((response.Content.Headers.ContentLength ?? 0) != 0 &&
// response.Headers.AcceptRanges.FirstOrDefault() == "bytes")
// {
// return await ResettingDownloader(response, message, outputPath, job, token);
// }
// */
//
// await using var stream = await response.Content.ReadAsStreamAsync(token);
// await using var outputStream = outputPath.Open(FileMode.Create, FileAccess.Write);
// return await stream.HashingCopy(outputStream, token, job);
}
private const int CHUNK_SIZE = 1024 * 1024 * 8;
private async Task<Hash> ResettingDownloader(HttpResponseMessage response, HttpRequestMessage message, AbsolutePath outputPath, IJob job, CancellationToken token)
{
using var rented = MemoryPool<byte>.Shared.Rent(CHUNK_SIZE);
var buffer = rented.Memory;
var hasher = new xxHashAlgorithm(0);
var running = true;
ulong finalHash = 0;
var inputStream = await response.Content.ReadAsStreamAsync(token);
await using var outputStream = outputPath.Open(FileMode.Create, FileAccess.Write, FileShare.None);
long writePosition = 0;
while (running && !token.IsCancellationRequested)
{
var totalRead = 0;
while (totalRead != buffer.Length)
{
var read = await inputStream.ReadAsync(buffer.Slice(totalRead, buffer.Length - totalRead),
token);
if (read == 0)
{
running = false;
break;
}
if (job != null)
await job.Report(read, token);
totalRead += read;
}
var pendingWrite = outputStream.WriteAsync(buffer[..totalRead], token);
if (running)
{
hasher.TransformByteGroupsInternal(buffer.Span);
await pendingWrite;
}
else
{
var preSize = (totalRead >> 5) << 5;
if (preSize > 0)
{
hasher.TransformByteGroupsInternal(buffer[..preSize].Span);
finalHash = hasher.FinalizeHashValueInternal(buffer[preSize..totalRead].Span);
await pendingWrite;
break;
}
finalHash = hasher.FinalizeHashValueInternal(buffer[..totalRead].Span);
await pendingWrite;
break;
}
{
writePosition += totalRead;
if (job != null)
await job.Report(totalRead, token);
message = CloneMessage(message);
message.Headers.Range = new RangeHeaderValue(writePosition, writePosition + CHUNK_SIZE);
await inputStream.DisposeAsync();
response.Dispose();
response = await _client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, token);
HttpException.ThrowOnFailure(response);
inputStream = await response.Content.ReadAsStreamAsync(token);
}
}
await outputStream.FlushAsync(token);
return new Hash(finalHash);
}
private HttpRequestMessage CloneMessage(HttpRequestMessage message)
{
var newMsg = new HttpRequestMessage(message.Method, message.RequestUri);
foreach (var header in message.Headers)
{
newMsg.Headers.Add(header.Key, header.Value);
}
return newMsg;
}
}