gird 0.1.2

A command line tool and library for downloading release artifacts
Documentation
use std::path::PathBuf;

use clap::{Parser, Subcommand};
use gird::{github, 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,
    },
}

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);
        }
    }

    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(),
        )?,
    };

    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(())
}