use clap::{Arg, Command};
use color_eyre::Result;
use color_eyre::eyre;
use color_eyre::eyre::WrapErr;
use std::path::{Path, PathBuf};
use crate::lfs;
use crate::utils;
pub struct WgetOptions {
pub output_file: Option<String>,
pub prefix_dir: Option<String>,
pub urls: Vec<String>,
#[allow(dead_code)] pub quiet: bool,
}
fn normalize_url(url: &str) -> String {
if url.contains("://") {
url.to_string()
} else {
eprintln!("Prepended http:// to '{}'", url);
format!("http://{}", url)
}
}
pub fn parse_options(matches: &clap::ArgMatches) -> Result<WgetOptions> {
let output_file = matches.get_one::<String>("output").cloned();
let prefix_dir = matches.get_one::<String>("directory-prefix").cloned();
let quiet = matches.get_flag("quiet");
let urls: Vec<String> = matches.get_many::<String>("urls")
.map(|vals| vals.map(|u| normalize_url(u)).collect())
.unwrap_or_default();
if output_file.is_some() && urls.len() > 1 {
return Err(eyre::eyre!("wget: option '-O' can only be used with a single URL"));
}
Ok(WgetOptions {
output_file,
prefix_dir,
urls,
quiet,
})
}
pub fn command() -> Command {
Command::new("wget")
.about("Download files from the web")
.arg_required_else_help(true)
.arg(Arg::new("output")
.short('O')
.long("output-document")
.value_name("FILE")
.help("Write documents to FILE"))
.arg(Arg::new("directory-prefix")
.short('P')
.long("directory-prefix")
.value_name("DIR")
.help("Save files to DIR"))
.arg(Arg::new("quiet")
.short('q')
.long("quiet")
.help("Quiet mode (no output)")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("urls")
.required(true)
.num_args(1..)
.help("URLs to download"))
}
pub fn run(options: WgetOptions) -> Result<()> {
use crate::download::download_urls;
let download_results = download_urls(options.urls.to_vec());
let output_dir = prepare_output_directory(&options.output_file, &options.prefix_dir)?;
process_downloaded_files(&download_results, &options.urls, &options.output_file, &output_dir)?;
Ok(())
}
fn prepare_output_directory(output_file: &Option<String>, prefix_dir: &Option<String>) -> Result<PathBuf> {
if let Some(ref output) = output_file {
let output_path = Path::new(output);
if let Some(parent) = output_path.parent() {
if !parent.as_os_str().is_empty() {
lfs::create_dir_all(parent)?;
}
}
return Ok(output_path.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from(".")));
}
if let Some(ref prefix) = prefix_dir {
let dir = Path::new(prefix);
if !dir.exists() {
lfs::create_dir_all(dir)?;
}
if !dir.is_dir() {
return Err(eyre::eyre!("wget: '-P' must specify a directory, got: '{}'", prefix));
}
return Ok(dir.to_path_buf());
}
Ok(PathBuf::from("."))
}
fn determine_output_path(
cached_path: &Path,
output_file: &Option<String>,
output_dir: &Path,
) -> PathBuf {
if let Some(ref output) = output_file {
PathBuf::from(output)
} else {
let filename = cached_path.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "index.html".to_string());
output_dir.join(&filename)
}
}
fn link_downloaded_file(final_cached_path: &Path, output: &str) -> Result<()> {
let out_path = Path::new(output);
if let Some(parent) = out_path.parent() {
if !parent.as_os_str().is_empty() {
lfs::create_dir_all(parent)?;
}
}
utils::force_symlink_file_for_native(final_cached_path, out_path)
.with_context(|| {
format!(
"download: failed to create symlink from '{}' to '{}'",
out_path.display(),
final_cached_path.display()
)
})?;
Ok(())
}
fn process_downloaded_files(
download_results: &[Result<String>],
urls: &[String],
output_file: &Option<String>,
output_dir: &Path,
) -> Result<()> {
let mut errors = Vec::new();
for (i, result) in download_results.iter().enumerate() {
let final_cached_path = match result {
Ok(path) => std::path::PathBuf::from(path),
Err(e) => {
let url = urls.get(i).map(|s| s.as_str()).unwrap_or("unknown url");
errors.push(format!("wget: download failed for {}: {}", url, e));
continue;
}
};
if !final_cached_path.exists() {
errors.push(format!(
"wget: internal error, expected downloaded file not found at '{}'",
final_cached_path.display()
));
continue;
}
let output_path = determine_output_path(&final_cached_path, output_file, output_dir);
if let Err(e) = link_downloaded_file(&final_cached_path, &output_path.to_string_lossy()) {
errors.push(format!(
"wget: failed to move file from '{}' to '{}': {}",
final_cached_path.display(),
output_path.display(),
e
));
continue;
}
}
if !errors.is_empty() {
let error_count = errors.len();
let error_details = errors.join("\n");
return Err(eyre::eyre!(
"{} error(s) occurred:\n{}",
error_count,
error_details
));
}
Ok(())
}