main.rs
raw
#![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<String>,
/// 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<PathBuf>,
/// 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<dyn std::error::Error>> {
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(())
}