Editor-side download manager for AutoRig. It performs HTTP downloads with resume support using .part files, verifies SHA-256, handles Google Drive confirm redirects by constructing a direct usercontent URL, and writes an ATTRIBUTION.md next to downloaded files.
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AutoRig.Dl;
namespace AutoRig.Editor;
/// <summary>
/// Editor-side model downloads: HttpClient transport over the engine-side
/// DownloadCore (progress, streaming SHA-256, resume via Range when a .part file
/// exists). Google Drive's confirm-token interstitial for large files is handled.
/// Writes ATTRIBUTION.md alongside the files from the catalog entry.
/// </summary>
public static class DownloadManager
{
static readonly HttpClient Http = new() { Timeout = TimeSpan.FromMinutes( 30 ) };
/// <summary>Downloads one file into <paramref name="targetDir"/>; returns the final path.</summary>
public static async Task<string> Download(
CatalogEntry entry, DownloadFile file, string targetDir,
Action<long, long> progress, CancellationToken ct )
{
Directory.CreateDirectory( targetDir );
var finalPath = Path.Combine( targetDir, file.FileName );
var partPath = finalPath + ".part";
var resumeFrom = File.Exists( partPath ) ? new FileInfo( partPath ).Length : 0L;
using var request = new HttpRequestMessage( HttpMethod.Get, file.Url );
if ( resumeFrom > 0 )
request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue( resumeFrom, null );
using var response = await Http.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, ct );
// Google Drive interstitial: an HTML answer means "big file, confirm first".
if ( response.Content.Headers.ContentType?.MediaType == "text/html"
&& file.Url.Contains( "drive.google.com", StringComparison.OrdinalIgnoreCase ) )
{
return await DownloadGoogleDrive( entry, file, targetDir, progress, ct );
}
response.EnsureSuccessStatusCode();
var resuming = resumeFrom > 0
&& response.StatusCode == System.Net.HttpStatusCode.PartialContent;
var total = (response.Content.Headers.ContentLength ?? 0) + (resuming ? resumeFrom : 0);
if ( total == 0 )
total = file.SizeBytes;
await using ( var target = new FileStream(
partPath, resuming ? FileMode.Append : FileMode.Create, FileAccess.Write ) )
await using ( var source = await response.Content.ReadAsStreamAsync( ct ) )
{
// Hash covers only this session's bytes; resumed files re-hash on disk below.
await Task.Run( () => DownloadCore.Copy(
source, target, resuming ? resumeFrom : 0, total, progress,
() => ct.IsCancellationRequested ), ct );
}
// Verify the WHOLE file (covers resumed downloads too).
var bytes = await File.ReadAllBytesAsync( partPath, ct );
var actual = Sha256Pure.HashHex( bytes );
if ( !DownloadCore.Verify( actual, file.Sha256, out var warning ) )
{
File.Delete( partPath );
throw new IOException(
$"Checksum mismatch for {file.FileName} - deleted (expected {file.Sha256}, got {actual})." );
}
if ( warning.Length > 0 )
Log.Warning( $"[auto-rig] {file.FileName}: {warning}" );
File.Move( partPath, finalPath, overwrite: true );
WriteAttribution( entry, targetDir );
return finalPath;
}
static async Task<string> DownloadGoogleDrive(
CatalogEntry entry, DownloadFile file, string targetDir,
Action<long, long> progress, CancellationToken ct )
{
// The modern bypass: the usercontent host with confirm=t streams directly.
var id = ExtractDriveId( file.Url );
var direct = $"https://drive.usercontent.google.com/download?id={id}&export=download&confirm=t";
var redirected = new DownloadFile
{
Url = direct,
FileName = file.FileName,
SizeBytes = file.SizeBytes,
Sha256 = file.Sha256,
};
return await Download( entry, redirected, targetDir, progress, ct );
}
static string ExtractDriveId( string url )
{
var marker = "id=";
var at = url.IndexOf( marker, StringComparison.OrdinalIgnoreCase );
if ( at < 0 )
throw new IOException( $"Cannot extract a Drive file id from {url}." );
var id = url[(at + marker.Length)..];
var amp = id.IndexOf( '&' );
return amp < 0 ? id : id[..amp];
}
static void WriteAttribution( CatalogEntry entry, string targetDir )
{
var path = Path.Combine( targetDir, "ATTRIBUTION.md" );
File.WriteAllText( path,
$"# {entry.Title}\n\n{entry.Attribution}\n\nLicense: {entry.License}\n\n"
+ $"Downloaded by the user from the authors' official distribution "
+ $"({entry.HomepageUrl}); this library redistributes nothing.\n" );
}
}