From 0cbbd85d64b7de6ba89a9c39897e363f6342ee03 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Fri, 5 Feb 2021 21:05:30 -0800 Subject: lots more progress on batch commands (and more) --- rust/fatcat-cli/src/api.rs | 34 +++++++ rust/fatcat-cli/src/commands.rs | 189 ++++++++++++++++++++++++++++++++++++-- rust/fatcat-cli/src/download.rs | 192 +++++++++++++++++++++++++++++++++------ rust/fatcat-cli/src/entities.rs | 34 ++++++- rust/fatcat-cli/src/lib.rs | 19 +++- rust/fatcat-cli/src/main.rs | 184 ++++++++++++++----------------------- rust/fatcat-cli/src/specifier.rs | 40 +++++--- 7 files changed, 520 insertions(+), 172 deletions(-) diff --git a/rust/fatcat-cli/src/api.rs b/rust/fatcat-cli/src/api.rs index cc6fa6a..2463aab 100644 --- a/rust/fatcat-cli/src/api.rs +++ b/rust/fatcat-cli/src/api.rs @@ -403,4 +403,38 @@ impl FatcatApiClient { } .with_context(|| format!("failed to update {:?}", specifier)) } + + pub fn create_editgroup(&mut self, description: Option) -> Result { + let mut eg = models::Editgroup::new(); + eg.description = description; + eg.extra = Some({ + let mut extra = std::collections::HashMap::new(); + extra.insert( + "agent".to_string(), + serde_json::Value::String("fatcat-cli".to_string()), + // TODO: version? + ); + extra + }); + let result = self.rt.block_on(self.api.create_editgroup(eg))?; + match result { + fatcat_openapi::CreateEditgroupResponse::SuccessfullyCreated(eg) => Ok(eg), + other => Err(anyhow!("{:?}", other)).context("failed to create editgroup"), + } + } + + pub fn accept_editgroup(&mut self, editgroup_id: String) -> Result { + let result = self + .rt + .block_on(self.api.accept_editgroup(editgroup_id.clone())) + .context("accept editgroup")?; + match result { + fatcat_openapi::AcceptEditgroupResponse::MergedSuccessfully(msg) => Ok(msg), + other => Err(anyhow!( + "failed to accept editgroup {}: {:?}", + editgroup_id, + other + )), + } + } } diff --git a/rust/fatcat-cli/src/commands.rs b/rust/fatcat-cli/src/commands.rs index 30fa0c4..15bfc81 100644 --- a/rust/fatcat-cli/src/commands.rs +++ b/rust/fatcat-cli/src/commands.rs @@ -3,14 +3,17 @@ use chrono_humanize::HumanTime; use fatcat_openapi::models; #[allow(unused_imports)] use log::{self, debug, info}; -use std::io::{Write, BufRead}; +use std::convert::TryInto; +use std::fs::File; +use std::io::{self, BufRead, Write}; +use std::path::PathBuf; use tabwriter::TabWriter; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; -use crate::api::FatcatApiClient; -//use crate::download::download_file; -use crate::entities::{ApiEntityModel, ApiModelIdent, ApiModelSer, Mutation, read_entity_file}; -use crate::specifier::Specifier; +use crate::{ + entity_model_from_json_str, read_entity_file, ApiModelSer, EntityType, FatcatApiClient, + Mutation, Specifier, +}; // Want to show: // - whether api_token found @@ -271,7 +274,13 @@ pub fn print_entity_histories( Ok(()) } -pub fn edit_entity_locally(api_client: &mut FatcatApiClient, specifier: Specifier, editgroup_id: String, json: bool, editing_command: String) -> Result { +pub fn edit_entity_locally( + api_client: &mut FatcatApiClient, + specifier: Specifier, + editgroup_id: String, + json: bool, + editing_command: String, +) -> Result { // TODO: fetch editgroup, check if this entity is already being updated in it. If so, // need to fetch that revision, do the edit, parse that synatx is good, then delete the // existing edit and update with the new one. @@ -312,3 +321,171 @@ pub fn edit_entity_locally(api_client: &mut FatcatApiClient, specifier: Specifi .context("updating after edit")?; Ok(ee) } + +pub enum BatchOp { + Create, + Update, + Delete, +} + +pub struct BatchGrouper { + entity_type: EntityType, + batch_size: u64, + limit: Option, + auto_accept: bool, + editgroup_description: String, + current_count: u64, + current_editgroup_id: Option, + total_count: u64, +} + +// Note: should be possible to add support for the single-call batch create endpoint by storing +// edits in a batch within this object. Might need to change control flow a bit. This optimization +// was mostly intended for bootstrapping of tens of thousands of entities, so not including for +// now. +impl BatchGrouper { + pub fn new( + entity_type: EntityType, + batch_size: u64, + limit: Option, + auto_accept: bool, + ) -> Self { + let editgroup_description = "part of a fatcat-cli batch operation".to_string(); + BatchGrouper { + entity_type, + batch_size, + limit, + auto_accept, + editgroup_description, + current_count: 0, + current_editgroup_id: None, + total_count: 0, + } + } + + pub fn run( + &mut self, + api_client: &mut FatcatApiClient, + input_path: Option, + op: BatchOp, + mutations: Option>, + ) -> Result<()> { + match input_path { + None => { + let stdin = io::stdin(); + let stdin_lock = stdin.lock(); + let lines = stdin_lock.lines(); + for line in lines { + let json_str = line?; + match op { + BatchOp::Create => self.push_create(api_client, &json_str)?, + BatchOp::Update => self.push_update( + api_client, + &json_str, + mutations.clone().unwrap_or(vec![]), + )?, + BatchOp::Delete => self.push_delete(api_client, &json_str)?, + }; + if let Some(limit) = self.limit { + if self.total_count + self.current_count >= limit { + break; + } + } + } + } + Some(path) => { + let input_file = File::open(path)?; + let buffered = io::BufReader::new(input_file); + let lines = buffered.lines(); + for line in lines { + let json_str = line?; + match op { + BatchOp::Create => self.push_create(api_client, &json_str)?, + BatchOp::Update => self.push_update( + api_client, + &json_str, + mutations.clone().unwrap_or(vec![]), + )?, + BatchOp::Delete => self.push_delete(api_client, &json_str)?, + }; + if let Some(limit) = self.limit { + if self.total_count + self.current_count >= limit { + break; + } + } + } + } + } + self.flush(api_client)?; + Ok(()) + } + + pub fn push_create( + &mut self, + api_client: &mut FatcatApiClient, + json_str: &str, + ) -> Result { + let editgroup_id = self.increment_editgroup(api_client)?; + api_client.create_entity_from_json(self.entity_type, json_str, editgroup_id) + } + + pub fn push_update( + &mut self, + api_client: &mut FatcatApiClient, + json_str: &str, + mutations: Vec, + ) -> Result { + let obj: serde_json::Value = serde_json::from_str(json_str)?; + let ident = obj["ident"].as_str().unwrap(); // TODO: safer extraction of this ident? + let editgroup_id = self.increment_editgroup(api_client)?; + let mut entity = entity_model_from_json_str(self.entity_type, &json_str)?; + entity.mutate(mutations)?; + api_client.update_entity_from_json( + Specifier::from_ident(self.entity_type, ident.to_string()), + &entity.to_json_string()?, + editgroup_id, + ) + } + + pub fn push_delete( + &mut self, + api_client: &mut FatcatApiClient, + json_str: &str, + ) -> Result { + let obj: serde_json::Value = serde_json::from_str(json_str)?; + let ident = obj["ident"].as_str().unwrap(); // TODO: safer extraction of this ident? + let editgroup_id = self.increment_editgroup(api_client)?; + api_client.delete_entity( + Specifier::from_ident(self.entity_type, ident.to_string()), + editgroup_id, + ) + } + + pub fn increment_editgroup(&mut self, api_client: &mut FatcatApiClient) -> Result { + if self.current_count >= self.batch_size.try_into().unwrap() { + self.flush(api_client)?; + }; + self.current_count += 1; + if let Some(eg) = &self.current_editgroup_id { + return Ok(eg.to_string()); + } + let eg = api_client.create_editgroup(Some(self.editgroup_description.clone()))?; + self.current_editgroup_id = eg.editgroup_id; + Ok(self.current_editgroup_id.as_ref().unwrap().to_string()) + } + + pub fn flush(&mut self, api_client: &mut FatcatApiClient) -> Result<()> { + if self.current_count > 0 && self.current_editgroup_id.is_some() { + let eg_id = self.current_editgroup_id.clone().unwrap(); + if self.auto_accept { + api_client.accept_editgroup(eg_id)?; + } else { + api_client.update_editgroup_submit(eg_id, true)?; + } + self.total_count += self.current_count; + self.current_count = 0; + self.current_editgroup_id = None; + } + Ok(()) + } +} diff --git a/rust/fatcat-cli/src/download.rs b/rust/fatcat-cli/src/download.rs index 5500a7a..7821b70 100644 --- a/rust/fatcat-cli/src/download.rs +++ b/rust/fatcat-cli/src/download.rs @@ -1,23 +1,56 @@ use anyhow::{anyhow, Context, Result}; use fatcat_openapi::models::{FileEntity, ReleaseEntity}; use indicatif::ProgressBar; +use log::info; use reqwest::header::USER_AGENT; +use std::fmt; use std::fs::File; -use url::Url; +use std::io::{self, BufRead}; use std::path::Path; - +use std::path::PathBuf; +use url::Url; #[derive(Debug, PartialEq, Clone)] pub enum DownloadStatus { Exists(String), Downloaded(String), NetworkError(String), - NoPublicAccess, + HttpError(u16), + PartialExists(String), + NoPublicFile, FileMissingMetadata, WrongSize, WrongHash, } +impl DownloadStatus { + pub fn details(&self) -> Option { + match self { + Self::Exists(p) => Some(p.to_string()), + Self::Downloaded(p) => Some(p.to_string()), + Self::NetworkError(p) => Some(p.to_string()), + Self::PartialExists(p) => Some(p.to_string()), + _ => None, + } + } +} + +impl fmt::Display for DownloadStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Exists(_) => write!(f, "exists"), + Self::Downloaded(_) => write!(f, "success"), + Self::NetworkError(_) => write!(f, "network-error"), + Self::HttpError(p) => write!(f, "http-{}", p), + Self::PartialExists(_) => write!(f, "partial-exists"), + Self::NoPublicFile => write!(f, "no-public-file"), + Self::FileMissingMetadata => write!(f, "missing-file-metadata"), + Self::WrongSize => write!(f, "wrong-file-size"), + Self::WrongHash => write!(f, "wrong-file-hash"), + } + } +} + // eg, https://web.archive.org/web/20140802044207/http://www.geo.coop:80/sites/default/files/labs_of_oligarchy.pdf fn rewrite_wayback_url(url: Url) -> Result { // TODO: make this function correct, and add tests @@ -33,8 +66,8 @@ fn rewrite_wayback_url(url: Url) -> Result { } /// Attempts to download a file entity, including verifying checksum. -pub fn download_file(fe: FileEntity) -> Result { - let sha1hex = match fe.sha1 { +pub fn download_file(fe: &FileEntity) -> Result { + let sha1hex = match &fe.sha1 { Some(v) => v, None => return Ok(DownloadStatus::FileMissingMetadata), }; @@ -48,72 +81,106 @@ pub fn download_file(fe: FileEntity) -> Result { Some("application/postscript") => ".pdf", Some("text/html") => ".html", Some("text/xml") => ".xml", - _ => "", + _ => ".unknown", }; // TODO: output directory let path_string = format!("{}{}", sha1hex, file_suffix); let final_path = Path::new(&path_string); + // NOTE: this isn't perfect; there could have been a race condition if final_path.exists() { - return Ok(DownloadStatus::Exists(final_path.to_string_lossy().to_string())); + return Ok(DownloadStatus::Exists( + final_path.to_string_lossy().to_string(), + )); }; let path_string = format!("{}{}.partial", sha1hex, file_suffix); let download_path = Path::new(&path_string); + // TODO: only archive.org URLs (?) let raw_url = match fe.urls.as_ref() { - None => return Ok(DownloadStatus::NoPublicAccess), - Some(url_list) if url_list.len() == 0 => return Ok(DownloadStatus::NoPublicAccess), + None => return Ok(DownloadStatus::NoPublicFile), + Some(url_list) if url_list.len() == 0 => return Ok(DownloadStatus::NoPublicFile), // TODO: remove clone (?) // TODO: better heuristic than "just try first URL" Some(url_list) => url_list[0].url.clone(), }; - // TODO: only archive.org URLs (?) - let raw_url = fe.urls.unwrap()[0].url.clone(); let mut url = Url::parse(&raw_url)?; if url.host_str() == Some("web.archive.org") { url = rewrite_wayback_url(url)?; } - // TODO: open temporary file (real file plus suffix?) - let download_file = File::create(download_path)?; + let download_file = match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(download_path) + { + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { + return Ok(DownloadStatus::PartialExists( + download_path.to_string_lossy().to_string(), + )) + } + Err(e) => return Err(e).context("opening temporary download file"), + Ok(f) => f, + }; - println!("downloading: {}", url); + // TODO: print to stderr + info!("downloading: {}", url); let client = reqwest::blocking::Client::new(); - let mut resp = client + let mut resp = match client .get(url) .header(USER_AGENT, "fatcat-cli/0.0.0") - .send()?; + .send() + { + Ok(r) => r, + Err(e) => { + std::fs::remove_file(download_path)?; + return Ok(DownloadStatus::NetworkError(format!("{:?}", e))); + } + }; - // TODO: parse headers - // TODO: resp.error_for_status()?; + // TODO: parse headers, eg size (?) if !resp.status().is_success() { - return Ok(DownloadStatus::NetworkError(format!("{}", resp.status()))); + std::fs::remove_file(download_path)?; + return Ok(DownloadStatus::HttpError(resp.status().as_u16())); } // TODO: what if no filesize? // TODO: compare with resp.content_length(() -> Option let pb = ProgressBar::new(fe.size.unwrap() as u64); - let out_size = resp.copy_to(&mut pb.wrap_write(download_file))?; + let out_size = match resp.copy_to(&mut pb.wrap_write(download_file)) { + Ok(r) => r, + Err(e) => { + std::fs::remove_file(download_path)?; + return Ok(DownloadStatus::NetworkError(format!("{:?}", e))); + } + }; if out_size != expected_size { - // TODO: delete partial file? + std::fs::remove_file(download_path)?; return Ok(DownloadStatus::WrongSize); } - Ok(DownloadStatus::Downloaded(final_path.to_string_lossy().to_string())) + std::fs::rename(download_path, final_path)?; + Ok(DownloadStatus::Downloaded( + final_path.to_string_lossy().to_string(), + )) } -pub fn download_release(re: ReleaseEntity) -> Result { - let file_entities = match re.files { - None => return Err(anyhow!("expected file sub-entities to be 'expanded' on release")), +pub fn download_release(re: &ReleaseEntity) -> Result { + let file_entities = match &re.files { + None => { + return Err(anyhow!( + "expected file sub-entities to be 'expanded' on release" + )) + } Some(list) => list, }; - let mut status = DownloadStatus::NoPublicAccess; + let mut status = DownloadStatus::NoPublicFile; for fe in file_entities { - status = download_file(fe)?; + status = download_file(&fe)?; match status { DownloadStatus::Exists(_) | DownloadStatus::Downloaded(_) => break, _ => (), @@ -121,3 +188,74 @@ pub fn download_release(re: ReleaseEntity) -> Result { } Ok(status) } + +/// Tries either file or release +fn download_entity(json_str: String) -> Result { + let release_attempt = serde_json::from_str::(&json_str); + if let Ok(re) = release_attempt { + if re.ident.is_some() && (re.title.is_some() || re.files.is_some()) { + let status = download_release(&re)?; + println!( + "release_{}\t{}\t{}", + re.ident.unwrap(), + status, + status.details().unwrap_or("".to_string()) + ); + return Ok(status); + }; + } + let file_attempt = + serde_json::from_str::(&json_str).context("parsing entity for download"); + match file_attempt { + Ok(fe) => { + if fe.ident.is_some() && fe.urls.is_some() { + let status = download_file(&fe)?; + println!( + "file_{}\t{}\t{}", + fe.ident.unwrap(), + status, + status.details().unwrap_or("".to_string()) + ); + return Ok(status); + } else { + Err(anyhow!("not a file entity (JSON)")) + } + } + Err(e) => Err(e), + } +} + +pub fn download_batch(input_path: Option, limit: Option) -> Result { + let count = 0; + match input_path { + None => { + let stdin = io::stdin(); + let stdin_lock = stdin.lock(); + let lines = stdin_lock.lines(); + for line in lines { + let json_str = line?; + download_entity(json_str)?; + if let Some(limit) = limit { + if count >= limit { + break; + } + } + } + } + Some(path) => { + let input_file = File::open(path)?; + let buffered = io::BufReader::new(input_file); + let lines = buffered.lines(); + for line in lines { + let json_str = line?; + download_entity(json_str)?; + if let Some(limit) = limit { + if count >= limit { + break; + } + } + } + } + }; + Ok(count) +} diff --git a/rust/fatcat-cli/src/entities.rs b/rust/fatcat-cli/src/entities.rs index 314af51..d61f6dc 100644 --- a/rust/fatcat-cli/src/entities.rs +++ b/rust/fatcat-cli/src/entities.rs @@ -1,11 +1,10 @@ -use crate::Specifier; +use crate::{EntityType, Specifier}; use anyhow::{anyhow, Context, Result}; use fatcat_openapi::models; use lazy_static::lazy_static; use log::{self, info}; use regex::Regex; -use std::io::BufRead; -use std::io::Read; +use std::io::{BufRead, Read}; use std::path::PathBuf; use std::str::FromStr; @@ -94,6 +93,35 @@ pub fn read_entity_file(input_path: Option) -> Result { } } +pub fn entity_model_from_json_str( + entity_type: EntityType, + json_str: &str, +) -> Result> { + match entity_type { + EntityType::Release => Ok(Box::new(serde_json::from_str::( + &json_str, + )?)), + EntityType::Work => Ok(Box::new(serde_json::from_str::( + &json_str, + )?)), + EntityType::Container => Ok(Box::new(serde_json::from_str::( + &json_str, + )?)), + EntityType::Creator => Ok(Box::new(serde_json::from_str::( + &json_str, + )?)), + EntityType::File => Ok(Box::new(serde_json::from_str::( + &json_str, + )?)), + EntityType::FileSet => Ok(Box::new(serde_json::from_str::( + &json_str, + )?)), + EntityType::WebCapture => Ok(Box::new(serde_json::from_str::( + &json_str, + )?)), + } +} + pub trait ApiModelSer { fn to_json_string(&self) -> Result; fn to_toml_string(&self) -> Result; diff --git a/rust/fatcat-cli/src/lib.rs b/rust/fatcat-cli/src/lib.rs index 206fd09..d648c1c 100644 --- a/rust/fatcat-cli/src/lib.rs +++ b/rust/fatcat-cli/src/lib.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Context, Result}; use data_encoding::BASE64; use macaroon::{Macaroon, Verifier}; +use std::path::PathBuf; use std::str::FromStr; mod api; @@ -12,10 +13,14 @@ mod specifier; pub use api::FatcatApiClient; pub use commands::{ - print_changelog_entries, print_editgroups, print_entity_histories, ClientStatus, edit_entity_locally, + edit_entity_locally, print_changelog_entries, print_editgroups, print_entity_histories, + BatchGrouper, BatchOp, ClientStatus, +}; +pub use download::{download_batch, download_file, download_release}; +pub use entities::{ + entity_model_from_json_str, read_entity_file, ApiEntityModel, ApiModelIdent, ApiModelSer, + Mutation, }; -pub use download::{download_release, download_file}; -pub use entities::{read_entity_file, ApiEntityModel, ApiModelIdent, ApiModelSer, Mutation}; pub use search::crude_search; pub use specifier::Specifier; @@ -78,3 +83,11 @@ pub fn parse_macaroon_editor_id(s: &str) -> Result { verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string())); Ok(editor_id) } + +pub fn path_or_stdin(raw: Option) -> Option { + // treat "-" as "use stdin" + match raw { + Some(s) if s.to_string_lossy() == "-" => None, + _ => raw, + } +} diff --git a/rust/fatcat-cli/src/main.rs b/rust/fatcat-cli/src/main.rs index 79efbd3..3b0d382 100644 --- a/rust/fatcat-cli/src/main.rs +++ b/rust/fatcat-cli/src/main.rs @@ -1,7 +1,7 @@ +use crate::{path_or_stdin, BatchGrouper, BatchOp}; use anyhow::{anyhow, Context, Result}; use fatcat_cli::ApiModelSer; use fatcat_cli::*; -use fatcat_openapi::models; #[allow(unused_imports)] use log::{self, debug, info}; use std::io::Write; @@ -99,19 +99,8 @@ enum BatchCommand { #[structopt(long, default_value = "50")] batch_size: u64, - #[structopt(long)] - dry_run: bool, - #[structopt(long)] auto_accept: bool, - - #[structopt( - long = "--editgroup-id", - short, - env = "FATCAT_EDITGROUP", - hide_env_values = true - )] - editgroup_id: String, }, Update { entity_type: EntityType, @@ -120,19 +109,8 @@ enum BatchCommand { #[structopt(long, default_value = "50")] batch_size: u64, - #[structopt(long)] - dry_run: bool, - #[structopt(long)] auto_accept: bool, - - #[structopt( - long = "--editgroup-id", - short, - env = "FATCAT_EDITGROUP", - hide_env_values = true - )] - editgroup_id: String, }, Delete { entity_type: EntityType, @@ -140,19 +118,8 @@ enum BatchCommand { #[structopt(long, default_value = "50")] batch_size: u64, - #[structopt(long)] - dry_run: bool, - #[structopt(long)] auto_accept: bool, - - #[structopt( - long = "--editgroup-id", - short, - env = "FATCAT_EDITGROUP", - hide_env_values = true - )] - editgroup_id: String, }, Download {}, } @@ -231,6 +198,7 @@ enum Command { #[structopt(long)] json: bool, + #[allow(dead_code)] #[structopt(long)] toml: bool, @@ -243,7 +211,7 @@ enum Command { History { specifier: Specifier, - #[structopt(long, short = "-n", default_value = "20")] + #[structopt(long, short = "-n", default_value = "100")] limit: u64, #[structopt(long)] @@ -348,13 +316,13 @@ fn run(opt: Opt) -> Result<()> { specifier, expand, hide, - json: _, + 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 { + } else if json || true { writeln!(&mut std::io::stdout(), "{}", result.to_json_string()?)? } } @@ -398,13 +366,16 @@ fn run(opt: Opt) -> Result<()> { toml: _, editing_command, } => { - let ee = edit_entity_locally(&mut api_client, specifier, editgroup_id, json, 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, - } => { + Command::Changelog { limit, json } => { let resp = api_client .rt .block_on(api_client.api.get_changelog(Some(limit))) @@ -414,9 +385,8 @@ fn run(opt: Opt) -> Result<()> { print_changelog_entries(change_list, json)?; } other => { - return Err(anyhow!("{:?}", other)).with_context(|| { - format!("failed to fetch changelogs") - }) + return Err(anyhow!("{:?}", other)) + .with_context(|| format!("failed to fetch changelogs")) } } } @@ -425,90 +395,99 @@ fn run(opt: Opt) -> Result<()> { BatchCommand::Create { entity_type, batch_size, - dry_run, auto_accept, - editgroup_id, }, input_path, limit, } => { - unimplemented!("batch create") - }, + 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, - dry_run, auto_accept, - editgroup_id, }, input_path, limit, } => { - unimplemented!("batch update") - }, + 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, - dry_run, auto_accept, - editgroup_id, }, input_path, limit, } => { - unimplemented!("batch delete") - }, + 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 { - // TODO - }, + cmd: BatchCommand::Download {}, input_path, limit, } => { - unimplemented!("batch create") - }, - Command::Download{specifier} => { + 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::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 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) - }, + fatcat_openapi::GetReleaseResponse::FoundEntity(model) => Ok(model), resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", ident)), }?; - download_release(release_entity) - }, + download_release(&release_entity) + } Specifier::File(ident) => { - let result = api_client.rt.block_on( - api_client.api.get_file(ident.clone(), None, None) - )?; + 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) - }, + fatcat_openapi::GetFileResponse::FoundEntity(model) => Ok(model), resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", ident)), }?; - download_file(file_entity) - }, + download_file(&file_entity) + } other => Err(anyhow!("Don't know how to download: {:?}", other)), }?; - println!("{:?}", status); + if let Some(detail) = status.details() { + println!("{}: {}", status, detail); + } else { + println!("{}", status); + } } Command::Search { entity_type, @@ -519,7 +498,7 @@ fn run(opt: Opt) -> Result<()> { hide, } => { let limit: Option = match limit { - l if l < 0 => None, + l if l <= 0 => None, l => Some(l as u64), }; let results = fatcat_cli::crude_search(&opt.search_host, entity_type, limit, terms) @@ -618,45 +597,14 @@ fn run(opt: Opt) -> Result<()> { Command::Editgroup { cmd: EditgroupCommand::Create { description }, } => { - let mut eg = models::Editgroup::new(); - eg.description = Some(description); - eg.extra = Some({ - let mut extra = std::collections::HashMap::new(); - extra.insert( - "agent".to_string(), - serde_json::Value::String("fatcat-cli".to_string()), - ); - extra - }); - let result = api_client - .rt - .block_on(api_client.api.create_editgroup(eg))?; - match result { - fatcat_openapi::CreateEditgroupResponse::SuccessfullyCreated(eg) => { - println!("{}", serde_json::to_string(&eg)?) - } - other => return Err(anyhow!("{:?}", other)).context("failed to create editgroup"), - } + let eg = api_client.create_editgroup(Some(description))?; + println!("{}", serde_json::to_string(&eg)?) } Command::Editgroup { cmd: EditgroupCommand::Accept { editgroup_id }, } => { - let result = api_client - .rt - .block_on(api_client.api.accept_editgroup(editgroup_id.clone())) - .context("accept editgroup")?; - match result { - fatcat_openapi::AcceptEditgroupResponse::MergedSuccessfully(msg) => { - println!("{}", serde_json::to_string(&msg)?) - } - other => { - return Err(anyhow!( - "failed to accept editgroup {}: {:?}", - editgroup_id, - other - )) - } - } + let msg = api_client.accept_editgroup(editgroup_id.clone())?; + println!("{}", serde_json::to_string(&msg)?); } Command::Editgroup { cmd: EditgroupCommand::Submit { editgroup_id }, diff --git a/rust/fatcat-cli/src/specifier.rs b/rust/fatcat-cli/src/specifier.rs index c7a7b57..0d8d209 100644 --- a/rust/fatcat-cli/src/specifier.rs +++ b/rust/fatcat-cli/src/specifier.rs @@ -1,4 +1,4 @@ -use crate::{ApiEntityModel, FatcatApiClient}; +use crate::{ApiEntityModel, EntityType, FatcatApiClient}; use anyhow::{anyhow, Context, Result}; use lazy_static::lazy_static; use regex::Regex; @@ -50,6 +50,18 @@ pub enum Specifier { } impl Specifier { + pub fn from_ident(entity_type: EntityType, ident: String) -> Specifier { + match entity_type { + EntityType::Release => Specifier::Release(ident), + EntityType::Work => Specifier::Work(ident), + EntityType::Container => Specifier::Container(ident), + EntityType::Creator => Specifier::Creator(ident), + EntityType::File => Specifier::File(ident), + EntityType::FileSet => Specifier::FileSet(ident), + EntityType::WebCapture => Specifier::WebCapture(ident), + } + } + /// If this Specifier is a lookup, call the API to do the lookup and return the resulting /// specific entity specifier (eg, with an FCID). If already specific, just pass through. pub fn into_entity_specifier(self, api_client: &mut FatcatApiClient) -> Result { @@ -382,19 +394,18 @@ impl Specifier { .rt .block_on(api_client.api.get_work_history(fcid.to_string(), limit))? { - fatcat_openapi::GetWorkHistoryResponse::FoundEntityHistory(entries) => { - Ok(entries) - } + fatcat_openapi::GetWorkHistoryResponse::FoundEntityHistory(entries) => Ok(entries), fatcat_openapi::GetWorkHistoryResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, - Container(fcid) => match api_client - .rt - .block_on(api_client.api.get_container_history(fcid.to_string(), limit))? - { + Container(fcid) => match api_client.rt.block_on( + api_client + .api + .get_container_history(fcid.to_string(), limit), + )? { fatcat_openapi::GetContainerHistoryResponse::FoundEntityHistory(entries) => { Ok(entries) } @@ -421,9 +432,7 @@ impl Specifier { .rt .block_on(api_client.api.get_file_history(fcid.to_string(), limit))? { - fatcat_openapi::GetFileHistoryResponse::FoundEntityHistory(entries) => { - Ok(entries) - } + fatcat_openapi::GetFileHistoryResponse::FoundEntityHistory(entries) => Ok(entries), fatcat_openapi::GetFileHistoryResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } @@ -443,10 +452,11 @@ impl Specifier { resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, - WebCapture(fcid) => match api_client - .rt - .block_on(api_client.api.get_webcapture_history(fcid.to_string(), limit))? - { + WebCapture(fcid) => match api_client.rt.block_on( + api_client + .api + .get_webcapture_history(fcid.to_string(), limit), + )? { fatcat_openapi::GetWebcaptureHistoryResponse::FoundEntityHistory(entries) => { Ok(entries) } -- cgit v1.2.3