#![forbid(unsafe_code)] use std::path::PathBuf; use clap::{Parser, Subcommand}; use gird::{github, gitlab, Error}; use ureq::Response; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Config { /// A file name substring which identifies the correct artifact to /// download. It should uniquely identify an artifact, or match a /// group of artifacts any of which is acceptable, or be further /// narrowed by the --exclude option. artifact: String, /// A substring which will prevent artifacts whose file names /// match it from being selected for downloading. Can be specified /// multiple times to exclude multiple substrings. #[arg(short, long)] exclude: Vec, /// The file or directory to store the downloaded file in. /// /// If not provided, the file will be stored with its natural name /// in the current working directory. /// /// If a directory is specified, the file will be stored with its /// natural name in that directory. /// /// If a file name is specified, the file will be stored with that /// name. #[arg(short, long)] output: Option, /// Identifies the specific source from which to download the /// artifact. Source-specific options must come after this. #[command(subcommand)] source: Source, } #[derive(Subcommand, Debug)] enum Source { /// Download a file from the most recent GitHub release for a given repository. Github { /// The username which owns the target repository user: String, /// The name of the target repository repository: String, }, /// Download a file from the most recent GitLab release for a given repository. Gitlab { /// The domain name where the GitLab software is running. #[arg(long, default_value = "gitlab.com")] host: String, /// The username which owns the target repository user: String, /// The name of the target repository repository: String, }, } fn default_filename(response: &Response, artifact: &str) -> String { if let Some(dispostition) = response.header("Content-Disposition") { if let Some((_, fname)) = dispostition.split_once("filename=") { return String::from(fname.trim_matches('"')); } } let url = response.get_url().to_string(); let path = if let Some((before, _)) = response.get_url().split_once("?") { before } else { url.as_str() }; match path.rsplit_once('/') { Some((_, after)) if !after.is_empty() => after.to_string(), _ => match response.header("Content-Type") { // In practice, almost all gzipped artifacts are tarred as // well. The same goes for other compressors which are not // also archivers. Some(ct) if ct == "application/gzip" => format!("{}.tar.gz", artifact), Some(ct) if ct == "application/x-bzip" => format!("{}.tar.bz", artifact), Some(ct) if ct == "application/x-bzip2" => format!("{}.tar.bz2", artifact), // These ones are also archivers, so they are rarely used // to compress tar files Some(ct) if ct == "application/zip" => format!("{}.zip", artifact), Some(ct) if ct == "application/vnd.rar" => format!("{}.rar", artifact), Some(ct) if ct == "application/x-7z-compressed" => format!("{}.7z", artifact), Some(ct) if ct == "application/java-archive" => format!("{}.jar", artifact), _ => format!("{}.bin", artifact), }, } } fn main() -> Result<(), Box> { let config = Config::parse(); let response = match config.source { Source::Github { user, repository } => github::download_release( config.artifact.as_str(), config.exclude.iter(), user.as_str(), repository.as_str(), )?, Source::Gitlab { host, user, repository, } => gitlab::download_release( config.artifact.as_str(), config.exclude.iter(), host.as_str(), user.as_str(), repository.as_str(), )?, }; let mut output = config.output.unwrap_or_else(|| { std::env::current_dir().expect("Cannot determine current working directory") }); if output.is_dir() { output.push(default_filename(&response, &config.artifact)); } std::fs::create_dir_all( output .parent() .ok_or_else(|| Error::Misc("Unable to determine the output directory".into()))?, )?; let mut outfile = std::fs::File::create(&output)?; std::io::copy(&mut response.into_reader(), &mut outfile)?; println!("Saved {}", output.to_string_lossy()); Ok(()) }