aboutsummaryrefslogtreecommitdiffstats
path: root/fatcat-cli/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'fatcat-cli/src/main.rs')
-rw-r--r--fatcat-cli/src/main.rs631
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(())
+}