diff options
author | Bryan Newbold <bnewbold@archive.org> | 2021-02-09 18:57:00 -0800 |
---|---|---|
committer | Bryan Newbold <bnewbold@archive.org> | 2021-02-09 18:57:00 -0800 |
commit | 19c582a3cf1c42e9c75170650ccd141eda903479 (patch) | |
tree | 0738781689e4c12308016f184cb6eb02af1716a6 /fatcat-cli/src/main.rs | |
parent | bab3fb9fdcc921e1bb8a81e0f2b4e12558d2dde7 (diff) | |
download | fatcat-cli-19c582a3cf1c42e9c75170650ccd141eda903479.tar.gz fatcat-cli-19c582a3cf1c42e9c75170650ccd141eda903479.zip |
move source code to top-level directory
Diffstat (limited to 'fatcat-cli/src/main.rs')
-rw-r--r-- | fatcat-cli/src/main.rs | 631 |
1 files changed, 631 insertions, 0 deletions
diff --git a/fatcat-cli/src/main.rs b/fatcat-cli/src/main.rs new file mode 100644 index 0000000..3b0d382 --- /dev/null +++ b/fatcat-cli/src/main.rs @@ -0,0 +1,631 @@ +use crate::{path_or_stdin, BatchGrouper, BatchOp}; +use anyhow::{anyhow, Context, Result}; +use fatcat_cli::ApiModelSer; +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}; + +#[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<String>, + + #[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<String>, + + #[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<Mutation>, + + #[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<String>, + + #[structopt(long = "--hide")] + hide: Option<String>, + + #[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<PathBuf>, + + #[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<PathBuf>, + + #[structopt( + long = "--editgroup-id", + short, + env = "FATCAT_EDITGROUP", + hide_env_values = true + )] + editgroup_id: String, + + mutations: Vec<Mutation>, + }, + 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<String>, + + #[structopt(long = "--expand")] + expand: Option<String>, + + #[structopt(long = "--hide")] + hide: Option<String>, + + #[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<PathBuf>, + + #[structopt(long)] + limit: Option<u64>, + }, + 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"); + + if let Err(err) = run(opt) { + // Be graceful about some errors + if let Some(io_err) = err.root_cause().downcast_ref::<std::io::Error>() { + 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(), "{}", result.to_json_string()?)? + } + } + 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)?; + println!("{}", serde_json::to_string(&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)?; + println!("{}", serde_json::to_string(&ee)?); + } + Command::Edit { + specifier, + editgroup_id, + json, + toml: _, + editing_command, + } => { + let ee = edit_entity_locally( + &mut api_client, + specifier, + editgroup_id, + json, + editing_command, + )?; + println!("{}", serde_json::to_string(&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<u64> = 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))?; + println!("{}", serde_json::to_string(&eg)?) + } + Command::Editgroup { + cmd: EditgroupCommand::Accept { editgroup_id }, + } => { + let msg = api_client.accept_editgroup(editgroup_id.clone())?; + println!("{}", serde_json::to_string(&msg)?); + } + Command::Editgroup { + cmd: EditgroupCommand::Submit { editgroup_id }, + } => { + let eg = api_client.update_editgroup_submit(editgroup_id, true)?; + println!("{}", eg.to_json_string()?); + } + Command::Editgroup { + cmd: EditgroupCommand::Unsubmit { editgroup_id }, + } => { + let eg = api_client.update_editgroup_submit(editgroup_id, false)?; + println!("{}", eg.to_json_string()?); + } + Command::Status { json } => { + let status = ClientStatus::generate(&mut api_client)?; + if json { + println!("{}", serde_json::to_string(&status)?) + } else { + status.pretty_print()?; + } + } + } + Ok(()) +} |