#![allow(clippy::exit)]
extern crate cargo_fetcher as cf;
use anyhow::Context as _;
use std::{path::PathBuf, sync::Arc, time::Duration};
use tracing_subscriber::filter::LevelFilter;
use url::Url;
mod mirror;
mod sync;
#[derive(Clone)]
struct Dur(Duration);
impl std::str::FromStr for Dur {
type Err = clap::Error;
fn from_str(src: &str) -> Result<Self, Self::Err> {
let suffix_pos = src.find(char::is_alphabetic).unwrap_or(src.len());
let num: u64 = src[..suffix_pos]
.parse()
.map_err(|err| clap::Error::raw(clap::error::ErrorKind::ValueValidation, err))?;
let suffix = if suffix_pos == src.len() {
"s"
} else {
&src[suffix_pos..]
};
let duration = match suffix {
"s" | "S" => Duration::from_secs(num),
"m" | "M" => Duration::from_secs(num * 60),
"h" | "H" => Duration::from_secs(num * 60 * 60),
"d" | "D" => Duration::from_secs(num * 60 * 60 * 24),
s => {
return Err(clap::Error::raw(
clap::error::ErrorKind::ValueValidation,
format!("unknown duration suffix '{s}'"),
))
}
};
Ok(Dur(duration))
}
}
#[derive(clap::Subcommand)]
enum Command {
#[clap(name = "mirror")]
Mirror(mirror::Args),
#[clap(name = "sync")]
Sync(sync::Args),
}
#[derive(clap::Parser)]
#[clap(
author,
version,
about,
long_about = "cargo plugin to quickly fetch crate sources from cloud or local storage"
)]
struct Opts {
#[clap(short, long, env = "GOOGLE_APPLICATION_CREDENTIALS")]
credentials: Option<PathBuf>,
#[clap(short, long)]
url: Url,
#[clap(short, long, default_value = "Cargo.lock")]
lock_file: PathBuf,
#[clap(
short = 'L',
long,
default_value = "info",
long_help = "The log level for messages, only log messages at or above the level will be emitted.
Possible values:
* off
* error
* warn
* info (default)
* debug
* trace"
)]
log_level: LevelFilter,
#[clap(long)]
json: bool,
#[clap(short, long)]
include_index: bool,
#[clap(
short,
env = "CARGO_FETCHER_TIMEOUT",
default_value = "30s",
long_help = "The maximum duration of a single crate request
Times may be specified with no suffix (default seconds), or one of:
* (s)econds
* (m)inutes
* (h)ours
* (d)ays
"
)]
timeout: Dur,
#[clap(subcommand)]
cmd: Command,
}
fn init_backend(
loc: cf::CloudLocation<'_>,
_credentials: Option<PathBuf>,
_timeout: Duration,
) -> anyhow::Result<Arc<dyn cf::Backend + Sync + Send>> {
match loc {
#[cfg(feature = "gcs")]
cf::CloudLocation::Gcs(gcs) => {
let cred_path = _credentials.context("GCS credentials not specified")?;
let gcs = cf::backends::gcs::GcsBackend::new(gcs, &cred_path, _timeout)?;
Ok(Arc::new(gcs))
}
#[cfg(not(feature = "gcs"))]
cf::CloudLocation::Gcs(_) => anyhow::bail!("GCS backend not enabled"),
#[cfg(feature = "s3")]
cf::CloudLocation::S3(loc) => {
let make_bucket = loc.bucket == "testing" && loc.host.contains("localhost");
let s3 = cf::backends::s3::S3Backend::new(loc, _timeout)?;
if make_bucket {
s3.make_bucket().context("failed to create test bucket")?;
}
Ok(Arc::new(s3))
}
#[cfg(not(feature = "s3"))]
cf::CloudLocation::S3(_) => anyhow::bail!("S3 backend not enabled"),
#[cfg(feature = "fs")]
cf::CloudLocation::Fs(loc) => Ok(Arc::new(cf::backends::fs::FSBackend::new(loc)?)),
#[cfg(not(feature = "fs"))]
cf::CloudLocation::Fs(_) => anyhow::bail!("filesystem backend not enabled"),
#[cfg(feature = "blob")]
cf::CloudLocation::Blob(loc) => Ok(Arc::new(cf::backends::blob::BlobBackend::new(
loc, _timeout,
)?)),
#[cfg(not(feature = "blob"))]
cf::CloudLocation::Blob(_) => anyhow::bail!("blob backend not enabled"),
}
}
fn real_main() -> anyhow::Result<()> {
use clap::Parser;
let args = Opts::parse_from({
std::env::args().enumerate().filter_map(|(i, a)| {
if i == 1 && a == "fetcher" {
None
} else {
Some(a)
}
})
});
let mut env_filter = tracing_subscriber::EnvFilter::from_default_env();
env_filter = env_filter.add_directive(format!("cargo_fetcher={}", args.log_level).parse()?);
let subscriber = tracing_subscriber::FmtSubscriber::builder().with_env_filter(env_filter);
if args.json {
tracing::subscriber::set_global_default(subscriber.json().finish())
.context("failed to set default subscriber")?;
} else {
tracing::subscriber::set_global_default(subscriber.finish())
.context("failed to set default subscriber")?;
};
let cloud_location = cf::util::CloudLocationUrl::from_url(args.url.clone())?;
let location = cf::util::parse_cloud_location(&cloud_location)?;
let backend = init_backend(location, args.credentials, args.timeout.0)?;
let root_dir = if args.lock_file.is_relative() {
let mut root_dir = std::env::current_dir()
.context("unable to acquire current directory")?
.join(&args.lock_file);
root_dir.pop();
root_dir
} else {
let mut root_dir = args.lock_file.clone();
root_dir.pop();
root_dir
};
let cargo_root = cf::cargo::determine_cargo_root(Some(&root_dir))
.context("failed to determine $CARGO_HOME")?;
let registries = cf::read_cargo_config(cargo_root.clone(), root_dir)?;
let (krates, registries) = cf::cargo::read_lock_file(args.lock_file, registries)
.context("failed to get crates from lock file")?;
match args.cmd {
Command::Mirror(margs) => {
let ctx = cf::Ctx::new(None, backend, krates, registries)
.context("failed to create context")?;
mirror::cmd(ctx, args.include_index, margs)
}
Command::Sync(sargs) => {
let ctx = cf::Ctx::new(Some(cargo_root), backend, krates, registries)
.context("failed to create context")?;
sync::cmd(ctx, args.include_index, sargs)
}
}
}
fn main() {
match real_main() {
Ok(_) => {}
Err(e) => {
tracing::error!("{:#}", e);
std::process::exit(1);
}
}
}