2021-09-27 12:42:46 +00:00
using System ;
using System.Collections.Generic ;
2022-02-05 15:47:15 +00:00
using System.IO ;
2021-09-27 12:42:46 +00:00
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using Microsoft.Extensions.Logging ;
2023-06-27 14:16:03 +00:00
using Wabbajack.Common ;
2021-09-27 12:42:46 +00:00
using Wabbajack.Downloaders.Interfaces ;
2022-10-18 04:48:49 +00:00
using Wabbajack.Downloaders.VerificationCache ;
2021-09-27 12:42:46 +00:00
using Wabbajack.DTOs ;
using Wabbajack.DTOs.DownloadStates ;
using Wabbajack.DTOs.ServerResponses ;
using Wabbajack.DTOs.Validation ;
using Wabbajack.Hashing.xxHash64 ;
using Wabbajack.Networking.Http ;
using Wabbajack.Networking.WabbajackClientApi ;
using Wabbajack.Paths ;
using Wabbajack.Paths.IO ;
using Wabbajack.RateLimiter ;
2022-10-18 04:48:49 +00:00
using StringExtensions = Wabbajack . Paths . StringExtensions ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
namespace Wabbajack.Downloaders ;
public class DownloadDispatcher
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
private readonly IDownloader [ ] _downloaders ;
private readonly IResource < DownloadDispatcher > _limiter ;
private readonly ILogger < DownloadDispatcher > _logger ;
private readonly Client _wjClient ;
2022-06-08 03:48:13 +00:00
private readonly bool _useProxyCache ;
2022-10-18 04:48:49 +00:00
private readonly IVerificationCache _verificationCache ;
2021-10-23 16:51:17 +00:00
public DownloadDispatcher ( ILogger < DownloadDispatcher > logger , IEnumerable < IDownloader > downloaders ,
2022-10-18 04:48:49 +00:00
IResource < DownloadDispatcher > limiter , Client wjClient , IVerificationCache verificationCache , bool useProxyCache = true )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_downloaders = downloaders . OrderBy ( d = > d . Priority ) . ToArray ( ) ;
_logger = logger ;
_wjClient = wjClient ;
_limiter = limiter ;
2022-06-08 03:48:13 +00:00
_useProxyCache = useProxyCache ;
2022-10-18 04:48:49 +00:00
_verificationCache = verificationCache ;
2023-10-31 03:04:52 +00:00
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
2023-10-31 03:04:52 +00:00
public bool UseProxy { get ; set ; } = false ;
2022-08-09 23:41:11 +00:00
public async Task < Hash > Download ( Archive a , AbsolutePath dest , CancellationToken token , bool? proxy = null )
2021-10-23 16:51:17 +00:00
{
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested )
{
return new Hash ( ) ;
}
2022-05-20 23:00:54 +00:00
using var downloadScope = _logger . BeginScope ( "Downloading {Name}" , a . Name ) ;
using var job = await _limiter . Begin ( "Downloading " + a . Name , a . Size , token ) ;
2023-06-27 14:16:03 +00:00
var hash = await Download ( a , dest , job , token , proxy ) ;
_logger . LogInformation ( "Finished downloading {name}. Hash: {hash}; Size: {size}/{expectedSize}" , a . Name , hash , dest . Size ( ) . ToFileSizeString ( ) , a . Size . ToFileSizeString ( ) ) ;
return hash ;
2021-10-23 16:51:17 +00:00
}
2022-06-20 23:21:04 +00:00
public async Task < Archive > MaybeProxy ( Archive a , CancellationToken token )
2021-10-23 16:51:17 +00:00
{
2023-10-31 03:04:52 +00:00
if ( ! UseProxy ) return a ;
2022-06-22 21:11:26 +00:00
var downloader = Downloader ( a ) ;
if ( downloader is not IProxyable p ) return a ;
2022-06-20 23:21:04 +00:00
var uri = p . UnParse ( a . State ) ;
var newUri = await _wjClient . MakeProxyUrl ( a , uri ) ;
if ( newUri ! = null )
2022-06-08 03:48:13 +00:00
{
a = new Archive
{
Name = a . Name ,
Size = a . Size ,
Hash = a . Hash ,
State = new DTOs . DownloadStates . Http ( )
{
Url = newUri
}
} ;
2022-06-20 23:21:04 +00:00
}
return a ;
}
2022-08-09 23:41:11 +00:00
public async Task < Hash > Download ( Archive a , AbsolutePath dest , Job < DownloadDispatcher > job , CancellationToken token , bool? useProxy = null )
2022-06-20 23:21:04 +00:00
{
2023-05-07 20:32:18 +00:00
try
2022-06-20 23:21:04 +00:00
{
2023-05-07 20:32:18 +00:00
if ( ! dest . Parent . DirectoryExists ( ) )
dest . Parent . CreateDirectory ( ) ;
var downloader = Downloader ( a ) ;
if ( ( useProxy ? ? _useProxyCache ) & & downloader is IProxyable p )
2022-06-20 23:21:04 +00:00
{
2023-05-07 20:32:18 +00:00
var uri = p . UnParse ( a . State ) ;
var newUri = await _wjClient . MakeProxyUrl ( a , uri ) ;
if ( newUri ! = null )
2022-06-20 23:21:04 +00:00
{
2023-05-07 20:32:18 +00:00
a = new Archive
2022-06-20 23:21:04 +00:00
{
2023-05-07 20:32:18 +00:00
Name = a . Name ,
Size = a . Size ,
Hash = a . Hash ,
State = new DTOs . DownloadStates . Http ( )
{
Url = newUri
}
} ;
downloader = Downloader ( a ) ;
_logger . LogInformation ( "Downloading Proxy ({Hash}) {Uri}" , ( await uri . ToString ( ) . Hash ( ) ) . ToHex ( ) , uri ) ;
}
2022-06-20 23:21:04 +00:00
}
2022-06-08 03:48:13 +00:00
2023-05-07 20:32:18 +00:00
var hash = await downloader . Download ( a , dest , job , token ) ;
return hash ;
}
catch ( TaskCanceledException )
{
return new Hash ( ) ;
}
2021-10-23 16:51:17 +00:00
}
2022-10-07 22:14:01 +00:00
public Task < IDownloadState ? > ResolveArchive ( IReadOnlyDictionary < string , string > ini )
2021-10-23 16:51:17 +00:00
{
2022-10-07 22:14:01 +00:00
return Task . FromResult ( _downloaders . Select ( downloader = > downloader . Resolve ( ini ) ) . FirstOrDefault ( result = > result ! = null ) ) ;
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public async Task < bool > Verify ( Archive a , CancellationToken token )
{
try
2021-09-27 12:42:46 +00:00
{
2022-12-28 16:21:58 +00:00
var ( valid , newState ) = await _verificationCache . Get ( a . State ) ;
if ( valid = = true )
{
a . State = newState ;
2022-10-18 04:48:49 +00:00
return true ;
2022-12-28 16:21:58 +00:00
}
2023-10-31 03:04:52 +00:00
if ( UseProxy )
a = await MaybeProxy ( a , token ) ;
2021-10-23 16:51:17 +00:00
var downloader = Downloader ( a ) ;
using var job = await _limiter . Begin ( $"Verifying {a.State.PrimaryKeyString}" , - 1 , token ) ;
2022-10-18 04:48:49 +00:00
var result = await downloader . Verify ( a , job , token ) ;
await _verificationCache . Put ( a . State , result ) ;
return result ;
2021-09-27 12:42:46 +00:00
}
2024-05-30 23:03:31 +00:00
catch ( HttpException ex )
2021-09-27 12:42:46 +00:00
{
2024-05-30 23:03:31 +00:00
_logger . LogError ( $"Failed verifying {a.State.PrimaryKeyString}: {ex}" ) ;
2022-10-18 04:48:49 +00:00
await _verificationCache . Put ( a . State , false ) ;
2021-10-23 16:51:17 +00:00
return false ;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public async Task < ( DownloadResult , Hash ) > DownloadWithPossibleUpgrade ( Archive archive , AbsolutePath destination ,
CancellationToken token )
{
var downloadedHash = await Download ( archive , destination , token ) ;
if ( downloadedHash ! = default & & ( downloadedHash = = archive . Hash | | archive . Hash = = default ) )
return ( DownloadResult . Success , downloadedHash ) ;
2023-06-27 14:16:03 +00:00
try
{
downloadedHash = await DownloadFromMirror ( archive , destination , token ) ;
if ( downloadedHash ! = default ) return ( DownloadResult . Mirror , downloadedHash ) ;
}
catch ( NotSupportedException )
{
// Thrown if downloading from mirror is not supported for archive, keep original hash
}
2021-10-23 16:51:17 +00:00
return ( DownloadResult . Failure , downloadedHash ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
// TODO: implement patching
/ *
if ( ! ( archive . State is IUpgradingState ) )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Download failed for {name} and no upgrade from this download source is possible" , archive . Name ) ;
return DownloadResult . Failure ;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Trying to find solution to broken download for {name}" , archive . Name ) ;
var result = await FindUpgrade ( archive ) ;
if ( result = = default )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
result = await AbstractDownloadState . ServerFindUpgrade ( archive ) ;
if ( result = = default )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger . LogInformation (
"No solution for broken download {name} {primaryKeyString} could be found" , archive . Name , archive . State . PrimaryKeyString ) ;
2021-09-27 12:42:46 +00:00
return DownloadResult . Failure ;
}
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( $"Looking for patch for {archive.Name} ({(long)archive.Hash} {archive.Hash.ToHex()} -> {(long)result.Archive!.Hash} {result.Archive!.Hash.ToHex()})" ) ;
var patchResult = await ClientAPI . GetModUpgrade ( archive , result . Archive ! ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( $"Downloading patch for {archive.Name} from {patchResult}" ) ;
var tempFile = new TempFile ( ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
if ( WabbajackCDNDownloader . DomainRemaps . TryGetValue ( patchResult . Host , out var remap ) )
{
var builder = new UriBuilder ( patchResult ) { Host = remap } ;
patchResult = builder . Uri ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
using var response = await ( await ClientAPI . GetClient ( ) ) . GetAsync ( patchResult ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await tempFile . Path . WriteAllAsync ( await response . Content . ReadAsStreamAsync ( ) ) ;
response . Dispose ( ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( $"Applying patch to {archive.Name}" ) ;
await using ( var src = await result . NewFile . Path . OpenShared ( ) )
await using ( var final = await destination . Create ( ) )
{
Utils . ApplyPatch ( src , ( ) = > tempFile . Path . OpenShared ( ) . Result , final ) ;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
var hash = await destination . FileHashCachedAsync ( ) ;
if ( hash ! = archive . Hash & & archive . Hash ! = default )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Archive hash didn't match after patching" ) ;
return DownloadResult . Failure ;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
return DownloadResult . Update ;
* /
}
2022-06-20 23:21:04 +00:00
2021-10-23 16:51:17 +00:00
private async Task < Hash > DownloadFromMirror ( Archive archive , AbsolutePath destination , CancellationToken token )
{
try
2021-09-27 12:42:46 +00:00
{
2021-12-18 16:14:39 +00:00
var url = _wjClient . GetMirrorUrl ( archive . Hash ) ;
2021-10-23 16:51:17 +00:00
if ( url = = null ) return default ;
var newArchive =
new Archive
{
Hash = archive . Hash ,
Size = archive . Size ,
Name = archive . Name ,
State = new WabbajackCDN { Url = url }
} ;
return await Download ( newArchive , destination , token ) ;
2021-09-27 12:42:46 +00:00
}
2023-06-27 14:16:03 +00:00
catch ( Exception ex ) when ( ex is not NotSupportedException )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger . LogCritical ( ex , "While finding mirror for {hash}" , archive . Hash ) ;
return default ;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public IDownloader Downloader ( Archive archive )
{
var result = _downloaders . FirstOrDefault ( d = > d . CanDownload ( archive ) ) ;
if ( result ! = null ) return result ! ;
_logger . LogError ( "No downloader found for {type}" , archive . State . GetType ( ) ) ;
throw new NotImplementedException ( $"No downloader for {archive.State.GetType()}" ) ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public bool TryGetDownloader ( Archive archive , out IDownloader downloader )
{
var result = _downloaders . FirstOrDefault ( d = > d . CanDownload ( archive ) ) ;
if ( result ! = null )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
downloader = result ! ;
return true ;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
downloader = _downloaders . First ( ) ;
return false ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public async Task < Archive > FillInMetadata ( Archive a )
{
var downloader = Downloader ( a ) ;
if ( downloader is IMetaStateDownloader msd )
return await msd . FillInMetadata ( a ) ;
return a ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public IDownloadState ? Parse ( Uri url )
{
return _downloaders . OfType < IUrlDownloader > ( )
. Select ( downloader = > downloader . Parse ( url ) )
. FirstOrDefault ( parsed = > parsed ! = null ) ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public IEnumerable < string > MetaIni ( Archive archive )
{
return Downloader ( archive ) . MetaIni ( archive ) ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public string MetaIniSection ( Archive archive )
{
return string . Join ( "\n" , new [ ] { "[General]" } . Concat ( MetaIni ( archive ) ) ) ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public bool IsAllowed ( Archive archive , ServerAllowList allowList )
{
return Downloader ( archive ) . IsAllowed ( allowList , archive . State ) ;
}
2021-09-27 12:42:46 +00:00
2022-10-07 22:14:01 +00:00
public Task < bool > IsAllowed ( ModUpgradeRequest request , CancellationToken allowList )
2021-10-23 16:51:17 +00:00
{
throw new NotImplementedException ( ) ;
}
2021-09-27 12:42:46 +00:00
2022-10-07 22:14:01 +00:00
public Task < IEnumerable < IDownloader > > AllDownloaders ( IEnumerable < IDownloadState > downloadStates )
2021-10-23 16:51:17 +00:00
{
2022-10-07 22:14:01 +00:00
return Task . FromResult ( downloadStates . Select ( d = > Downloader ( new Archive { State = d } ) ) . Distinct ( ) ) ;
2021-10-23 16:51:17 +00:00
}
public bool Matches ( Archive archive , ServerAllowList mirrorAllowList )
{
if ( archive . State is DTOs . DownloadStates . GoogleDrive gdrive )
2021-12-17 18:02:46 +00:00
return mirrorAllowList . GoogleIDs ? . Contains ( gdrive . Id ) ? ? false ;
2021-10-23 16:51:17 +00:00
var downloader = Downloader ( archive ) ;
if ( downloader is not IUrlDownloader ud ) return false ;
var url = ud . UnParse ( archive . State ) . ToString ( ) ;
return mirrorAllowList . AllowedPrefixes . Any ( p = > url . StartsWith ( p ) ) ;
2021-09-27 12:42:46 +00:00
}
2022-02-05 15:47:15 +00:00
public async ValueTask < Stream > ChunkedSeekableStream ( Archive archive , CancellationToken token )
{
if ( ! TryGetDownloader ( archive , out var downloader ) )
{
throw new NotImplementedException ( $"Now downloader ot handle {archive.State}" ) ;
}
if ( downloader is IChunkedSeekableStreamDownloader cs )
{
return await cs . GetChunkedSeekableStream ( archive , token ) ;
}
else
{
throw new NotImplementedException ( $"Downloader {archive.State} does not support chunked seekable streams" ) ;
}
}
2021-09-27 12:42:46 +00:00
}