Downloader

bezzadMIT活跃维护

面向 .NET 应用的多线程分片下载库,支持异步进度回调,跨平台且稳定,适合在 C# 项目里集成文件下载能力。

Fast, cross-platform and reliable multipart downloader with asynchronous progress events for .NET applications.

v7.0.0发布于 2026-11-07 16:58 UTC1,726star +1 / 7 天252fork同步于 2026-11-10 11:46 UTC

怎么上手

由 README 提炼,执行前请确认
dotnet add package Downloader
硬件要求按 README 说明

README 未说明,纯 .NET 库,可在 Windows、Linux、macOS 上运行,无需 GPU

有 Docker 镜像
下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

面向 .NET 的跨平台多分片下载库,用异步进度事件解决大文件下载慢和断点续传问题

面向 .NET 应用的多线程分片下载库,支持异步进度回调,跨平台且稳定,适合在 C# 项目里集成文件下载能力。

核心能力

  • 按 ChunkCount 把文件切成多片并行下载
  • 下载过程中实时推送每个分片的进度与速度
  • 自动续传:把元数据写入 .download 文件,下次自动恢复
  • 支持限速、失败重试、指定字节区间下载
  • 可下载到内存流,支持边下边播的流式读取

容易踩的坑

v3.2.0 起只支持 .NET 8 及以上,老框架需用 v3.1.x;下载中文件先写成 .download 临时文件,完成后才改名;自定义 Accept-Encoding 为 gzip/br 时必须同时设置 AutomaticDecompression,否则存下的是压缩字节

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v7.0.0
  1. v7.0.0最新正式版2026-11-07 · 2026-11-07 16:58 UTC

    Progress events are now raised one call at a time, in order (#256). It has breaking changes (marked BREAKING below), so the major version goes up.

    🔄 Changed

    • DownloadProgressChanged is raised one call at a time, in order (#256). Before, every chunk thread raised it, so with a parallel download the handler ran on several threads at once, an older total could arrive after a newer one, and Rx observers broke. Now the storage writer reports the bytes it has written, and one deliverer on the thread pool raises the event. The download never waits for the handler: a slow handler gets merged updates (ProgressedByteSize is the sum since the last one), and the last update can arrive after DownloadFileCompleted. A handler that throws is logged and the download goes on. ChunkDownloadProgressChanged does not change: it still runs on each chunk's thread, so use it if you must stop a download at an exact point.
    • BREAKING (behaviour): live streaming uses one chunk. With EnableLiveStreaming = true the download uses one connection, so ReceivedBytes comes in file order, and every block reaches the handler. When the waiting blocks pass MaximumMemoryBufferBytes, the download waits for the handler (0 = no limit).
    • BREAKING: EnableLiveStreaming can not change while a download runs. Setting it to a new value then throws an InvalidOperationException.
    • BREAKING (API): ConcurrentStream.Write and ConcurrentStream.WriteAsync are internal now. Only the download itself writes to its storage; these methods were public only because the class is.

    🐛 Fixes

    • A download could hang forever when the memory buffer was full. If the writer emptied the buffer at the same moment a chunk stopped adding to it, nothing started the adding again. This happens most with a small MaximumMemoryBufferBytes.

    What's Changed

    Full Changelog: https://github.com/bezzad/Downloader/compare/v6.0.1...v7.0.0

  2. v6.0.12026-11-03 · 2026-11-03 09:51 UTC
    查看更新说明

    🐛 Fixes

    • A pause while the download is starting is kept again. 6.0.0 made Pause() and Resume() do nothing unless the download was Running or Paused, so a Pause() while the server was still asked for the size (status None or Created) was lost and the download ran on. Now only a finished download (Completed, Failed or Stopped) ignores them.

    Full Changelog: https://github.com/bezzad/Downloader/compare/v6.0.0...v6.0.1

  3. v6.0.02026-11-02 · 2026-11-02 16:27 UTC
    查看更新说明

    A big safety and reliability release. It has two breaking changes (marked BREAKING below), so the major version goes up. Version 5.11.0 was never published; its fixes are part of this release.

    🔒 Security

    • BREAKING: server certificates are checked strictly. The library had its own certificate check that accepted expired, self-signed and wrong-host certificates, so anyone on the network could replace the file or read your credentials. Now the normal platform check is used, and a certificate error is not retried. If you must trust a private or self-signed certificate, give your own handler with CustomHttpMessageHandlerFactory (see the README).
    • A server file name can no longer write outside your folder. The name from Content-Disposition (for example ../../.bashrc, /etc/x or C:\x.exe) is cleaned: only the last part is kept, and characters that are not allowed in a file name become _. filename*=UTF-8''... names are now read, and FileName= is matched without caring about case.
    • Credentials are not sent to another host after a redirect. The Authorization header (from RequestConfiguration.Authorization or Credentials) is sent only to the scheme, host and port you asked for. After a redirect to a CDN, or from https to http, it is left out.

    🔄 Changed

    • BREAKING: the system proxy is used when RequestConfiguration.Proxy is null. Before, no proxy was used at all, so downloads failed on networks that need one. Now the OS proxy settings (or HTTP_PROXY / HTTPS_PROXY / NO_PROXY) are used, like HttpClient does. To use no proxy, set Proxy = new WebProxy().

    🐛 Fixes

    • A server that ignores ranges no longer breaks the file. When a server (or one of the mirror URLs) said it supports ranges but sent the whole file to a chunk request, those bytes were written in the middle of the file and the download reported Completed. Every chunk now checks the answer. On a wrong answer the download starts again once over one connection; a RangeDownload fails with the new InvalidRangeResponseException.
    • Auto-resume no longer mixes two versions of a file. The ETag and Last-Modified headers are saved with the resume state (new PackageInfo.ETag / LastModified, also on RemoteFileInfo). If the server file changed but kept its size, the download starts again instead of mixing old and new bytes.
    • Auto-resume reads its state correctly. The saved state now starts with its length, so a shorter state written over a longer one is read right (before, the download silently started again from 0). Old .download files are still read.
    • Bytes that were not written are not counted. A write to a closed storage threw nothing and the chunk counted the bytes anyway, which could leave holes after a manual resume. Now the write throws ObjectDisposedException, the rented buffer is returned, and the packet is not left in the queue.
    • Your configuration is no longer changed by a download. For a small file, a server without range support, or a range download, the library wrote ChunkCount, ParallelCount, ParallelDownload, RangeLow/RangeHigh and CustomHttpClientFactory back into your DownloadConfiguration, so the next download with the same object was slow. These values are now kept per download. DownloadConfiguration.Clone() now also copies RequestConfiguration (headers and client certificates are new lists; cookies, proxy and credentials stay shared).
    • Long retry runs are safe. A chunk retry called itself again, so many failures in a row made a deeper and deeper call chain. It is now a loop. The read timeout that grows on every retry is capped at 60 seconds (or BlockTimeout if that is bigger); before, it could overflow to a negative value.
    • ParallelDownload = false now really uses one connection. All chunks were started at once and only waited on a semaphore of ParallelCount, so a "serial" download opened up to ParallelCount connections. Now chun
  4. v5.10.22026-11-01 · 2026-11-01 06:31 UTC
    查看更新说明

    A small fix for pause/resume and cancel threading.

    🐛 Fixes

    • Resume and cancel no longer run work on the wrong thread (#251). PauseTokenSource and the download's completion source finished their tasks inline. So Resume() could run the paused download work on the caller's thread (for example the UI thread), and code after await CancelTaskAsync() could run on the download thread before DownloadFileCompleted was raised. Both now use TaskCreationOptions.RunContinuationsAsynchronously.

    Full Changelog: https://github.com/bezzad/Downloader/compare/v5.10.1...v5.10.2

  5. v5.10.02026-10-15 · 2026-10-15 13:35 UTC
    查看更新说明

    Safer live settings, correct file size after a server error, and smarter splitting of the last chunks.

    ✨ New

    • Idle connections can help finish the download. When MinimumChunkSize is set (greater than 0), a parallel worker that has finished its own chunk can split the largest unfinished chunk and download its second half, as long as both halves stay at least MinimumChunkSize. With the default 0 nothing changes. (#245, thanks @crazysmile-PhD)

    🐛 Fixes

    • A server error on the first probe could give a wrong, tiny file reported as complete (#248). The headers of an error response (for example a 500 with a 41-byte XML body) were read as the file's headers, so the error body's length became the file size. Now only a success or a redirect fills the response headers, so the download really asks again and fails if it never reaches the file.
    • Range support is kept after a passing server error. A 500 on the first Range: 0-0 probe no longer switches the download to one connection; the Range probe is asked again. A transient status on the probe (503, 429, 504, 408) is retried up to 3 times instead of failing the download.
    • Settings you change during a download are thread-safe (#247). MaximumBytesPerSecond, ActiveChunks, BufferBlockSize and EnableLiveStreaming use atomic reads and writes, so a change from another thread (like a UI speed limit) is seen by the download threads at once, and PropertyChanged fires once per real change.

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    Windows x64 Ubuntu x64 MacOS codecov NuGet NuGet CodeFactor License Generic badge Generic badge Generic badge Generic badge Generic badge

    Generated by github-dependents-info

    Downloader

    :rocket: Fast, cross-platform, and reliable multipart downloader in .Net :rocket:

    [!TIP] Prefer a ready-to-use app over a library? Meet Downloader Desktop — a free, open-source GUI download manager for Windows · Linux · macOS, powered by this engine: multi-connection downloads, pause/resume, queues, scheduler, speed limits and more — with no dependencies to install.

    Downloader is a modern, fluent, asynchronous, and portable library for .NET, built with testability in mind. It supports multipart downloads with real-time asynchronous progress events. The current v5.x line targets .NET 8, .NET 9, .NET 10, and .NET 11 (preview). (If you need .NET Standard 2.1 or older runtimes such as .NET Framework 4.6.1, use the v3.1.x line — see the note below.)

    Downloader works on Windows, Linux, and macOS.

    Note: Support for older versions of .NET was removed in Downloader v3.2.0. From this version onwards, only .Net 8.0 and higher versions are supported.
    If you need compatibility with older .NET versions (e.g., .NET Framework 4.6.1), use Downloader v3.1.*.

    For a complete example, see the Downloader.Sample project in this repository.

    Table of Contents

    Quick Start

    dotnet add package Downloader
    
    await DownloadBuilder
        .New()
        .WithUrl(@"https://host.com/test-file.zip")
        .WithDirectory(@"C:\temp")
        .Build()
        .StartAsync();
    

    Sample Console Application

    sample-project


    Key Features

    • Simple interface for download requests.
    • Asynchronous, non-blocking file downloads.
    • Supports all file types (e.g., images, videos, PDFs, APKs).
    • Cross-platform support for files of any size.
    • Real-time progress updates for each download chunk.
    • Downloads files in multiple parts (parallel download).
    • Resilient to client-side and server-side errors.
    • Configurable ChunkCount to control download segmentation.
    • Supports both in-memory and on-disk multipart downloads.
    • Parallel saving of all chunks directly into one file (no extra file per chunk).
    • Always downloads to a temporary file (configurable extension, default .download), then renames to the final name on completion.
    • Always pre-allocates file size before download begins.
    • Resume downloads manually by saving and restoring the DownloadPackage object.
    • Automatic resume: when enabled, download metadata is embedded inside the .download file — no extra files or manual serialization needed.
    • Provides real-time speed and progress data.
    • Asynchronous pause and resume functionality.
    • Download files with dynamic speed limits.
    • Supports downloading to memory streams (without saving to disk).
    • Supports large file downloads and live-streaming (e.g., music playback during download).
    • Download a specific byte range from a large file.
    • Resolve a remote file's name, size, and range support without downloading it (RemoteFileResolver).
    • Lightweight, fast codebase. The only dependency is Microsoft.Extensions.Logging.Abstractions.
    • Manage RAM usage during downloads.
    • Supports custom HttpClient or HttpMessageHandler injection for advanced scenarios (e.g., IHttpClientFactory, HTTP caching, custom delegating handlers).

    Installation via NuGet

    PM> Install-Package Downloader
    

    Installation via the .NET CLI

    dotnet add package Downloader
    

    Usage

    Step 1: Create a Custom Configuration

    Simple Configuration
    var downloadOpt = new DownloadConfiguration()
    {
        // Number of file parts, default is 1
        ChunkCount = 8, 
        // Download parts in parallel (default is false)
        ParallelDownload = true 
    };
    
    Complex Configuration

    Note: Only include the options you need in your application.

    var downloadOpt = new DownloadConfiguration()
    {
        // size of each read from the network (1 byte to 1MB),
        // the default value is 1024 bytes
        BufferBlockSize = 10240, // 10KB
        // file parts to download, the default value is 1
        ChunkCount = 8,             
        // download speed limited to MaximumBytesPerSecond, 
        // the default value is zero, which means unlimited
        MaximumBytesPerSecond = 1024*1024*2, // 2MB/s
        // the maximum number of retries for each chunk,
        // the default value is int.MaxValue (retry forever)
        MaxTryAgainOnFailure = 5,    
        // release memory buffer after each MaximumMemoryBufferBytes 
        MaximumMemoryBufferBytes = 1024 * 1024 * 50, // 50MB
        // download parts of the file in parallel or not. 
        // The default value is false
        ParallelDownload = true,
        // number of parallel downloads. 
        // The default value is the same as the chunk count
        ParallelCount = 4,    
        // timeout (millisecond) per stream block reader, 
        // the default value is 5000 (minimum 100)
        BlockTimeout = 5000,
        // timeout (millisecond) per HttpClient request, 
        // default value is 100 seconds
        HttpClientTimeout = 100 * 1000,
        // set true if you want to download just a specific 
        // range of bytes of a large file
        RangeDownload = false,
        // floor offset of download range of a large file
        RangeLow = 0,
        // ceiling offset of download range of a large file
        RangeHigh = 0, 
        // clear package chunks data when download completed 
        // with failure, default value is false
        ClearPackageOnCompletionWithFailure = true, 
        // the minimum size of file to chunking or download a
        // file in multiple parts, the default value is 512
        MinimumSizeOfChunking = 102400, // 100KB
        // the minimum size of a single chunk, 
        // default value is 0 equal unlimited
        MinimumChunkSize = 10240, // 10KB
        // Get on-demand downloaded data with 
        // ReceivedBytes on the downloadProgressChanged event.
        // Live streaming uses one chunk, and it can not be
        // changed while a download runs (see Step 3)
        EnableLiveStreaming = false,
        // How to handle an existing filename when 
        // starting to download?
        FileExistPolicy = FileExistPolicy.Delete,
        // When enabled, the Downloader appends package 
        // metadata to the end of the .download file. 
        // On the next download attempt, 
        // if metadata is found in an existing .download file,
        // the download resumes automatically.
        EnableAutoResumeDownload = true,
        // A temporary extension appended to 
        // the real filename while downloading.
        // e.g., "file.zip" becomes "file.zip.download" 
        // during download. The Downloader always uses this
        // extension regardless of EnableAutoResumeDownload.
        // When the download completes, 
        // the file is renamed back to its final name.
        DownloadFileExtension = ".download",
        // config and customize request headers
        RequestConfiguration = 
        {        
            Accept = "*/*",
            CookieContainer = cookies,
            // your custom headers. Note: the default list is ["Accept-Encoding: identity"];
            // if you ask for gzip/br here, also set AutomaticDecompression,
            // or the compressed bytes are saved as the file.
            Headers = ["Accept-Encoding: identity", "X-Custom-Header: value"],
            KeepAlive = true, 
            ProtocolVersion = HttpVersion.Version11,
            // your custom user agent or 
            // your_app_name/app_version.
            UserAgent = "Mozilla/5.0",
            // without a Proxy the system proxy is used;
            // new WebProxy() (no address) means no proxy at all.
            Proxy = new WebProxy() {
               Address = new Uri(
                "http://YourProxyServer:8080"),
               UseDefaultCredentials = false,
               Credentials = System.Net.CredentialCache.DefaultNetworkCredentials,
               BypassProxyOnLocal = true
            },
            Authorization = new AuthenticationHeaderValue
                ("Bearer", "XX_YOUR_TOKEN_XX")
        }
    };
    

    Recommended setup for fast and reliable downloads

    A good general-purpose starting point — parallel for speed, with retries and auto-resume for safety:

    var downloadOpt = new DownloadConfiguration
    {
        // split into 8 parts
        ChunkCount = 8,         
        // download chunks as parallel (fast)        
        ParallelDownload = true,
        // cap concurrent connections to be a good citizen
        ParallelCount = 4,              
        // retry transient network errors before giving up
        MaxTryAgainOnFailure = 5,       
        // survive crashes/network drops with zero extra code
        EnableAutoResumeDownload = true,
        // cap RAM used for buffering (50MB)
        MaximumMemoryBufferBytes = 50 * 1024 * 1024, 
        // fail early if there isn't enough disk space
        CheckDiskSizeBeforeDownload = true, 
        // 0 = unlimited; set a value to limit speed B/s
        MaximumBytesPerSecond = 0,   
    };
    

    Tip: Higher ChunkCount/ParallelCount is not always faster — many servers rate-limit or cap connections per client. Values around 8 chunks and 4 parallel connections are a safe, fast default; tune for your target hosts.

    Step 2: Create the Download Service

    var downloader = new DownloadService(downloadOpt);
    

    Step 3: Handle Download Events

    // Provide `FileName` and `TotalBytesToReceive` at 
    // the start of each download
    downloader.DownloadStarted += OnDownloadStarted;
    
    // Provide any information about chunk downloads, 
    // like progress percentage per chunk, speed, 
    // total received bytes and received bytes array 
    // to live streaming.
    downloader.ChunkDownloadProgressChanged += OnChunkDownloadProgressChanged;
    
    // Provide any information about download progress, 
    // like progress percentage of sum of chunks, total speed, 
    // average speed, total received bytes and 
    // received bytes array to live streaming.
    downloader.DownloadProgressChanged += OnDownloadProgressChanged;
    
    // Download completed event that can include errors or 
    // canceled or download completed successfully.
    downloader.DownloadFileCompleted += OnDownloadFileCompleted;
    

    Example handler implementations:

    void OnDownloadStarted(object? sender, DownloadStartedEventArgs e)
        => Console.WriteLine($"Started '{e.FileName}' ({e.TotalBytesToReceive} bytes)");
    
    void OnDownloadProgressChanged(object? sender, DownloadProgressChangedEventArgs e)
        => Console.WriteLine($"{e.ProgressPercentage:F1}% @ {e.BytesPerSecondSpeed:F0} B/s");
    
    void OnDownloadFileCompleted(object? sender, System.ComponentModel.AsyncCompletedEventArgs e)
    {
        if (e.Cancelled)
            Console.WriteLine("Download was stopped by the caller.");
        else if (e.Error is not null)
            Console.WriteLine($"Download FAILED: {e.Error.Message}"); // inspect/log e.Error
        else
            Console.WriteLine("Download completed successfully.");
    }
    

    ℹ️ How progress events are raised

    • ChunkDownloadProgressChanged runs on each chunk's own thread, so with a parallel download these calls can run at the same time.
    • DownloadProgressChanged reports the bytes that are already written to the file (or memory stream). It is raised one call at a time and in order, so the total never goes down, and it is safe for Rx (OnNext is never called twice at once). The download never waits for this handler: when the handler is slow, several updates are merged into one, and ProgressedByteSize is the sum of the bytes since the last event.
    • The handler runs on a thread-pool thread, so keep it short. An exception it throws is logged, and the download goes on.
    • DownloadFileCompleted does not wait for the progress handler, so the last progress event (100%) can arrive a little after it.
    • Live streaming (EnableLiveStreaming = true): the download uses one chunk, so ReceivedBytes comes in file order, and every block reaches the handler (nothing is merged). If the handler is slower than the network, the blocks wait in memory; when they pass MaximumMemoryBufferBytes, the download waits for the handler (0 = no limit). The last block is the one with ReceivedBytesSize == TotalBytesToReceive. Set EnableLiveStreaming before the download starts; changing it while a download runs throws an InvalidOperationException.

    ⚠️ Important: how failures and cancellations are reported

    DownloadFileTaskAsync(...) / StartAsync(...) do not throw when a download fails or is cancelled. The awaited call completes normally, and the outcome is delivered through the DownloadFileCompleted event:

    • Success → e.Cancelled == false and e.Error == null
    • Failure → e.Error contains the exception (e.g. a network error, or an IncompleteDownloadException if the server ended the stream early)
    • Stopped by you → e.Cancelled == true (also when FileExistPolicy.IgnoreDownload skips an existing file). Pause() does not raise this event; the download is still running.

    Always subscribe to DownloadFileCompleted and check e.Error before treating the file as complete — relying only on the await returning will silently miss failed downloads. You can also inspect downloader.Package.Status (Completed / Stopped / Failed).

    Step 4: Start the Download

    string file = @"Your_Path\fileName.zip";
    string url = @"https://file-examples.com/fileName.zip";
    await downloader.DownloadFileTaskAsync(url, file);
    

    Step 4b: Start the download without a file name

    DirectoryInfo path = new DirectoryInfo("Your_Path");
    string url = @"https://file-examples.com/fileName.zip";
    // download into "Your_Path\fileName.zip"
    await downloader.DownloadFileTaskAsync(url, path); 
    

    The file name comes from the server (Content-Disposition, filename*= first) or from the URL. It is cleaned before use: only the last path part is kept and invalid characters become _, so a server can never write a file outside path.

    Step 4c: Download in MemoryStream

    // After the download completes, you get a MemoryStream
    Stream destinationStream = await downloader.DownloadFileTaskAsync(url); 
    

    How to get the file name and size without downloading

    Sometimes you need a remote file's name and size before (or without ever) downloading it — for example to populate a list/grid of queued downloads with their names and sizes while they wait for a slot, instead of starting and immediately stopping a real download just to read the headers.

    Use RemoteFileResolver. It performs a single lightweight header probe (a Range: 0-0 GET that follows redirects) and resolves the file name exactly the way the downloader does internally — from the Content-Disposition header, falling back to the URL path, and finally to a generated GUID — plus the size (Content-Range → Content-Length) and whether the server supports ranged (resumable) downloads.

    // Just the file name 
    // (resilient: falls back to the URL-derived name 
    // on a network/server error)
    string fileName = await RemoteFileResolver.GetFileNameAsync(url);
    
    // Full metadata in one probe:
    RemoteFileInfo info = await RemoteFileResolver.GetFileInfoAsync(url);
    Console.WriteLine($"{info.FileName} — {info.FileSize} bytes, range: {info.SupportsRange}");
    // info.Address is the final URL after any redirects.
    

    Both methods accept an optional DownloadConfiguration (to reuse your headers, proxy, credentials, cookies, redirect policy, …) and a CancellationToken. Each call owns and disposes its own client, so when resolving many URLs at once add your own concurrency limiting / timeouts.

    If you already hold a download service, the same lookup is available on it (using that service's configuration, without disturbing any download in progress):

    RemoteFileInfo info = await downloader.GetFileInfoAsync(url);
    

    Note: FileSize is -1 when the server does not advertise a length. GetFileNameAsync and the service/GetFileInfoAsync previews are best-effort and won't throw on a server that hides its size — you still get a usable name.


    How to pause and resume downloads quickly

    When you want to resume a download quickly after pausing for a few seconds, call the Pause function of the downloader service. This way, the streams stay alive and are only suspended by a lock, to be released and resumed whenever you want.

    // Pause the download
    downloader.Pause();
    
    // Resume the download
    downloader.Resume();
    

    How to stop and resume downloads (manual approach)

    The DownloadService class has a property called Package that holds a live snapshot of the download state (chunk positions, URL, file path, etc.). While the download is in progress, this object is updated continuously.

    To stop and later resume a download manually, you are responsible for keeping the Package object yourself — either in memory or serialized to disk. The Downloader does not store it for you in this approach.

    // 1. Keep a reference to the package before or after stopping:
    DownloadPackage pack = downloader.Package;
    

    Stop or cancel the download:

    // Fire-and-forget cancellation (returns void):
    downloader.CancelAsync();
    
    // Or await until the download has actually stopped before reading the package:
    await downloader.CancelTaskAsync();
    

    Resume later — even after restarting the application:

    // Pass the same (or deserialized) package to resume from the last position:
    await downloader.DownloadFileTaskAsync(pack);
    

    The Package object is lightweight — it contains only the URL, file path, and the position of each chunk (not the downloaded bytes). You can serialize it to JSON or binary (see Serialization section) and restore it at any time.

    For more details see the StopResumeDownloadTest method.

    Note: If the server does not support HTTP range requests, the download cannot be resumed and will restart from the beginning.


    How to automatically resume downloads (recommended)

    If you don't want to manage DownloadPackage serialization yourself, enable EnableAutoResumeDownload. The Downloader will then handle everything automatically — no manual serialization or package management is needed.

    Configuration:

    var downloadOpt = new DownloadConfiguration()
    {
        EnableAutoResumeDownload = true,
        // optional, the default extension is '.download'
        DownloadFileExtension = ".download" 
    };
    
    var downloader = new DownloadService(downloadOpt);
    

    How .download files work:

    The Downloader always uses a temporary file with the .download extension (configurable via DownloadFileExtension) — this behavior is independent of EnableAutoResumeDownload. For example, downloading report.pdf creates report.pdf.download on disk. While the download is in progress, only the .download file exists — the user does not see the final filename. When the download completes successfully, the file is renamed to report.pdf.

    What EnableAutoResumeDownload adds:

    When this option is true, the Downloader appends package metadata to the end of the .download file, immediately after the file data. This metadata enables automatic resume without any work from the caller.

    The file structure during download looks like this:

    report.pdf.download:
     _____________________________________________________
    |                                    |                |
    |<------------ File Data ----------->|<-- Metadata -->|
    |____________________________________|________________|
    

    The metadata is an 8-byte length and then the JSON, so an older and longer metadata that is still on disk behind it is ignored. Files from older versions (JSON only) are still read.

    • File Data region: The actual file content. The file size for this region is pre-allocated at the start.
    • Metadata region: The DownloadPackage state (includes TotalFileSize and Chunks) is serialized to JSON and then written as binary at the end of the same file.

    The metadata grows as the download progresses (more chunk positions are recorded), so the on-disk file size is always TotalFileSize + current metadata size. The metadata only grows — it never shrinks — so no padding or extra management is needed.

    On interruption:

    If the download is interrupted (crash, network failure, app restart), the .download file remains on disk with both the partial file data and the latest metadata embedded at the end.

    On resume:

    When you call DownloadFileTaskAsync for the same file again, the Downloader:

    1. Detects the existing .download file
    2. Reads the metadata from the end of the file to restore the DownloadPackage state
    3. Verifies the server still supports range requests, that the file size has not changed, and that the ETag and Last-Modified headers (when the server sends them) are the same as before
    4. Resumes downloading from where each chunk left off
    5. Falls back to a fresh download if any validation fails

    On completion:

    When the download finishes successfully:

    1. The file stream is truncated to TotalFileSize using SetLength(TotalFileSize), which removes the appended metadata
    2. The file is renamed from report.pdf.download to report.pdf

    The result is a clean final file with no metadata artifacts.

    Note: If the server does not support range requests or the remote file size has changed, the Downloader will discard the partial data and start a fresh download.


    Fluent download builder usage

    For easy and fluent use of the downloader, you can use the DownloadBuilder class. Consider the following examples:

    Simple usage:

    await DownloadBuilder
        .New()
        .WithUrl(@"https://host.com/test-file.zip")
        .WithDirectory(@"C:\temp")
        .Build()
        .StartAsync();
    

    Complex usage:

    IDownload download = DownloadBuilder
        .New()
        .WithUrl(@"https://host.com/test-file.zip")
        .WithDirectory(@"C:\temp")
        .WithFileName("test-file.zip")
        .WithConfiguration(new DownloadConfiguration())
        .Build();
    
    download.DownloadProgressChanged += DownloadProgressChanged;
    download.DownloadFileCompleted += DownloadFileCompleted;
    download.DownloadStarted += DownloadStarted;
    download.ChunkDownloadProgressChanged += ChunkDownloadProgressChanged;
    
    await download.StartAsync();
    
    download.Stop(); // cancel current download
    

    Resume the existing download package:

    await DownloadBuilder.New().Build(package).StartAsync();
    

    Resume the existing download package with a new configuration:

    await DownloadBuilder.New().Build(package, config).StartAsync();
    

    Pause and Resume quickly:

    var download = DownloadBuilder
        .New()
        .WithUrl(url)
        .WithFileLocation(path)
        .Build();
    
    download.DownloadProgressChanged += (_, _) => {
        // pause current download quickly
        download.Pause();  
        // continue current download quickly
        download.Resume(); 
    };
    
    await download.StartAsync().ConfigureAwait(false);
    

    Using a Custom HttpClient or HttpMessageHandler

    Some scenarios require using a custom HttpClient or HttpMessageHandler — for example, to reuse the connection pool from IHttpClientFactory, add HTTP caching via a DelegatingHandler, or apply custom authentication logic.

    The Downloader provides two delegate properties on DownloadConfiguration for this purpose:

    Option 1: Provide a fully custom HttpClient

    Use CustomHttpClientFactory when you want full control over the HttpClient instance. The Downloader will skip all internal handler and header configuration and use the returned client directly.

    var downloadOpt = new DownloadConfiguration()
    {
        ChunkCount = 8,
        ParallelDownload = true,
        CustomHttpClientFactory = () => {
            // Example: use IHttpClientFactory
            return httpClientFactory.CreateClient("MyDownloader");
        }
    };
    
    var downloader = new DownloadService(downloadOpt);
    await downloader.DownloadFileTaskAsync(url, filePath);
    

    Option 2: Provide a custom HttpMessageHandler

    Use CustomHttpMessageHandlerFactory when you want to customize only the handler (e.g., for caching or custom SSL), while letting the Downloader still configure default request headers and timeout on the HttpClient.

    var downloadOpt = new DownloadConfiguration()
    {
        ChunkCount = 8,
        ParallelDownload = true,
        CustomHttpMessageHandlerFactory = () => {
            return new SocketsHttpHandler {
                MaxConnectionsPerServer = 500,
                PooledConnectionLifetime = TimeSpan.FromMinutes(10)
            };
        }
    };
    
    var downloader = new DownloadService(downloadOpt);
    await downloader.DownloadFileTaskAsync(url, filePath);
    

    Using the fluent builder API

    Both options are also available through the DownloadBuilder:

    // With a custom HttpClient
    await DownloadBuilder.New()
        .WithUrl(url)
        .WithDirectory(@"C:\temp")
        .WithHttpClient(() => httpClientFactory.CreateClient("MyDownloader"))
        .Build()
        .StartAsync();
    
    // With a custom HttpMessageHandler
    await DownloadBuilder.New()
        .WithUrl(url)
        .WithDirectory(@"C:\temp")
        .WithHttpMessageHandler(() => new SocketsHttpHandler {
            MaxConnectionsPerServer = 500,
            PooledConnectionLifetime = TimeSpan.FromMinutes(10)
        })
        .Build()
        .StartAsync();
    

    Note: If both CustomHttpClientFactory and CustomHttpMessageHandlerFactory are set, CustomHttpClientFactory takes precedence and CustomHttpMessageHandlerFactory is ignored.

    Private or self-signed certificates

    Server certificates are checked by the platform. A download from a server with an expired, self-signed or wrong-host certificate fails. If you trust such a server (for example on your own network), give your own handler and check the certificate yourself:

    var config = new DownloadConfiguration {
        CustomHttpMessageHandlerFactory = () => new SocketsHttpHandler {
            SslOptions = {
                // Only accept the one certificate you trust, never "return true" for all.
                RemoteCertificateValidationCallback = (_, cert, _, errors) =>
                    errors == SslPolicyErrors.None || cert?.GetCertHashString() == "YOUR-CERT-THUMBPRINT"
            }
        }
    };
    

    When does the Downloader fail to download in multiple chunks?

    Content-Length

    If your server does not provide the file size in the response header (Content-Length), the Downloader cannot split the file into multiple parts and continues its work with one chunk.

    Accept-Ranges

    If the server returns Accept-Ranges: none in the response header, that means the server does not support downloading in range, so the Downloader cannot use multiple chunks and continues its work with a single chunk.

    Content-Range

    At first, the Downloader sends a GET request to the server to fetch the file's size in the range. If the server does not provide Content-Range in the header, that means the server does not support downloading in range. Therefore, the Downloader has to continue its work with one chunk.

    Range answers

    Some servers (or a CDN node, or a mirror) say they support ranges but then send the whole file with 200 OK. Every chunk checks the answer: it must be 206 from the asked byte (or 200 when the chunk starts at byte 0). If not, the Downloader starts again once over one connection, so the file is never broken. For a RangeDownload this is not possible, so the download fails with InvalidRangeResponseException.

    Content-Encoding (compressed responses)

    If the server returns a compressed response (a Content-Encoding header such as gzip, deflate, or br), the Downloader falls back to a single, non-ranged chunk and treats the size as unknown. A Range request addresses byte offsets in the compressed stream, which do not map to offsets in the decompressed bytes the client ultimately delivers — so splitting a compressed representation into ranges would corrupt the output. Downloading as one chunk keeps decompression consistent. By default the Downloader sends Accept-Encoding: identity, so servers normally do not compress. If you ask for compression (for example with a custom Accept-Encoding header), also set RequestConfiguration.AutomaticDecompression; otherwise the compressed bytes are saved as the file.


    Redirects, cookies, and protected links

    The Downloader follows HTTP redirects automatically (AllowAutoRedirect = true, up to MaximumAutomaticRedirections = 50), including 301, 302, 303, 307, and 308. It also follows same-URL "challenge" redirects — for example a CDN that answers with a 307 whose Location points back to the same URL and a Set-Cookie, expecting the client to retry carrying that cookie.

    To make those challenges resolvable, RequestConfiguration.CookieContainer now defaults to a new CookieContainer, so cookies issued mid-redirect are stored and replayed on the follow-up request. Set it to null to disable cookie handling entirely, or supply your own container with pre-seeded cookies:

    var downloadOpt = new DownloadConfiguration
    {
        RequestConfiguration =
        {
            CookieContainer = myCookieContainer, // or null to disable cookies
        }
    };
    

    Note: No HTTP client can pass a JavaScript-based bot challenge (the cookie value must be computed by a browser), nor revive an expired signed link — such URLs typically return a small HTML "expired/blocked" page with status 200. If a download produces a tiny unexpected file, inspect the real response first (e.g. curl -I <url>) before assuming a Downloader bug.


    How to serialize and deserialize the downloader package

    Tip: If you use EnableAutoResumeDownload = true, you do not need to serialize the package yourself — the Downloader handles it automatically by embedding metadata in the .download file. The section below is only relevant if you use the manual resume approach.

    The DownloadPackage object holds the download state (URL, file path, chunk positions). You can serialize it to JSON or binary so that you can restore and resume a stopped download later — even after the application restarts.

    JSON Serialization

    // Serialize
    var packageJson = JsonConvert.SerializeObject(package);
    
    // Deserialize
    var restoredPack = JsonConvert.DeserializeObject<DownloadPackage>(packageJson);
    
    // Resume
    await downloader.DownloadFileTaskAsync(restoredPack);
    

    For more details see the PackageSerializationTest method.

    Binary Serialization

    To save the package as a binary file, serialize it to JSON first and then write it with BinaryWriter.

    NOTE: Do not use BinaryFormatter — it is deprecated and insecure. Reference

    🚀 Building a Native AOT Version

    The library is Native AOT compatible (IsAotCompatible, no reflection-based serialization), so you can use it in an app that you publish with Native AOT. Follow these steps to build a native AOT version of the library itself.


    Prerequisites

    • .NET 8.0 SDK or later.
    • A supported platform (Windows, Linux, or macOS).

    Build Instructions

    1. Clone the Repository
    git clone https://github.com/bezzad/downloader.git
    cd downloader
    
    2. Build the Native AOT Executable

    Run the following command to compile the project for your target platform:

    Windows (x64):

    dotnet publish src/Downloader/Downloader.csproj -r win-x64 -f net8.0 -c Release
    

    Linux (x64):

    dotnet publish src/Downloader/Downloader.csproj -r linux-x64 -f net8.0 -c Release
    

    macOS (x64):

    dotnet publish src/Downloader/Downloader.csproj -r osx-x64 -f net8.0 -c Release
    
    3. Find the Output

    The output will be located in:

    src/Downloader/bin/Release/net8.0/<RUNTIME_IDENTIFIER>/publish/
    

    Example for Windows:

    src/Downloader/bin/Release/net8.0/win-x64/publish/
    

    Instructions for Contributing

    Contributions are welcome — feel free to make changes and open a Pull Request against the develop branch. You can use either the latest version of Visual Studio or Visual Studio Code and .NET CLI for Windows, Mac and Linux.

    For GitHub workflow, check out our Git workflow below this paragraph. We are following the excellent GitHub Flow process, and would like to make sure you have all the information needed to be a world-class contributor!

    Git Workflow

    The general process for working with Downloader is:

    1. Fork on GitHub
    2. Make sure your line endings are correctly configured and fix your line endings!
    3. Clone your fork locally
    4. Configure the upstream repo (git remote add upstream https://github.com/bezzad/downloader.git)
    5. Switch to the latest development branch (git checkout develop)
    6. Create a local branch from that (git checkout -b myBranch).
    7. Work on your feature
    8. Rebase if required
    9. Push the branch up to GitHub (git push origin myBranch)
    10. Send a Pull Request on GitHub - the PR should target (have as a base branch) the develop branch rather than master.

    We accept pull requests from the community. But you should never work on a clone of master, and you should never send a pull request from master - always from a branch. Please be sure to branch from the head of the develop branch (rather than master) when developing contributions.

    You can run tests with the Docker Compose file with the following command

    docker compose -p downloader up

    (Older Docker installs use docker-compose instead.)

    Or with docker file

    docker build -f ./dockerfile -t downloader-linux . docker run --name downloader-linux-container -d downloader-linux --env=ASPNETCORE_ENVIRONMENT=Development .

    Or run the following command to call docker directly

    docker run --rm -v ${pwd}:/app --env=ASPNETCORE_ENVIRONMENT=Development -w /app/src mcr.microsoft.com/dotnet/sdk:10.0 dotnet test -p:TargetFrameworks=net10.0 --logger:trx


    Support the Project

    I've spent a lot of time creating this project.

    If you like my work, please give it a ⭐ — thanks! ❤️

    Want to support the project?

    You can make a donation in any of the following ways:

    Donate using Liberapay

    donate tether (BSC)


    License

    Licensed under the terms of the MIT License

    FOSSA Status

    Contributors

    Thanks go to these wonderful people (List made with contrib. rocks):

    Top Dependent Repositories

    A few of the public projects that depend on Downloader, sorted by stars. See the full list or browse all of them on GitHub Dependents.

    RepositoryStars
      2dust / v2rayN119666
      lively-community / lively19980
      luolangaga / tubatools5966
      PCL-Community / PCL-CE5023
      goatcorp / FFXIVQuickLauncher3195
      ClassIsland / ClassIsland2896
      Paving-Base / APK-Installer1712
      rogerfar / rdt-client1600
      Taiizor / Sucrose1583
      CHKZL / DDTV1496
      jxlpzqc / TMSpeech1495
      shuyu-labs / AntSK1329
      Otiel / BandcampDownloader1243
      Reaparr / Reaparr699
      crazysmile-PhD / downkyicore589
      chickensoft-games / GodotEnv586
      anobaka / Bakabase567
      K12f / BlueCatKoKo497
      Euterpe-org / App492
      HandyOrg / HandyWinGet396