takanawa-cli 0.4.2

Command-line downloader using the Takanawa resumable range download engine
use std::path::PathBuf;

use takanawa_core::{HashConfig, HashKind, Result, TakanawaError};
use takanawa_http::{
    DEFAULT_MAX_IO, DownloadConfig, DownloadEngine, RetryConfig, TimeoutConfig,
    download_to_completion,
};

#[tokio::main]
async fn main() {
    if let Err(err) = run().await {
        eprintln!("error: {err}");
        std::process::exit(1);
    }
}

async fn run() -> Result<()> {
    let args = std::env::args().skip(1).collect::<Vec<_>>();
    if !(2..=4).contains(&args.len()) {
        return Err(TakanawaError::InvalidConfig(
            "usage: takanawa-cli <url> <target-path> [hash-kind] [hash-hex]\n       takanawa-cli <url> <target-path> [sha256-hex]"
                .to_owned(),
        ));
    }

    let hash = parse_hash_args(&args[2..])?;

    let engine = DownloadEngine::new(DEFAULT_MAX_IO)?;
    let snapshot = download_to_completion(
        engine,
        DownloadConfig {
            url: args[0].clone(),
            target_path: PathBuf::from(&args[1]),
            chunk_size: 0,
            parallelism: 0,
            max_parallel_chunks: 0,
            retry: RetryConfig::default(),
            timeout: TimeoutConfig::default(),
            bytes_per_second_limit: 0,
            hash,
        },
    )
    .await?;

    println!(
        "completed: {} bytes in {} chunks",
        snapshot.content_len, snapshot.chunk_count
    );
    Ok(())
}

fn parse_hash_args(args: &[String]) -> Result<HashConfig> {
    match args {
        [] => Ok(HashConfig::None),
        [sha256_hex] => parse_hash(HashKind::Sha256, sha256_hex),
        [kind, hash_hex] => parse_hash(parse_hash_kind(kind)?, hash_hex),
        _ => Err(TakanawaError::InvalidConfig(
            "expected [hash-kind] [hash-hex]".to_owned(),
        )),
    }
}

fn parse_hash_kind(value: &str) -> Result<HashKind> {
    match value.to_ascii_lowercase().replace('-', "").as_str() {
        "sha1" => Ok(HashKind::Sha1),
        "sha256" => Ok(HashKind::Sha256),
        "sha512" => Ok(HashKind::Sha512),
        "md5" => Ok(HashKind::Md5),
        "crc32" => Ok(HashKind::Crc32),
        _ => Err(TakanawaError::InvalidConfig(format!(
            "unsupported hash kind {value}; expected sha1, sha256, sha512, md5, or crc32"
        ))),
    }
}

fn parse_hash(kind: HashKind, value: &str) -> Result<HashConfig> {
    let bytes = hex::decode(value).map_err(|err| {
        TakanawaError::InvalidConfig(format!("invalid {} hex: {err}", kind.name()))
    })?;
    HashConfig::from_expected_bytes(kind, &bytes).ok_or_else(|| {
        TakanawaError::InvalidConfig(format!(
            "{} must be {} bytes",
            kind.name(),
            kind.expected_len()
        ))
    })
}