Editor/AutoRig/DownloadManager.cs

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.

NetworkingFile AccessHttp Calls
🌐 https://drive.usercontent.google.com/download?id={id}&export=download&confirm=t
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" );
    }
}