blob: b182e6db71d12c8e2ed36aedaf824854a1d20a84 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! A tool for downloading data.
//! (Think `gsutil` with fewer options, but has support for Fuchsia specific
//! needs.)
use {
crate::config::{read_config, write_config, Configuration},
anyhow::{anyhow, bail, Context, Result},
args::{Args, CatArgs, ConfigArgs, CpArgs, ListArgs, SubCommand},
fuchsia_hyper::new_https_client,
gcs::{
client::ClientFactory,
gs_url::split_gs_url,
token_store::{auth_code_url, TokenStore},
},
home::home_dir,
std::{
fs::OpenOptions,
io::{self, BufRead, BufReader, Read, Write},
path::{Path, PathBuf},
},
};
mod args;
mod config;
const VERSION: &str = "0.3";
#[fuchsia_async::run_singlethreaded]
async fn main() -> Result<()> {
let args: Args = argh::from_env();
main_helper(args).await
}
/// A wrapper for the main program flow (allows for easier testing).
async fn main_helper(args: Args) -> Result<()> {
if args.version {
println!("Version {}", VERSION);
return Ok(());
}
let config_path = build_config_path()?;
match args.cmd {
Some(cmd) => match cmd {
SubCommand::Cat(args) => cat_command(&args, &config_path).await,
SubCommand::Config(args) => config_command(&args, &config_path).await,
SubCommand::Cp(args) => cp_command(&args, &config_path).await,
SubCommand::List(args) => list_command(&args, &config_path).await,
},
None => bail!("Please enter a subcommand such as help, cat, config, cp, or list."),
}
}
/// Helper function form common task of creating a client factory.
async fn create_client_factory(config_path: &Path) -> Result<ClientFactory> {
let config = read_config(&config_path).await?;
let refresh_token = config.gcs.require_refresh_token()?.to_string();
let token_store = TokenStore::new_with_auth(refresh_token, /*access_token=*/ None)?;
Ok(ClientFactory::new(token_store))
}
/// Determine where the configuration file should be.
fn build_config_path() -> Result<PathBuf> {
let home = home_dir().ok_or(anyhow!("Unable to find home directory."))?;
let dir = home.join(".fuchsia").join("fgsutil");
std::fs::create_dir_all(&dir)?;
Ok(dir.join("config.json"))
}
/// Handle the `cat` (concatenate) command and its args.
///
/// Loop over a list of gs URLs printing contents to stdout.
async fn cat_command(args: &CatArgs, config_path: &Path) -> Result<()> {
if args.gs_url.is_empty() {
bail!("One or more URLs are required. (e.g. gs://foo/bar)");
}
let factory = create_client_factory(config_path).await?;
let client = factory.create_client();
let stdout = io::stdout();
let mut writer = stdout.lock();
for split in args.gs_url.iter().map(split_gs_url) {
let (bucket, object) = split?;
client.write(bucket, object, &mut writer).await?;
}
Ok(())
}
/// Handle the `config` (configuration/set up) command and its args.
///
/// Prompt user for auth code and store the value in a configuration file.
async fn config_command(_args: &ConfigArgs, config_path: &Path) -> Result<()> {
let auth_code = get_auth_code()?;
let refresh_token = auth_code_to_refresh(&auth_code).await?;
let mut config = Configuration::default();
config.gcs.refresh_token = Some(refresh_token.to_owned());
write_config(&config, &config_path).await?;
Ok(())
}
/// Convert an authorization code to a refresh token.
async fn auth_code_to_refresh(auth_code: &str) -> Result<String> {
let token_store = TokenStore::new_with_code(&new_https_client(), auth_code).await?;
match token_store.refresh_token() {
Some(s) => Ok(s.to_string()),
None => bail!("auth_code_to_refresh failed"),
}
}
/// Handle the `cp` (copy) command and its args.
///
/// Very limited version of gsutil cp: download one file only.
async fn cp_command(args: &CpArgs, config_path: &Path) -> Result<()> {
let factory = create_client_factory(config_path).await?;
let client = factory.create_client();
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&args.destination)
.context("create output file")?;
let (bucket, object) = split_gs_url(&args.source)?;
client.write(bucket, object, &mut file).await
}
/// Handle the `ls` (list) command and its args.
async fn list_command(args: &ListArgs, config_path: &Path) -> Result<()> {
if args.gs_url.is_empty() {
bail!("One or more URLs are required. (e.g. gs://foo/bar)");
}
let factory = create_client_factory(config_path).await?;
let client = factory.create_client();
let stdout = io::stdout();
let mut writer = stdout.lock();
for split in args.gs_url.iter().map(split_gs_url) {
let (bucket, object_prefix) = split?;
for item in client.list(bucket, object_prefix).await? {
writeln!(&mut writer, "{}", item)?;
}
}
Ok(())
}
/// Ask the user to visit a URL and copy-paste the auth code provided.
///
/// A helper wrapper around get_auth_code_with() using stdin/stdout.
fn get_auth_code() -> Result<String> {
let stdout = io::stdout();
let mut output = stdout.lock();
let stdin = io::stdin();
let mut input = stdin.lock();
get_auth_code_with(&mut output, &mut input)
}
/// Ask the user to visit a URL and copy-paste the auth code provided.
fn get_auth_code_with<W, R>(writer: &mut W, reader: &mut R) -> Result<String>
where
W: Write,
R: Read,
{
writeln!(
writer,
"Please visit this site and copy the authentication code:\
\n\n{}\n\nPaste the auth_code (from website) here and press return: ",
auth_code_url(),
)?;
writer.flush().expect("flush auth code prompt");
let mut auth_code = String::new();
let mut buf_reader = BufReader::new(reader);
buf_reader.read_line(&mut auth_code).expect("Need an auth_code.");
Ok(auth_code)
}
#[cfg(test)]
mod tests {
use super::*;
async fn gcs_download() -> Result<()> {
let auth_code = get_auth_code()?;
let token_store =
TokenStore::new_with_code(&new_https_client(), &auth_code).await.expect("token_store");
let factory = ClientFactory::new(token_store);
let client = factory.create_client();
let bucket = "fuchsia-sdk";
let object = "development/LATEST_LINUX";
let res = client.stream(bucket, object).await.expect("client download");
assert_eq!(res.status(), 200, "res {:?}", res);
Ok(())
}
async fn https_download() -> Result<()> {
use hyper::{body::HttpBody, Body, Method, Request, Response, StatusCode};
println!("hyper_test");
let https_client = new_https_client();
let req = Request::builder().method(Method::GET).uri("https://www.google.com/");
let req = req.body(Body::from(""))?;
let mut res: Response<Body> = https_client.request(req).await?;
if res.status() == StatusCode::OK {
let stdout = io::stdout();
let mut handle = stdout.lock();
while let Some(next) = res.data().await {
let chunk = next?;
handle.write_all(&chunk)?;
}
}
Ok(())
}
#[test]
fn test_get_auth_code_with() {
let mut output: Vec<u8> = Vec::new();
let mut input = "fake_auth_code".as_bytes();
let auth_code = get_auth_code_with(&mut output, &mut input).expect("auth code");
assert_eq!(auth_code, "fake_auth_code");
}
/// This test relies on a local file which is not present on test bots, so
/// it is marked "ignore".
/// This can be run with `fx test fgsutil_test -- --ignored`.
#[ignore]
#[fuchsia_async::run_singlethreaded(test)]
async fn test_gcs_download() {
gcs_download().await.expect("gcs download");
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_https_download() {
https_download().await.expect("https download from google.com");
}
}