2023-05-07 20:32:18 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.ComponentModel;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Downloader;
|
2023-06-27 14:16:03 +00:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
2023-10-12 18:33:06 +00:00
|
|
|
|
using Wabbajack.Configuration;
|
2023-05-07 20:32:18 +00:00
|
|
|
|
using Wabbajack.Hashing.xxHash64;
|
|
|
|
|
using Wabbajack.Paths;
|
|
|
|
|
using Wabbajack.Paths.IO;
|
|
|
|
|
using Wabbajack.RateLimiter;
|
|
|
|
|
|
|
|
|
|
namespace Wabbajack.Networking.Http;
|
|
|
|
|
|
|
|
|
|
internal class ResumableDownloader
|
|
|
|
|
{
|
|
|
|
|
private readonly IJob _job;
|
|
|
|
|
private readonly HttpRequestMessage _msg;
|
|
|
|
|
private readonly AbsolutePath _outputPath;
|
|
|
|
|
private readonly AbsolutePath _packagePath;
|
2023-10-12 18:33:06 +00:00
|
|
|
|
private readonly PerformanceSettings _performanceSettings;
|
2023-06-27 14:16:03 +00:00
|
|
|
|
private readonly ILogger<SingleThreadedDownloader> _logger;
|
2023-05-07 20:32:18 +00:00
|
|
|
|
private CancellationToken _token;
|
|
|
|
|
private Exception? _error;
|
|
|
|
|
|
2023-06-27 14:16:03 +00:00
|
|
|
|
|
2023-10-12 18:33:06 +00:00
|
|
|
|
public ResumableDownloader(HttpRequestMessage msg, AbsolutePath outputPath, IJob job, PerformanceSettings performanceSettings, ILogger<SingleThreadedDownloader> logger)
|
2023-05-07 20:32:18 +00:00
|
|
|
|
{
|
|
|
|
|
_job = job;
|
|
|
|
|
_msg = msg;
|
|
|
|
|
_outputPath = outputPath;
|
|
|
|
|
_packagePath = outputPath.WithExtension(Extension.FromPath(".download_package"));
|
2023-10-12 18:33:06 +00:00
|
|
|
|
_performanceSettings = performanceSettings;
|
2023-06-27 14:16:03 +00:00
|
|
|
|
_logger = logger;
|
2023-05-07 20:32:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<Hash> Download(CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
_token = token;
|
|
|
|
|
|
|
|
|
|
var downloader = new DownloadService(CreateConfiguration(_msg));
|
|
|
|
|
downloader.DownloadStarted += OnDownloadStarted;
|
|
|
|
|
downloader.DownloadProgressChanged += OnDownloadProgressChanged;
|
|
|
|
|
downloader.DownloadFileCompleted += OnDownloadFileCompleted;
|
|
|
|
|
|
|
|
|
|
// Attempt to resume previous download
|
|
|
|
|
var downloadPackage = LoadPackage();
|
|
|
|
|
if (downloadPackage != null)
|
|
|
|
|
{
|
|
|
|
|
// Resume with different Uri in case old one is no longer valid
|
|
|
|
|
downloadPackage.Address = _msg.RequestUri!.AbsoluteUri;
|
|
|
|
|
|
2023-06-27 14:16:03 +00:00
|
|
|
|
_logger.LogDebug("Download for {name} is resuming...", _outputPath.FileName.ToString());
|
2023-05-07 20:32:18 +00:00
|
|
|
|
await downloader.DownloadFileTaskAsync(downloadPackage, token);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2023-06-27 14:16:03 +00:00
|
|
|
|
_logger.LogDebug("Download for '{name}' is starting from scratch...", _outputPath.FileName.ToString());
|
2023-05-07 20:32:18 +00:00
|
|
|
|
_outputPath.Delete();
|
|
|
|
|
await downloader.DownloadFileTaskAsync(_msg.RequestUri!.AbsoluteUri, _outputPath.ToString(), token);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save progress if download isn't completed yet
|
|
|
|
|
if (downloader.Status is DownloadStatus.Stopped or DownloadStatus.Failed)
|
|
|
|
|
{
|
2023-06-27 14:16:03 +00:00
|
|
|
|
_logger.LogDebug("Download for '{name}' stopped before completion. Saving package...", _outputPath.FileName.ToString());
|
2023-05-07 20:32:18 +00:00
|
|
|
|
SavePackage(downloader.Package);
|
2023-06-27 14:16:03 +00:00
|
|
|
|
if (_error == null || _error.GetType() == typeof(TaskCanceledException))
|
2023-05-07 20:32:18 +00:00
|
|
|
|
{
|
2023-06-27 14:16:03 +00:00
|
|
|
|
return new Hash();
|
2023-05-07 20:32:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-27 14:16:03 +00:00
|
|
|
|
if (_error.GetType() == typeof(NotSupportedException))
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning("Download for '{name}' doesn't support resuming. Deleting package...", _outputPath.FileName.ToString());
|
|
|
|
|
DeletePackage();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError(_error,"Download for '{name}' encountered error. Throwing...", _outputPath.FileName.ToString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw _error;
|
2023-05-07 20:32:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (downloader.Status == DownloadStatus.Completed)
|
|
|
|
|
{
|
|
|
|
|
DeletePackage();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!_outputPath.FileExists())
|
|
|
|
|
{
|
|
|
|
|
return new Hash();
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-27 14:16:03 +00:00
|
|
|
|
await using var file = _outputPath.Open(FileMode.Open);
|
|
|
|
|
return await file.Hash(token);
|
2023-05-07 20:32:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private DownloadConfiguration CreateConfiguration(HttpRequestMessage message)
|
|
|
|
|
{
|
2023-10-12 18:33:06 +00:00
|
|
|
|
var maximumMemoryPerDownloadThreadMb = Math.Max(0, _performanceSettings.MaximumMemoryPerDownloadThreadMb);
|
2023-05-07 20:32:18 +00:00
|
|
|
|
var configuration = new DownloadConfiguration
|
|
|
|
|
{
|
2023-10-12 18:33:06 +00:00
|
|
|
|
MaximumMemoryBufferBytes = maximumMemoryPerDownloadThreadMb * 1024 * 1024,
|
2023-06-27 14:16:03 +00:00
|
|
|
|
Timeout = (int)TimeSpan.FromSeconds(120).TotalMilliseconds,
|
|
|
|
|
ReserveStorageSpaceBeforeStartingDownload = true,
|
2023-05-07 20:32:18 +00:00
|
|
|
|
RequestConfiguration = new RequestConfiguration
|
|
|
|
|
{
|
|
|
|
|
Headers = message.Headers.ToWebHeaderCollection(),
|
|
|
|
|
ProtocolVersion = message.Version,
|
|
|
|
|
UserAgent = message.Headers.UserAgent.ToString()
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return configuration;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDownloadFileCompleted(object? sender, AsyncCompletedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
_error = e.Error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void OnDownloadProgressChanged(object? sender, DownloadProgressChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
var processedSize = e.ProgressedByteSize;
|
|
|
|
|
if (_job.Current == 0)
|
|
|
|
|
{
|
|
|
|
|
// Set current to total in case this download resumes from a previous one
|
|
|
|
|
processedSize = e.ReceivedBytesSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await _job.Report(processedSize, _token);
|
2023-06-27 14:16:03 +00:00
|
|
|
|
if (_job.Current > _job.Size)
|
|
|
|
|
{
|
|
|
|
|
// Increase job size so progress doesn't appear stalled
|
|
|
|
|
_job.Size = (long)Math.Floor(_job.Current * 1.1);
|
|
|
|
|
}
|
2023-05-07 20:32:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDownloadStarted(object? sender, DownloadStartedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
_job.ResetProgress();
|
2023-06-27 14:16:03 +00:00
|
|
|
|
|
|
|
|
|
if (_job.Size < e.TotalBytesToReceive)
|
|
|
|
|
{
|
|
|
|
|
_job.Size = e.TotalBytesToReceive;
|
|
|
|
|
}
|
2023-05-07 20:32:18 +00:00
|
|
|
|
|
|
|
|
|
// Get rid of package, since we can't use it to resume anymore
|
|
|
|
|
DeletePackage();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DeletePackage()
|
|
|
|
|
{
|
|
|
|
|
_packagePath.Delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private DownloadPackage? LoadPackage()
|
|
|
|
|
{
|
|
|
|
|
if (!_packagePath.FileExists())
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-20 19:24:31 +00:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var packageJson = _packagePath.ReadAllText();
|
|
|
|
|
return JsonSerializer.Deserialize<DownloadPackage>(packageJson);
|
|
|
|
|
}
|
|
|
|
|
catch (JsonException ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning(ex, "Package for '{name}' couldn't be parsed. Deleting package and starting from scratch...", _outputPath.FileName.ToString());
|
|
|
|
|
DeletePackage();
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2023-05-07 20:32:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SavePackage(DownloadPackage package)
|
|
|
|
|
{
|
|
|
|
|
var packageJson = JsonSerializer.Serialize(package);
|
|
|
|
|
_packagePath.WriteAllText(packageJson);
|
|
|
|
|
}
|
|
|
|
|
}
|