Skip to main content

Crate fast_down_api

Crate fast_down_api 

Source
Expand description

§fast-down-api

GitHub last commit Test codecov Latest version Documentation License

A convenient, high-level wrapper around fast-down that turns the pull/push engine into a few lines of async code: spawn a download, drain progress events, resume after interruption, and cancel cooperatively.

  • Concurrent, resumable downloads powered by the fast-down engine (work-stealing, range requests).
  • Two layers of entry points: the fire-and-forget download / resume wrappers, and the lower-level plan / plan_resume pair that prefetches the remote and inspects the disk without writing a single byte — so you can preview the outcome and decide before committing.
  • Event stream: a single channel carries prefetch, disk allocation, per-worker progress, resume, rename, and lifecycle events. Every run ends with exactly one Event::Terminated(TerminationReason).
  • Cooperative cancellation: cancelling mid-flight preserves the .part / .fd files so you can resume later.
  • Configurable: threads, chunk size, write method (Mmap / Std), proxies, headers, retries, disk pre-allocation, and more via PartialConfig.

§Quick start

use fast_down_api::{create_cancellation_token, create_channel, download, Event, PartialConfig};
use std::path::PathBuf;
use url::Url;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 1. Channel for progress / lifecycle events, plus a cancellation token.
    let (tx, rx) = create_channel();
    let token = create_cancellation_token();

    // 2. Configure the download. Every field is optional; unset fields
    //    fall back to Config::default(). (save_dir is required at runtime.)
    let config = PartialConfig {
        save_dir: Some(PathBuf::from("./downloads")),
        overwrite: Some(true),
        threads: Some(16),
        ..Default::default()
    };

    // 3. Start the download. This spawns a detached task and returns at once.
    let url = Url::parse("https://example.com/large-file.bin")?;
    download(url, config, tx, token.clone());

    // 4. Drain events until the task finishes or is cancelled.
    while let Ok(event) = rx.recv().await {
        match event {
            // Aggregated progress on a fixed cadence (Config::progress_emit_gap).
            // Convenient for a progress bar — no need to re-accumulate ranges.
            Event::Progress(sample) => {
                // `downloaded`, `percent`, and `total` are pre-computed for you;
                // the fields below are equivalent to deriving them from `progress`.
                let written: u64 = sample.progress.iter().map(|r| r.end - r.start).sum();
                assert_eq!(written, sample.downloaded);
                let pct = if sample.total > 0 {
                    written * 100 / sample.total
                } else {
                    0
                };
                // `eta` is the estimated remaining time, or `None` until a rate
                // can be measured.
                let eta_str = sample
                    .eta
                    .map_or_else(|| "?".to_string(), |d| format!("{d:?}"));
                println!(
                    "progress: {pct}% ({:.1}%)  {written}/{} bytes  \
                     {} B/s (inst)  {} B/s (avg)  elapsed {:?}  eta {eta_str}",
                    sample.percent, sample.total, sample.bps, sample.avg_bps, sample.elapsed
                );
            }
            Event::PushProgress(p) => println!("wrote range: {p:?}"),
            Event::Renamed(path) => {
                println!("done -> {path:?}");
                break;
            }
            Event::RenameFailed(e) => {
                eprintln!("rename failed: {e}");
                break;
            }
            Event::ResumeError(e) => eprintln!("resume error: {e}"),
            Event::Allocating(size) => println!("pre-allocating {size} bytes on disk"),
            Event::AllocError(e) => eprintln!("pre-allocation failed (continuing): {e}"),
            Event::Terminated(reason) => {
                println!("terminated: {reason:?}");
                break;
            }
            _ => {}
        }
    }

    // 5. Draining `rx` above already waits for completion: the loop ends when the
    //    task drops its last sender, so no separate join handle is needed.
    Ok(())
}

§Resuming an interrupted download

resume targets the existing .part file. The url argument is optional: pass None to re-use the initial URL already recorded in the .fd state file, so you can resume purely from the .part path. If the download cannot be continued (no .fd state, no range support, or the remote file changed) it emits Event::ResumeError and does not silently restart — unlike download, which auto-resumes when it can and otherwise falls back to a fresh download. With url = None and no resolvable URL available it reports StateError::NoUrl.

use fast_down_api::{PartialConfig, create_cancellation_token, create_channel, resume};
use url::Url;

let url = Url::parse("https://example.com/large-file.bin")?;
let config = PartialConfig::default();
let (tx, _rx) = create_channel();
let token = create_cancellation_token();
resume(
    "./downloads/large-file.bin.part", // the .part file from a previous run
    Some(url),                          // pass None to reuse the URL stored in the .fd
    config,
    tx,
    token,
);

§Cancelling cooperatively

use fast_down_api::create_cancellation_token;

let token = create_cancellation_token();
token.cancel(); // stops fetching, keeps .part / .fd so you can resume later

§Two-phase planning (inspect before you commit)

plan and plan_resume do everything download / resume do except touch the disk: they prefetch the remote metadata, resolve the output path, and probe the .fd / .part pair left by a previous run. The returned DownloadPlan tells you what starting it would do — DownloadPlan::resume_outcome reports Resumable, Fresh, or Mismatch — and nothing is created until you call one of its start methods. Dropping the plan abandons the download with no side effects.

use fast_down_api::{
    PartialConfig, ResumeOutcome, create_cancellation_token, create_channel, plan,
};
use url::Url;

let url = Url::parse("https://example.com/large-file.bin")?;
let config = PartialConfig::default();
let (tx, _rx) = create_channel();
let token = create_cancellation_token();
let plan = plan(url, config.clone(), tx.clone(), token.clone()).await?;

match plan.resume_outcome() {
    ResumeOutcome::Resumable { .. } => println!("will continue from a previous run"),
    ResumeOutcome::Fresh => println!("will download the whole file"),
    ResumeOutcome::Mismatch(e) => println!("stale state: {e} (use start_forced_resume)"),
}

// Commit when you're ready. Each start method emits exactly one `Event::Terminated`.
plan.start().await;                 // resume if possible, else fresh (or refuse for plan_resume)
// plan.start_fresh().await;        // ignore any saved progress and re-download
// plan.start_forced_resume().await; // continue from a mismatched state when only identity changed

plan_resume takes a .part path instead of a URL and hard-refuses a Mismatch (sending Event::ResumeError + TerminationReason::Failed) rather than restarting — because the caller asked to continue one specific file, not to fetch it again. Pass a url to re-fetch the metadata, or None to reuse the URL recorded in the .fd.

§API overview

ItemPurpose
downloadStart a download; auto-resume when a valid .fd + .part exist, else fresh. Observe completion by draining the Rx from create_channel until it disconnects.
resumeResume a specific .part file; hard-error (Event::ResumeError) if it can’t. Completion is observed the same way, by draining Rx.
planPrefetch + probe the disk and return a DownloadPlan without writing anything; start it only when ready.
plan_resumeLike plan but targets a specific .part; refuses a Mismatch instead of re-downloading.
DownloadPlanA prepared, not-yet-started download. Inspect with resume_outcome, then call start / start_fresh / start_forced_resume.
create_channelCreate the (Tx, Rx) event channel.
create_cancellation_tokenCreate a CancellationToken for cooperative cancellation.
EventThe event enum delivered over the channel.
PartialConfigLayered, optional configuration for a download.
StateErrorErrors surfaced via Event::ResumeError.

§How resume works

During a download the engine periodically persists a .fd state file next to the .part partial file. That state records the byte ranges already written (downloaded_chunk) and the remote file identity (etag / last_modified / size). On the next run:

  1. download loads the .fd, validates it still matches the remote (size + identity), and — if the .part file is present — emits Event::Resumed and continues from the recorded offset.
  2. If validation fails (remote changed) or there is no .part, it starts fresh.
  3. resume applies the same checks but, instead of falling back, reports Event::ResumeError.

Cancellation leaves both files in place, so a later resume (or download) can pick up exactly where it stopped.

Every run — whether it completes, is cancelled, stops incomplete, or fails — ends with exactly one Event::Terminated(TerminationReason) as the last event on the channel, so draining Rx until Terminated is the reliable way to know a run has finished.

§License

MIT — see LICENSE.

Re-exports§

pub use fast_down;

Structs§

CancellationToken
A token which can be used to signal a cancellation request to one or more tasks.
Config
Configuration for a download task.
DownloadPlan
A prepared, not-yet-started download.
DownloadState
On-disk state for an in-progress download, backing resume support.
DownloadStateInner
Full (resolved) download state that is serialized into the .fd file.
PartialConfig
PartialDownloadStateInner
ProgressSample
Computed aggregate view of the current download progress, carried by Event::Progress.
ResumeInfo
已保存、可续传下载的只读快照,由 DownloadState::snapshot 产出。

Enums§

Event
Events emitted by a download run, consumed through the crossfire channel returned by crate::create_channel.
PlanError
Why a download plan could not be produced.
ResumeOutcome
What the disk probe found for the download a DownloadPlan describes.
StateError
Errors that can occur when an explicit resume cannot continue an interrupted download.
TerminationReason
How a download run ended, carried by Event::Terminated.
WriteMethod
File write method for downloaded data.

Functions§

create_cancellation_token
Create a new CancellationToken for cooperative cancellation of a download.
create_channel
Create a new unbounded event channel for receiving download progress events.
download
Spawn a detached background download task that resumes automatically when possible.
download_from_fd
Spawn a detached task that downloads from a .fd state file used as a download manifest.
peek_resume
Read a previously-saved download state from disk without contacting the network. The returned state is not validated against the current remote.
plan
Prepare a download without touching the filesystem.
plan_from_fd
Prepare a download driven by a .fd state file used as a download manifest.
plan_resume
Prepare a resume of an interrupted download from its .part file.
prefetch
Resolve the remote file’s metadata, retrying up to Config::retry_times.
resume
Spawn a detached task that resumes a previously interrupted download from its .part file.

Type Aliases§

Rx
Receiver half of the event channel, used to receive Events from the download task.
Tx
Sender half of the event channel, used to push Events from the download task.