use cargo_fetcher::Krate;
use failure::{format_err, Error, ResultExt};
use log::{debug, error};
use reqwest::Client;
use std::path::PathBuf;
use structopt::StructOpt;
use tame_gcs::BucketName;
mod mirror;
mod sync;
#[derive(StructOpt)]
enum Command {
#[structopt(name = "mirror")]
Mirror(mirror::Args),
#[structopt(name = "sync")]
Sync(sync::Args),
}
fn parse_level(s: &str) -> Result<log::LevelFilter, Error> {
s.parse::<log::LevelFilter>()
.map_err(|_| format_err!("failed to parse level '{}'", s))
}
#[derive(StructOpt)]
struct Opts {
#[structopt(short, long, parse(from_os_str))]
credentials: Option<PathBuf>,
#[structopt(short = "g", long = "gcs")]
gcs_url: String,
#[structopt(short, long, parse(from_os_str))]
lock_file: PathBuf,
#[structopt(
short = "L",
long = "log-level",
default_value = "info",
parse(try_from_str = "parse_level"),
long_help = "The log level for messages, only log messages at or above the level will be emitted.
Possible values:
* off
* critical
* error
* warning
* info
* debug
* trace"
)]
log_level: log::LevelFilter,
#[structopt(subcommand)]
cmd: Command,
}
fn gs_url_to_bucket_and_prefix(url: &str) -> Result<(BucketName<'_>, &str), Error> {
use std::convert::TryFrom;
let no_scheme = url.trim_start_matches("gs://");
let mut split = no_scheme.splitn(2, '/');
let bucket = split
.next()
.ok_or_else(|| failure::err_msg("unknown bucket"))?;
let bucket = BucketName::try_from(bucket)?;
let prefix = split.next().unwrap_or_default();
Ok((bucket, prefix))
}
fn acquire_token(cred_path: PathBuf) -> Result<tame_oauth::Token, Error> {
use tame_gcs::http;
use tame_oauth::gcp;
let svc_account_info =
gcp::ServiceAccountInfo::deserialize(std::fs::read_to_string(&cred_path)?)
.context("failed to deserilize service account")?;
let svc_account_access = gcp::ServiceAccountAccess::new(svc_account_info)?;
let token = match svc_account_access.get_token(&[tame_gcs::Scopes::ReadWrite])? {
gcp::TokenOrRequest::Request {
request,
scope_hash,
..
} => {
let (parts, body) = request.into_parts();
let client = reqwest::Client::new();
let uri = parts.uri.to_string();
let builder = match parts.method {
http::Method::GET => client.get(&uri),
http::Method::POST => client.post(&uri),
http::Method::DELETE => client.delete(&uri),
http::Method::PUT => client.put(&uri),
method => unimplemented!("{} not implemented", method),
};
let req = builder
.headers(parts.headers)
.body(reqwest::Body::new(std::io::Cursor::new(body)))
.build()?;
let mut res = client.execute(req)?;
let response = cargo_fetcher::convert_response(&mut res)?;
svc_account_access.parse_token_response(scope_hash, response)?
}
_ => unreachable!(),
};
Ok(token)
}
pub struct Context<'a> {
client: Client,
gcs_bucket: BucketName<'a>,
prefix: &'a str,
krates: &'a [Krate],
}
fn real_main() -> Result<(), Error> {
use reqwest::header;
use std::convert::TryInto;
let args = Opts::from_args();
env_logger::builder().filter_level(args.log_level).init();
let (bucket, prefix) =
gs_url_to_bucket_and_prefix(&args.gcs_url).context("gs:// url is invalid")?;
let cred_path = args
.credentials
.or_else(|| std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS").map(PathBuf::from))
.ok_or_else(|| format_err!("credentials not specified"))?;
debug!("using credentials in {}", cred_path.display());
let token = acquire_token(cred_path)?;
let krates = cargo_fetcher::gather(args.lock_file)?;
let mut hm = header::HeaderMap::new();
hm.insert(header::AUTHORIZATION, token.try_into()?);
let client = Client::builder().default_headers(hm).gzip(false).build()?;
let ctx = Context {
client,
gcs_bucket: bucket,
prefix,
krates: &krates[..],
};
match args.cmd {
Command::Mirror(args) => mirror::cmd(ctx, args),
Command::Sync(args) => sync::cmd(ctx, args),
}
}
fn main() {
match real_main() {
Ok(_) => {}
Err(e) => {
error!("{}", e);
std::process::exit(1);
}
}
}