use clap::{Parser, Subcommand, ValueEnum};
use color_eyre::{Result, eyre::ContextCompat};
use crossterm::terminal;
use reqwest::header::{HeaderMap, HeaderName};
use std::{path::PathBuf, str::FromStr, time::Duration};
#[derive(Debug, Clone, ValueEnum)]
pub enum WriteMethod {
Mmap,
Std,
}
#[derive(Parser, Debug)]
#[command(name = "fast-down")]
#[command(author, about)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Parser, Debug)]
#[command(name = "fast-down")]
#[command(author, about)]
struct CliDefault {
#[command(flatten)]
cmd: DownloadCli,
}
#[derive(Subcommand, Debug)]
#[allow(clippy::large_enum_variant)]
enum Commands {
Download(DownloadCli),
List(ListCli),
}
#[derive(clap::Args, Debug)]
struct DownloadCli {
#[arg(required = true)]
url: String,
#[arg(short, long)]
force: bool,
#[arg(long)]
no_resume: bool,
#[arg(short = 'd', long = "dir", default_value = ".")]
save_folder: PathBuf,
#[arg(short, long, default_value_t = 32)]
threads: usize,
#[arg(short = 'o', long = "out")]
file_name: Option<String>,
#[arg(short, long)]
proxy: Option<String>,
#[arg(short = 'H', long = "header", value_name = "Key: Value")]
headers: Vec<String>,
#[arg(long, default_value_t = 8 * 1024)]
chunk_window: u64,
#[arg(long, default_value_t = 1024 * 1024)]
min_chunk_size: u64,
#[arg(long, default_value_t = 8 * 1024 * 1024)]
write_buffer_size: usize,
#[arg(long, default_value_t = 10240)]
write_queue_cap: usize,
#[arg(long)]
progress_width: Option<u16>,
#[arg(long, default_value_t = 500)]
retry_gap: u64,
#[arg(long, default_value_t = 200)]
repaint_gap: u64,
#[arg(long, default_value_t = 5000)]
pull_timeout: u64,
#[arg(long)]
browser: bool,
#[arg(short, long)]
yes: bool,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
accept_invalid_certs: bool,
#[arg(long)]
accept_invalid_hostnames: bool,
#[arg(short, long)]
interface: bool,
#[arg(long = "ip", value_name = "网卡的 ip 地址")]
ips: Vec<String>,
#[arg(long, default_value_t = 3)]
max_speculative: usize,
#[arg(long, default_value = "mmap")]
write_method: WriteMethod,
#[arg(long)]
pre_alloc: bool,
}
#[derive(clap::Args, Debug)]
struct ListCli {
#[arg(short, long)]
details: bool,
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Args {
Download(DownloadArgs),
List(ListArgs),
}
#[derive(Debug, Clone)]
pub struct DownloadArgs {
pub url: String,
pub force: bool,
pub resume: bool,
pub save_folder: PathBuf,
pub threads: usize,
pub file_name: Option<String>,
pub proxy: Option<String>,
pub headers: HeaderMap,
pub chunk_window: u64,
pub min_chunk_size: u64,
pub write_buffer_size: usize,
pub write_queue_cap: usize,
pub repaint_gap: Duration,
pub progress_width: u16,
pub retry_gap: Duration,
pub pull_timeout: Duration,
pub browser: bool,
pub yes: bool,
pub verbose: bool,
pub accept_invalid_certs: bool,
pub accept_invalid_hostnames: bool,
pub interface: bool,
pub ips: Vec<String>,
pub max_speculative: usize,
pub write_method: WriteMethod,
pub pre_alloc: bool,
}
#[derive(Debug, Clone)]
pub struct ListArgs {
pub details: bool,
}
impl Args {
pub fn parse() -> Result<Args> {
match Cli::try_parse().or_else(|err| match err.kind() {
clap::error::ErrorKind::InvalidSubcommand | clap::error::ErrorKind::UnknownArgument => {
CliDefault::try_parse().map(|cli_default| Cli {
command: Commands::Download(cli_default.cmd),
})
}
_ => Err(err),
}) {
Ok(cli) => match cli.command {
Commands::Download(cli) => {
let mut args = DownloadArgs {
url: cli.url,
force: cli.force,
resume: !cli.no_resume,
save_folder: cli.save_folder,
threads: cli.threads,
file_name: cli.file_name,
proxy: cli.proxy,
headers: HeaderMap::new(),
chunk_window: cli.chunk_window,
min_chunk_size: cli.min_chunk_size,
write_buffer_size: cli.write_buffer_size,
write_queue_cap: cli.write_queue_cap,
progress_width: terminal::size()
.ok()
.and_then(|s| s.0.checked_sub(36))
.unwrap_or(50),
retry_gap: Duration::from_millis(cli.retry_gap),
repaint_gap: Duration::from_millis(cli.repaint_gap),
pull_timeout: Duration::from_millis(cli.pull_timeout),
browser: cli.browser,
yes: cli.yes,
verbose: cli.verbose,
accept_invalid_certs: cli.accept_invalid_certs,
accept_invalid_hostnames: cli.accept_invalid_hostnames,
interface: cli.interface,
ips: cli.ips,
max_speculative: cli.max_speculative,
write_method: cli.write_method,
pre_alloc: cli.pre_alloc,
};
for header in cli.headers {
let mut parts = header.splitn(2, ':').map(|t| t.trim());
let name = parts
.next()
.with_context(|| format!("请求头格式错误: {header}"))?;
let value = parts
.next()
.with_context(|| format!("请求头格式错误: {header}"))?;
args.headers
.insert(HeaderName::from_str(name)?, value.parse()?);
}
Ok(Args::Download(args))
}
Commands::List(cli) => Ok(Args::List(ListArgs {
details: cli.details,
})),
},
Err(err) => err.exit(),
}
}
}