use crate::{path_or_stdin, BatchGrouper, BatchOp}; use anyhow::{anyhow, Context, Result}; use fatcat_cli::*; #[allow(unused_imports)] use log::{self, debug, info}; use std::io::Write; use std::path::PathBuf; use structopt::StructOpt; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use colored_json::to_colored_json_auto; #[derive(StructOpt)] #[structopt(rename_all = "kebab-case", about = "CLI interface to Fatcat API")] struct Opt { #[structopt( global = true, long = "--api-host", env = "FATCAT_API_HOST", default_value = "https://api.fatcat.wiki" )] api_host: String, /// API auth tokens can be generated from the account page in the fatcat.wiki web interface #[structopt( global = true, long = "--api-token", env = "FATCAT_API_AUTH_TOKEN", hide_env_values = true )] api_token: Option, #[structopt( global = true, long = "--search-host", env = "FATCAT_SEARCH_HOST", default_value = "https://search.fatcat.wiki" )] search_host: String, /// Log more messages. Pass multiple times for ever more verbosity /// /// By default, it'll only report errors. Passing `-v` one time also prints /// warnings, `-vv` enables info logging, `-vvv` debug, and `-vvvv` trace. #[structopt(global = true, long, short = "v", parse(from_occurrences))] verbose: i8, #[structopt(subcommand)] cmd: Command, } #[derive(StructOpt)] enum EditgroupCommand { /// Create a new editgroup Create { #[structopt(long, short)] description: String, }, /// Print editgroups for current user List { #[structopt(long = "--editor-id", short)] editor_id: Option, #[structopt(long, short = "-n", default_value = "20")] limit: i64, #[structopt(long)] json: bool, }, /// Print recent editgroups from any user which need review Reviewable { #[structopt(long, short = "-n", default_value = "20")] limit: i64, #[structopt(long)] json: bool, }, /// Accept (merge) a single editgroup Accept { #[structopt(env = "FATCAT_EDITGROUP", hide_env_values = true)] editgroup_id: String, }, /// Submit a single editgroup for review Submit { #[structopt(env = "FATCAT_EDITGROUP", hide_env_values = true)] editgroup_id: String, }, /// Un-submit a single editgroup (for more editing) Unsubmit { #[structopt(env = "FATCAT_EDITGROUP", hide_env_values = true)] editgroup_id: String, }, } #[derive(StructOpt)] enum BatchCommand { Create { entity_type: EntityType, #[structopt(long, default_value = "50")] batch_size: u64, #[structopt(long)] auto_accept: bool, }, Update { entity_type: EntityType, mutations: Vec, #[structopt(long, default_value = "50")] batch_size: u64, #[structopt(long)] auto_accept: bool, }, Delete { entity_type: EntityType, #[structopt(long, default_value = "50")] batch_size: u64, #[structopt(long)] auto_accept: bool, }, Download {}, } #[derive(StructOpt)] enum Command { Get { specifier: Specifier, #[structopt(long = "--expand")] expand: Option, #[structopt(long = "--hide")] hide: Option, #[structopt(long)] json: bool, #[structopt(long)] toml: bool, }, Create { entity_type: EntityType, /// Input file, "-" for stdin. #[structopt(long = "--file", short = "-f", parse(from_os_str))] input_path: Option, #[structopt( long = "--editgroup-id", short, env = "FATCAT_EDITGROUP", hide_env_values = true )] editgroup_id: String, }, Update { specifier: Specifier, /// Input file, "-" for stdin. #[structopt(long = "--file", short = "-f", parse(from_os_str))] input_path: Option, #[structopt( long = "--editgroup-id", short, env = "FATCAT_EDITGROUP", hide_env_values = true )] editgroup_id: String, mutations: Vec, }, Delete { specifier: Specifier, #[structopt( long = "--editgroup-id", short, env = "FATCAT_EDITGROUP", hide_env_values = true )] editgroup_id: String, }, Edit { specifier: Specifier, #[structopt( long = "--editgroup-id", short, env = "FATCAT_EDITGROUP", hide_env_values = true )] editgroup_id: String, #[structopt(long)] json: bool, #[allow(dead_code)] #[structopt(long)] toml: bool, #[structopt(long = "--editing-command", env = "EDITOR")] editing_command: String, }, Download { specifier: Specifier, }, History { specifier: Specifier, #[structopt(long, short = "-n", default_value = "100")] limit: u64, #[structopt(long)] json: bool, }, Search { entity_type: EntityType, terms: Vec, #[structopt(long = "--expand")] expand: Option, #[structopt(long = "--hide")] hide: Option, #[structopt(long, short = "-n", default_value = "20")] limit: i64, #[structopt(long = "--search-schema")] search_schema: bool, }, Editgroup { #[structopt(subcommand)] cmd: EditgroupCommand, }, Changelog { #[structopt(long, short = "-n", default_value = "20")] limit: i64, /* TODO: follow (streaming) mode for changelog #[structopt(long, short = "-f")] follow: bool, */ #[structopt(long)] json: bool, }, Batch { #[structopt(subcommand)] cmd: BatchCommand, /// Input file, "-" for stdin. #[structopt(long = "--file", short = "-f", parse(from_os_str))] input_path: Option, #[structopt(long)] limit: Option, }, Status { #[structopt(long)] json: bool, }, } fn main() -> Result<()> { let opt = Opt::from_args(); let log_level = match opt.verbose { std::i8::MIN..=-1 => "none", 0 => "error", 1 => "warn", 2 => "info", 3 => "debug", 4..=std::i8::MAX => "trace", }; // hyper logging is very verbose, so crank that down even if everything else is more verbose let log_filter = format!("{},hyper=error", log_level); env_logger::from_env(env_logger::Env::default().default_filter_or(log_filter)) .format_timestamp(None) .init(); debug!("Args parsed, starting up"); #[cfg(windows)] colored_json::enable_ansi_support(); if let Err(err) = run(opt) { // Be graceful about some errors if let Some(io_err) = err.root_cause().downcast_ref::() { if let std::io::ErrorKind::BrokenPipe = io_err.kind() { // presumably due to something like writing to stdout and piped to `head -n10` and // stdout was closed debug!("got BrokenPipe error, assuming stdout closed as expected and exiting with success"); std::process::exit(0); } } let mut color_stderr = StandardStream::stderr(if atty::is(atty::Stream::Stderr) { ColorChoice::Auto } else { ColorChoice::Never }); color_stderr.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?; eprintln!("Error: {:?}", err); color_stderr.set_color(&ColorSpec::new())?; std::process::exit(1); } Ok(()) } fn run(opt: Opt) -> Result<()> { let mut api_client = FatcatApiClient::new(opt.api_host.clone(), opt.api_token.clone())?; match opt.cmd { Command::Get { specifier, expand, hide, json, toml, } => { let result = specifier.get_from_api(&mut api_client, expand, hide)?; if toml { writeln!(&mut std::io::stdout(), "{}", result.to_toml_string()?)? } else if json || true { writeln!(&mut std::io::stdout(), "{}", to_colored_json_auto(&result.to_json_value()?)?)? } } Command::Create { entity_type, input_path, editgroup_id, } => { let json_str = read_entity_file(input_path)?; let ee = api_client.create_entity_from_json(entity_type, &json_str, editgroup_id)?; writeln!(&mut std::io::stdout(), "{}", to_colored_json_auto(&serde_json::to_value(&ee)?)?)? } Command::Update { specifier, input_path, editgroup_id, mutations, } => { let (json_str, exact_specifier): (String, Specifier) = match (&input_path, mutations.len()) { // input path or no mutations: read from path or stdin (Some(_), _) | (None, 0) => ( read_entity_file(input_path)?, specifier.into_entity_specifier(&mut api_client)?, ), // no input path *and* mutations: fetch from API (None, _) => { let mut entity = specifier.get_from_api(&mut api_client, None, None)?; entity.mutate(mutations)?; (entity.to_json_string()?, entity.specifier()) } }; let ee = api_client.update_entity_from_json(exact_specifier, &json_str, editgroup_id)?; writeln!(&mut std::io::stdout(), "{}", to_colored_json_auto(&serde_json::to_value(&ee)?)?)? } Command::Edit { specifier, editgroup_id, json, toml: _, editing_command, } => { let ee = edit_entity_locally( &mut api_client, specifier, editgroup_id, json, editing_command, )?; writeln!(&mut std::io::stdout(), "{}", to_colored_json_auto(&serde_json::to_value(&ee)?)?)? } Command::Changelog { limit, json } => { let resp = api_client .rt .block_on(api_client.api.get_changelog(Some(limit))) .context("fetch recent changelogs")?; match resp { fatcat_openapi::GetChangelogResponse::Success(change_list) => { print_changelog_entries(change_list, json)?; } other => { return Err(anyhow!("{:?}", other)) .with_context(|| format!("failed to fetch changelogs")) } } } Command::Batch { cmd: BatchCommand::Create { entity_type, batch_size, auto_accept, }, input_path, limit, } => { let input_path = path_or_stdin(input_path); let mut batch = BatchGrouper::new(entity_type, batch_size, limit, auto_accept); batch.run(&mut api_client, input_path, BatchOp::Create, None)?; } Command::Batch { cmd: BatchCommand::Update { entity_type, mutations, batch_size, auto_accept, }, input_path, limit, } => { let input_path = path_or_stdin(input_path); let mut batch = BatchGrouper::new(entity_type, batch_size, limit, auto_accept); batch.run( &mut api_client, input_path, BatchOp::Update, Some(mutations), )?; } Command::Batch { cmd: BatchCommand::Delete { entity_type, batch_size, auto_accept, }, input_path, limit, } => { let input_path = path_or_stdin(input_path); let mut batch = BatchGrouper::new(entity_type, batch_size, limit, auto_accept); batch.run(&mut api_client, input_path, BatchOp::Delete, None)?; } Command::Batch { cmd: BatchCommand::Download {}, input_path, limit, } => { let input_path = path_or_stdin(input_path); download_batch(input_path, limit)?; } Command::Download { specifier } => { // run lookups if necessary (inefficient) let specifier = match specifier { Specifier::ReleaseLookup(_, _) | Specifier::FileLookup(_, _) => { specifier.into_entity_specifier(&mut api_client)? } _ => specifier, }; let status = match specifier { Specifier::Release(ident) => { let result = api_client.rt.block_on(api_client.api.get_release( ident.clone(), Some("files".to_string()), Some("abstracts,refs".to_string()), ))?; let release_entity = match result { fatcat_openapi::GetReleaseResponse::FoundEntity(model) => Ok(model), resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", ident)), }?; download_release(&release_entity) } Specifier::File(ident) => { let result = api_client.rt.block_on(api_client.api.get_file( ident.clone(), None, None, ))?; let file_entity = match result { fatcat_openapi::GetFileResponse::FoundEntity(model) => Ok(model), resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", ident)), }?; download_file(&file_entity) } other => Err(anyhow!("Don't know how to download: {:?}", other)), }?; if let Some(detail) = status.details() { println!("{}: {}", status, detail); } else { println!("{}", status); } } Command::Search { entity_type, terms, limit, search_schema, expand, hide, } => { let limit: Option = match limit { l if l <= 0 => None, l => Some(l as u64), }; let results = fatcat_cli::crude_search(&opt.search_host, entity_type, limit, terms) .with_context(|| format!("searching for {:?}", entity_type))?; eprintln!("Got {} hits in {}ms", results.count, results.took_ms); for hit in results { let hit = hit?; match (search_schema, entity_type) { (true, _) => writeln!(&mut std::io::stdout(), "{}", hit.to_string())?, (false, EntityType::Release) => { let specifier = Specifier::Release(hit["ident"].as_str().unwrap().to_string()); let entity = specifier.get_from_api( &mut api_client, expand.clone(), hide.clone(), )?; writeln!(&mut std::io::stdout(), "{}", entity.to_json_string()?)? } (false, _) => unimplemented!("searching other entity types"), } } } Command::Delete { specifier, editgroup_id, } => { let result = api_client .delete_entity(specifier.clone(), editgroup_id) .with_context(|| format!("delete entity: {:?}", specifier))?; println!("{}", serde_json::to_string(&result)?); } Command::History { specifier, limit, json, } => { let specifier = specifier.into_entity_specifier(&mut api_client)?; let history_entries = specifier.get_history(&mut api_client, Some(limit))?; print_entity_histories(history_entries, json)?; } Command::Editgroup { cmd: EditgroupCommand::List { editor_id, limit, json, }, } => { let editor_id = match editor_id.or(api_client.editor_id) { Some(eid) => eid, None => return Err(anyhow!("require either working auth token or --editor-id")), }; let result = api_client .rt .block_on(api_client.api.get_editor_editgroups( editor_id.clone(), Some(limit), None, None, )) .context("fetch editgroups")?; match result { fatcat_openapi::GetEditorEditgroupsResponse::Found(eg_list) => { print_editgroups(eg_list, json)?; } other => { return Err(anyhow!("{:?}", other)).with_context(|| { format!("failed to fetch editgroups for editor_{}", editor_id) }) } } } Command::Editgroup { cmd: EditgroupCommand::Reviewable { limit, json }, } => { let result = api_client .rt .block_on(api_client.api.get_editgroups_reviewable( Some("editors".to_string()), Some(limit), None, None, )) .context("fetch reviewable editgroups")?; match result { fatcat_openapi::GetEditgroupsReviewableResponse::Found(eg_list) => { print_editgroups(eg_list, json)?; } other => { return Err(anyhow!("{:?}", other)) .context("failed to fetch reviewable editgroups") } } } Command::Editgroup { cmd: EditgroupCommand::Create { description }, } => { let eg = api_client.create_editgroup(Some(description))?; writeln!(&mut std::io::stdout(), "{}", to_colored_json_auto(&serde_json::to_value(&eg)?)?)? } Command::Editgroup { cmd: EditgroupCommand::Accept { editgroup_id }, } => { let msg = api_client.accept_editgroup(editgroup_id.clone())?; writeln!(&mut std::io::stdout(), "{}", to_colored_json_auto(&serde_json::to_value(&msg)?)?)? } Command::Editgroup { cmd: EditgroupCommand::Submit { editgroup_id }, } => { let msg = api_client.update_editgroup_submit(editgroup_id, true)?; writeln!(&mut std::io::stdout(), "{}", to_colored_json_auto(&serde_json::to_value(&msg)?)?)? } Command::Editgroup { cmd: EditgroupCommand::Unsubmit { editgroup_id }, } => { let msg = api_client.update_editgroup_submit(editgroup_id, false)?; writeln!(&mut std::io::stdout(), "{}", to_colored_json_auto(&serde_json::to_value(&msg)?)?)? } Command::Status { json } => { let status = ClientStatus::generate(&mut api_client)?; if json { println!("{}", serde_json::to_string(&status)?) } else { status.pretty_print()?; } } } Ok(()) }