diff options
Diffstat (limited to 'rust')
-rw-r--r-- | rust/fatcat-cli/src/api.rs | 66 | ||||
-rw-r--r-- | rust/fatcat-cli/src/commands.rs | 272 | ||||
-rw-r--r-- | rust/fatcat-cli/src/download.rs | 17 | ||||
-rw-r--r-- | rust/fatcat-cli/src/entities.rs | 38 | ||||
-rw-r--r-- | rust/fatcat-cli/src/lib.rs | 190 | ||||
-rw-r--r-- | rust/fatcat-cli/src/main.rs | 241 | ||||
-rw-r--r-- | rust/fatcat-cli/src/specifier.rs | 110 |
7 files changed, 667 insertions, 267 deletions
diff --git a/rust/fatcat-cli/src/api.rs b/rust/fatcat-cli/src/api.rs index 2db2efb..cc6fa6a 100644 --- a/rust/fatcat-cli/src/api.rs +++ b/rust/fatcat-cli/src/api.rs @@ -1,4 +1,4 @@ -use crate::{parse_macaroon_editor_id, ClientStatus, EntityType, Specifier}; +use crate::{parse_macaroon_editor_id, EntityType, Specifier}; use anyhow::{anyhow, Context, Result}; use fatcat_openapi::models; use fatcat_openapi::{ApiNoContext, ContextWrapperExt}; @@ -14,16 +14,13 @@ type FatcatApiContextType = swagger::make_context_ty!( pub struct FatcatApiClient { pub api: Box<dyn ApiNoContext<FatcatApiContextType>>, pub rt: tokio::runtime::Runtime, - api_token: Option<String>, - api_host: String, + pub api_token: Option<String>, + pub api_host: String, pub editor_id: Option<String>, } impl FatcatApiClient { - pub fn new( - api_host: String, - api_token: Option<String>, - ) -> Result<Self> { + pub fn new(api_host: String, api_token: Option<String>) -> Result<Self> { let auth_data = match api_token { Some(ref token) => Some(AuthData::Bearer(auth::Bearer { token: token.clone(), @@ -39,9 +36,11 @@ impl FatcatApiClient { ); //let wrapped_client: swagger::ContextWrapper< - let client = fatcat_openapi::client::Client::try_new(&api_host).context("failed to create HTTP(S) client")?; + let client = fatcat_openapi::client::Client::try_new(&api_host) + .context("failed to create HTTP(S) client")?; let wrapped_client = Box::new(client.with_context(context)); - let rt: tokio::runtime::Runtime = tokio::runtime::Runtime::new().expect("create tokio runtime"); + let rt: tokio::runtime::Runtime = + tokio::runtime::Runtime::new().expect("create tokio runtime"); let editor_id = match api_token { Some(ref token) => { @@ -59,55 +58,6 @@ impl FatcatApiClient { }) } - pub fn status(&mut self) -> Result<ClientStatus> { - let last_changelog = match self.rt.block_on(self.api.get_changelog(Some(1))) { - Ok(fatcat_openapi::GetChangelogResponse::Success(entry_vec)) => { - Some(entry_vec[0].index) - } - Ok(_) | Err(_) => None, - }; - let has_api_token = self.api_token.is_some(); - let account: Option<models::Editor> = if has_api_token && last_changelog.is_some() { - match self - .rt - .block_on(self.api.auth_check(None)) - .context("check auth token")? - { - fatcat_openapi::AuthCheckResponse::Success(_) => Ok(()), - fatcat_openapi::AuthCheckResponse::Forbidden(err) => { - Err(anyhow!("Forbidden ({}): {}", err.error, err.message)) - } - fatcat_openapi::AuthCheckResponse::NotAuthorized { body: err, .. } => { - Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) - } - resp => return Err(anyhow!("{:?}", resp)).context("auth check failed"), - } - .context("check auth token")?; - match self - .rt - .block_on( - self.api - .get_editor(self.editor_id.as_ref().unwrap().to_string()), - ) - .context("fetching editor account info")? - { - fatcat_openapi::GetEditorResponse::Found(editor) => Some(editor), - fatcat_openapi::GetEditorResponse::NotFound(err) => { - return Err(anyhow!("Not Found: {}", err.message)) - } - resp => return Err(anyhow!("{:?}", resp)).context("editor fetch failed"), - } - } else { - None - }; - Ok(ClientStatus { - api_host: self.api_host.clone(), - has_api_token, - last_changelog, - account, - }) - } - pub fn update_editgroup_submit( &mut self, editgroup_id: String, diff --git a/rust/fatcat-cli/src/commands.rs b/rust/fatcat-cli/src/commands.rs new file mode 100644 index 0000000..c0000c7 --- /dev/null +++ b/rust/fatcat-cli/src/commands.rs @@ -0,0 +1,272 @@ +use anyhow::{anyhow, Context, Result}; +use chrono_humanize::HumanTime; +use fatcat_openapi::models; +#[allow(unused_imports)] +use log::{self, debug, info}; +use std::io::{Write, BufRead}; +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}; +//use crate::specifier::Specifier; + +// Want to show: +// - whether api_token found +// - configured api_host we are connecting to +// - whether we can connect to remote host (eg, get most recent changelog) +// - whether our auth is valid +// - current active editgroup +#[derive(Debug, PartialEq, Clone, serde::Serialize)] +pub struct ClientStatus { + pub has_api_token: bool, + pub api_host: String, + pub last_changelog: Option<i64>, + pub account: Option<models::Editor>, +} + +impl ClientStatus { + pub fn generate(api_client: &mut FatcatApiClient) -> Result<Self> { + let last_changelog = match api_client + .rt + .block_on(api_client.api.get_changelog(Some(1))) + { + Ok(fatcat_openapi::GetChangelogResponse::Success(entry_vec)) => { + Some(entry_vec[0].index) + } + Ok(_) | Err(_) => None, + }; + let has_api_token = api_client.api_token.is_some(); + let account: Option<models::Editor> = if has_api_token && last_changelog.is_some() { + match api_client + .rt + .block_on(api_client.api.auth_check(None)) + .context("check auth token")? + { + fatcat_openapi::AuthCheckResponse::Success(_) => Ok(()), + fatcat_openapi::AuthCheckResponse::Forbidden(err) => { + Err(anyhow!("Forbidden ({}): {}", err.error, err.message)) + } + fatcat_openapi::AuthCheckResponse::NotAuthorized { body: err, .. } => { + Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) + } + resp => return Err(anyhow!("{:?}", resp)).context("auth check failed"), + } + .context("check auth token")?; + match api_client + .rt + .block_on( + api_client + .api + .get_editor(api_client.editor_id.as_ref().unwrap().to_string()), + ) + .context("fetching editor account info")? + { + fatcat_openapi::GetEditorResponse::Found(editor) => Some(editor), + fatcat_openapi::GetEditorResponse::NotFound(err) => { + return Err(anyhow!("Not Found: {}", err.message)) + } + resp => return Err(anyhow!("{:?}", resp)).context("editor fetch failed"), + } + } else { + None + }; + Ok(ClientStatus { + api_host: api_client.api_host.clone(), + has_api_token, + last_changelog, + account, + }) + } + + pub fn pretty_print(self) -> Result<()> { + let mut color_stdout = StandardStream::stdout(if atty::is(atty::Stream::Stdout) { + ColorChoice::Auto + } else { + ColorChoice::Never + }); + let color_normal = ColorSpec::new(); + let mut color_bold = ColorSpec::new(); + color_bold.set_bold(true); + let mut color_happy = ColorSpec::new(); + color_happy.set_fg(Some(Color::Green)).set_bold(true); + let mut color_sad = ColorSpec::new(); + color_sad.set_fg(Some(Color::Red)).set_bold(true); + + color_stdout.set_color(&color_normal)?; + write!(&mut color_stdout, "{:>16}: ", "API host")?; + color_stdout.set_color(&color_bold)?; + write!(&mut color_stdout, "{}", self.api_host)?; + match self.last_changelog { + Some(index) => { + color_stdout.set_color(&color_happy)?; + writeln!(&mut color_stdout, " [successfully connected]")?; + color_stdout.set_color(&color_normal)?; + write!(&mut color_stdout, "{:>16}: ", "Last changelog")?; + color_stdout.set_color(&color_bold)?; + writeln!(&mut color_stdout, "{}", index)?; + } + None => { + color_stdout.set_color(&color_sad)?; + writeln!(&mut color_stdout, " [Failed to connect]")?; + } + }; + color_stdout.set_color(&color_normal)?; + write!(&mut color_stdout, "{:>16}: ", "API auth token")?; + if self.has_api_token { + color_stdout.set_color(&color_happy)?; + writeln!(&mut color_stdout, "[configured]")?; + } else { + color_stdout.set_color(&color_sad)?; + writeln!(&mut color_stdout, "[not configured]")?; + }; + if let Some(editor) = self.account { + color_stdout.set_color(&color_normal)?; + write!(&mut color_stdout, "{:>16}: ", "Account")?; + color_stdout.set_color(&color_bold)?; + write!(&mut color_stdout, "{}", editor.username)?; + if editor.is_bot == Some(true) { + color_stdout + .set_color(ColorSpec::new().set_fg(Some(Color::Blue)).set_bold(true))?; + write!(&mut color_stdout, " [bot]")?; + } + if editor.is_admin == Some(true) { + color_stdout + .set_color(ColorSpec::new().set_fg(Some(Color::Magenta)).set_bold(true))?; + write!(&mut color_stdout, " [admin]")?; + } + match editor.is_active { + Some(true) => { + color_stdout.set_color(&color_happy)?; + writeln!(&mut color_stdout, " [active]")?; + } + Some(false) | None => { + color_stdout.set_color(&color_sad)?; + writeln!(&mut color_stdout, " [disabled]")?; + } + }; + color_stdout.set_color(&color_normal)?; + writeln!( + &mut color_stdout, + "{:>16} editor_{}", + "", + editor.editor_id.unwrap() + )?; + }; + color_stdout.set_color(&color_normal)?; + Ok(()) + } +} + +pub fn print_editgroups(eg_list: Vec<models::Editgroup>, json: bool) -> Result<()> { + if json { + for eg in eg_list { + writeln!(&mut std::io::stdout(), "{}", eg.to_json_string()?)?; + } + } else { + let mut tw = TabWriter::new(std::io::stdout()); + writeln!( + tw, + "editgroup_id\tchangelog_index\tcreated\tsubmitted\tdescription" + )?; + for eg in eg_list { + writeln!( + tw, + "{}\t{}\t{}\t{}\t{}", + eg.editgroup_id.unwrap(), + eg.changelog_index + .map_or("-".to_string(), |v| v.to_string()), + eg.created + .map_or("-".to_string(), |v| HumanTime::from(v).to_string()), + eg.submitted + .map_or("-".to_string(), |v| HumanTime::from(v).to_string()), + eg.description.unwrap_or_else(|| "-".to_string()) + )?; + } + tw.flush()?; + } + Ok(()) +} + +pub fn print_changelog_entries(entry_list: Vec<models::ChangelogEntry>, json: bool) -> Result<()> { + if json { + for entry in entry_list { + writeln!(&mut std::io::stdout(), "{}", entry.to_json_string()?)?; + } + } else { + let mut tw = TabWriter::new(std::io::stdout()); + writeln!(tw, "index\ttimestamp\teditor\teditgroup_description")?; + for entry in entry_list { + writeln!( + tw, + "{}\t{}\t{}\t{}", + entry.index, + HumanTime::from(entry.timestamp).to_string(), + entry + .editgroup + .as_ref() + .unwrap() + .editor + .as_ref() + .map_or("-".to_string(), |v| v.username.to_string()), + entry + .editgroup + .as_ref() + .unwrap() + .description + .as_ref() + .map_or("-".to_string(), |v| v.to_string()), + )?; + } + tw.flush()?; + } + Ok(()) +} + +pub fn print_entity_histories( + history_list: Vec<models::EntityHistoryEntry>, + json: bool, +) -> Result<()> { + if json { + for history in history_list { + writeln!(&mut std::io::stdout(), "{}", history.to_json_string()?)?; + } + } else { + let mut tw = TabWriter::new(std::io::stdout()); + writeln!( + tw, + "changelog_index\ttype\ttimestamp\teditor\teditgroup_description" + )?; + for history in history_list { + let state = match ( + history.edit.revision, + history.edit.prev_revision, + history.edit.redirect_ident, + ) { + (Some(_), None, None) => "create", + (Some(_), Some(_), None) => "update", + (None, _, None) => "delete", + (None, _, Some(_)) => "redirect", + _ => "-", + }; + writeln!( + tw, + "{}\t{}\t{}\t{}\t{}", + history.changelog_entry.index, + state, + HumanTime::from(history.changelog_entry.timestamp).to_string(), + history + .editgroup + .editor + .map_or("-".to_string(), |v| v.username.to_string()), + history + .editgroup + .description + .unwrap_or_else(|| "-".to_string()) + )?; + } + tw.flush()?; + } + Ok(()) +} diff --git a/rust/fatcat-cli/src/download.rs b/rust/fatcat-cli/src/download.rs index c8c05fd..0fcf370 100644 --- a/rust/fatcat-cli/src/download.rs +++ b/rust/fatcat-cli/src/download.rs @@ -1,10 +1,9 @@ - use anyhow::{anyhow, Context, Result}; -use indicatif::ProgressBar; use fatcat_openapi::models::FileEntity; +use indicatif::ProgressBar; use reqwest::header::USER_AGENT; -use url::Url; use std::fs::File; +use url::Url; #[derive(Debug, PartialEq, Clone)] pub enum DownloadStatus { @@ -18,7 +17,11 @@ pub enum DownloadStatus { // 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<Url> { // TODO: make this function correct, and add tests - let mut segments: Vec<String> = url.path_segments().unwrap().map(|x| x.to_string()).collect(); + let mut segments: Vec<String> = url + .path_segments() + .unwrap() + .map(|x| x.to_string()) + .collect(); if segments[0] == "web" && segments[1].len() == 14 { segments[1] = format!("{}id_", segments[1]); } @@ -27,7 +30,6 @@ fn rewrite_wayback_url(url: Url) -> Result<Url> { /// Attempts to download a file entity, including verifying checksum. pub fn download_file(fe: FileEntity) -> Result<DownloadStatus> { - // TODO: check if file has sha1hex // TODO: check if file already exists @@ -44,7 +46,8 @@ pub fn download_file(fe: FileEntity) -> Result<DownloadStatus> { println!("downloading: {}", url); let client = reqwest::blocking::Client::new(); - let mut resp = client.get(url) + let mut resp = client + .get(url) .header(USER_AGENT, "fatcat-cli/0.0.0") .send()?; @@ -57,7 +60,7 @@ pub fn download_file(fe: FileEntity) -> Result<DownloadStatus> { // TODO: what if no filesize? // TODO: compare with resp.content_length(() -> Option<u64> let pb = ProgressBar::new(fe.size.unwrap() as u64); - let out_size = resp.copy_to(&mut pb.wrap_write(out_file))?; + let _out_size = resp.copy_to(&mut pb.wrap_write(out_file))?; Ok(DownloadStatus::NotYet) } diff --git a/rust/fatcat-cli/src/entities.rs b/rust/fatcat-cli/src/entities.rs index eee3946..314af51 100644 --- a/rust/fatcat-cli/src/entities.rs +++ b/rust/fatcat-cli/src/entities.rs @@ -1,8 +1,12 @@ use crate::Specifier; -use anyhow::{anyhow, Result}; +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::path::PathBuf; use std::str::FromStr; #[derive(Debug, PartialEq, Clone)] @@ -58,6 +62,38 @@ impl ApiEntityModel for models::Editor {} impl ApiEntityModel for models::Editgroup {} impl ApiEntityModel for models::ChangelogEntry {} +pub fn read_entity_file(input_path: Option<PathBuf>) -> Result<String> { + // treat "-" as "use stdin" + let input_path = match input_path { + Some(s) if s.to_string_lossy() == "-" => None, + _ => input_path, + }; + match input_path { + None => { + let mut line = String::new(); + std::io::stdin().read_line(&mut line)?; + Ok(line) + } + Some(path) if path.extension().map(|v| v.to_str()) == Some(Some("toml")) => { + info!("reading {:?} as TOML", path); + // as a hack, read TOML but then serialize it back to JSON + let mut contents = String::new(); + let mut input_file = + std::fs::File::open(path).context("reading entity from TOML file")?; + input_file.read_to_string(&mut contents)?; + let value: toml::Value = contents.parse().context("parsing TOML file")?; + Ok(serde_json::to_string(&value)?) + } + Some(path) => { + let mut line = String::new(); + let input_file = std::fs::File::open(path)?; + let mut buffered = std::io::BufReader::new(input_file); + buffered.read_line(&mut line)?; + Ok(line) + } + } +} + pub trait ApiModelSer { fn to_json_string(&self) -> Result<String>; fn to_toml_string(&self) -> Result<String>; diff --git a/rust/fatcat-cli/src/lib.rs b/rust/fatcat-cli/src/lib.rs index 93c17fb..8a48a3b 100644 --- a/rust/fatcat-cli/src/lib.rs +++ b/rust/fatcat-cli/src/lib.rs @@ -1,123 +1,23 @@ use anyhow::{anyhow, Context, Result}; -use chrono_humanize::HumanTime; use data_encoding::BASE64; -use fatcat_openapi::models; -#[allow(unused_imports)] -use log::{self, debug, info}; use macaroon::{Macaroon, Verifier}; -use std::io::BufRead; -use std::io::Read; -use std::io::Write; -use std::path::PathBuf; use std::str::FromStr; -use tabwriter::TabWriter; -use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; mod api; +mod commands; +mod download; mod entities; mod search; mod specifier; -mod download; pub use api::FatcatApiClient; -pub use entities::{ApiEntityModel, ApiModelIdent, ApiModelSer, Mutation}; +pub use commands::{ + print_changelog_entries, print_editgroups, print_entity_histories, ClientStatus, +}; +pub use download::download_file; +pub use entities::{read_entity_file, ApiEntityModel, ApiModelIdent, ApiModelSer, Mutation}; pub use search::crude_search; pub use specifier::Specifier; -pub use download::download_file; - -// Want to show: -// - whether api_token found -// - configured api_host we are connecting to -// - whether we can connect to remote host (eg, get most recent changelog) -// - whether our auth is valid -// - current active editgroup -#[derive(Debug, PartialEq, Clone, serde::Serialize)] -pub struct ClientStatus { - pub has_api_token: bool, - pub api_host: String, - pub last_changelog: Option<i64>, - pub account: Option<models::Editor>, -} - -impl ClientStatus { - pub fn pretty_print(self) -> Result<()> { - let mut color_stdout = StandardStream::stdout(if atty::is(atty::Stream::Stdout) { - ColorChoice::Auto - } else { - ColorChoice::Never - }); - let color_normal = ColorSpec::new(); - let mut color_bold = ColorSpec::new(); - color_bold.set_bold(true); - let mut color_happy = ColorSpec::new(); - color_happy.set_fg(Some(Color::Green)).set_bold(true); - let mut color_sad = ColorSpec::new(); - color_sad.set_fg(Some(Color::Red)).set_bold(true); - - color_stdout.set_color(&color_normal)?; - write!(&mut color_stdout, "{:>16}: ", "API host")?; - color_stdout.set_color(&color_bold)?; - write!(&mut color_stdout, "{}", self.api_host)?; - match self.last_changelog { - Some(index) => { - color_stdout.set_color(&color_happy)?; - writeln!(&mut color_stdout, " [successfully connected]")?; - color_stdout.set_color(&color_normal)?; - write!(&mut color_stdout, "{:>16}: ", "Last changelog")?; - color_stdout.set_color(&color_bold)?; - writeln!(&mut color_stdout, "{}", index)?; - } - None => { - color_stdout.set_color(&color_sad)?; - writeln!(&mut color_stdout, " [Failed to connect]")?; - } - }; - color_stdout.set_color(&color_normal)?; - write!(&mut color_stdout, "{:>16}: ", "API auth token")?; - if self.has_api_token { - color_stdout.set_color(&color_happy)?; - writeln!(&mut color_stdout, "[configured]")?; - } else { - color_stdout.set_color(&color_sad)?; - writeln!(&mut color_stdout, "[not configured]")?; - }; - if let Some(editor) = self.account { - color_stdout.set_color(&color_normal)?; - write!(&mut color_stdout, "{:>16}: ", "Account")?; - color_stdout.set_color(&color_bold)?; - write!(&mut color_stdout, "{}", editor.username)?; - if editor.is_bot == Some(true) { - color_stdout - .set_color(ColorSpec::new().set_fg(Some(Color::Blue)).set_bold(true))?; - write!(&mut color_stdout, " [bot]")?; - } - if editor.is_admin == Some(true) { - color_stdout - .set_color(ColorSpec::new().set_fg(Some(Color::Magenta)).set_bold(true))?; - write!(&mut color_stdout, " [admin]")?; - } - match editor.is_active { - Some(true) => { - color_stdout.set_color(&color_happy)?; - writeln!(&mut color_stdout, " [active]")?; - } - Some(false) | None => { - color_stdout.set_color(&color_sad)?; - writeln!(&mut color_stdout, " [disabled]")?; - } - }; - color_stdout.set_color(&color_normal)?; - writeln!( - &mut color_stdout, - "{:>16} editor_{}", - "", - editor.editor_id.unwrap() - )?; - }; - color_stdout.set_color(&color_normal)?; - Ok(()) - } -} #[derive(Debug, PartialEq, Clone, Copy)] pub enum EntityType { @@ -135,13 +35,13 @@ impl FromStr for EntityType { fn from_str(s: &str) -> Result<Self, Self::Err> { match s { - "release" => Ok(EntityType::Release), - "work" => Ok(EntityType::Work), - "container" => Ok(EntityType::Container), - "creator" => Ok(EntityType::Creator), - "file" => Ok(EntityType::File), - "FILESET" => Ok(EntityType::FileSet), - "webcapture" => Ok(EntityType::WebCapture), + "release" | "releases" => Ok(EntityType::Release), + "work" | "works" => Ok(EntityType::Work), + "container" | "containers" => Ok(EntityType::Container), + "creator" | "creators" => Ok(EntityType::Creator), + "file" | "files" => Ok(EntityType::File), + "fileset" | "filesets" => Ok(EntityType::FileSet), + "webcapture" | "webcaptures" => Ok(EntityType::WebCapture), _ => Err(anyhow!("invalid entity type : {}", s)), } } @@ -178,65 +78,3 @@ pub fn parse_macaroon_editor_id(s: &str) -> Result<String> { verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string())); Ok(editor_id) } - -pub fn print_editgroups(eg_list: Vec<models::Editgroup>, json: bool) -> Result<()> { - if json { - for eg in eg_list { - writeln!(&mut std::io::stdout(), "{}", eg.to_json_string()?)?; - } - } else { - let mut tw = TabWriter::new(std::io::stdout()); - writeln!( - tw, - "editgroup_id\tchangelog_index\tcreated\tsubmitted\tdescription" - )?; - for eg in eg_list { - writeln!( - tw, - "{}\t{}\t{}\t{}\t{}", - eg.editgroup_id.unwrap(), - eg.changelog_index - .map_or("-".to_string(), |v| v.to_string()), - eg.created - .map_or("-".to_string(), |v| HumanTime::from(v).to_string()), - eg.submitted - .map_or("-".to_string(), |v| HumanTime::from(v).to_string()), - eg.description.unwrap_or_else(|| "-".to_string()) - )?; - } - tw.flush()?; - } - Ok(()) -} - -pub fn read_entity_file(input_path: Option<PathBuf>) -> Result<String> { - // treat "-" as "use stdin" - let input_path = match input_path { - Some(s) if s.to_string_lossy() == "-" => None, - _ => input_path, - }; - match input_path { - None => { - let mut line = String::new(); - std::io::stdin().read_line(&mut line)?; - Ok(line) - } - Some(path) if path.extension().map(|v| v.to_str()) == Some(Some("toml")) => { - info!("reading {:?} as TOML", path); - // as a hack, read TOML but then serialize it back to JSON - let mut contents = String::new(); - let mut input_file = - std::fs::File::open(path).context("reading entity from TOML file")?; - input_file.read_to_string(&mut contents)?; - let value: toml::Value = contents.parse().context("parsing TOML file")?; - Ok(serde_json::to_string(&value)?) - } - Some(path) => { - let mut line = String::new(); - let input_file = std::fs::File::open(path)?; - let mut buffered = std::io::BufReader::new(input_file); - buffered.read_line(&mut line)?; - Ok(line) - } - } -} diff --git a/rust/fatcat-cli/src/main.rs b/rust/fatcat-cli/src/main.rs index 75ddc6a..b677aca 100644 --- a/rust/fatcat-cli/src/main.rs +++ b/rust/fatcat-cli/src/main.rs @@ -13,13 +13,16 @@ use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; #[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 @@ -27,17 +30,18 @@ struct Opt { api_token: Option<String>, #[structopt( + global = true, long = "--search-host", env = "FATCAT_SEARCH_HOST", default_value = "https://search.fatcat.wiki" )] search_host: String, - /// Pass many times for more log output + /// 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(long, short = "v", parse(from_occurrences))] + #[structopt(global = true, long, short = "v", parse(from_occurrences))] verbose: i8, #[structopt(subcommand)] @@ -46,10 +50,12 @@ struct Opt { #[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>, @@ -60,6 +66,7 @@ enum EditgroupCommand { #[structopt(long)] json: bool, }, + /// Print recent editgroups from any user which need review Reviewable { #[structopt(long, short = "-n", default_value = "20")] limit: i64, @@ -67,14 +74,17 @@ enum EditgroupCommand { #[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, @@ -82,11 +92,73 @@ enum EditgroupCommand { } #[derive(StructOpt)] -enum Command { - Status { +enum BatchCommand { + Create { + entity_type: EntityType, + + #[structopt(long, default_value = "50")] + batch_size: u64, + #[structopt(long)] - json: bool, + 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, + mutations: Vec<Mutation>, + + #[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, + + #[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 {}, +} + +#[derive(StructOpt)] +enum Command { Get { specifier: Specifier, @@ -97,6 +169,9 @@ enum Command { hide: Option<String>, #[structopt(long)] + json: bool, + + #[structopt(long)] toml: bool, }, Create { @@ -131,7 +206,7 @@ enum Command { mutations: Vec<Mutation>, }, - Edit { + Delete { specifier: Specifier, #[structopt( @@ -141,14 +216,8 @@ enum Command { hide_env_values = true )] editgroup_id: String, - - #[structopt(long)] - json: bool, - - #[structopt(long = "--editing-command", env = "EDITOR")] - editing_command: String, }, - Delete { + Edit { specifier: Specifier, #[structopt( @@ -158,16 +227,28 @@ enum Command { hide_env_values = true )] editgroup_id: String, - }, - Editgroup { - #[structopt(subcommand)] - cmd: EditgroupCommand, + + #[structopt(long)] + json: bool, + + #[structopt(long)] + toml: bool, + + #[structopt(long = "--editing-command", env = "EDITOR")] + editing_command: String, }, Download { specifier: Specifier, }, - //Changelog - //History + History { + specifier: Specifier, + + #[structopt(long, short = "-n", default_value = "20")] + limit: u64, + + #[structopt(long)] + json: bool, + }, Search { entity_type: EntityType, @@ -185,6 +266,36 @@ enum Command { #[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<()> { @@ -230,15 +341,15 @@ fn main() -> Result<()> { } fn run(opt: Opt) -> Result<()> { - let mut api_client = FatcatApiClient::new(opt.api_host.clone(), opt.api_token.clone())?; match opt.cmd { Command::Get { - toml, specifier, expand, hide, + json: _, + toml, } => { let result = specifier.get_from_api(&mut api_client, expand, hide)?; if toml { @@ -284,6 +395,7 @@ fn run(opt: Opt) -> Result<()> { specifier, editgroup_id, json, + toml: _, editing_command, } => { // TODO: fetch editgroup, check if this entity is already being updated in it. If so, @@ -326,11 +438,83 @@ fn run(opt: Opt) -> Result<()> { .context("updating after edit")?; 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, + dry_run, + auto_accept, + editgroup_id, + }, + input_path, + limit, + } => { + unimplemented!("batch create") + }, + Command::Batch { + cmd: + BatchCommand::Update { + entity_type, + mutations, + batch_size, + dry_run, + auto_accept, + editgroup_id, + }, + input_path, + limit, + } => { + unimplemented!("batch update") + }, + Command::Batch { + cmd: + BatchCommand::Delete { + entity_type, + batch_size, + dry_run, + auto_accept, + editgroup_id, + }, + input_path, + limit, + } => { + unimplemented!("batch delete") + }, + Command::Batch { + cmd: + BatchCommand::Download { + // TODO + }, + input_path, + limit, + } => { + unimplemented!("batch create") + }, 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)?, + // XXX: _ => specifier, }; let file_entities = match specifier { @@ -411,6 +595,15 @@ fn run(opt: Opt) -> Result<()> { .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 { @@ -521,7 +714,7 @@ fn run(opt: Opt) -> Result<()> { println!("{}", eg.to_json_string()?); } Command::Status { json } => { - let status = api_client.status()?; + let status = ClientStatus::generate(&mut api_client)?; if json { println!("{}", serde_json::to_string(&status)?) } else { diff --git a/rust/fatcat-cli/src/specifier.rs b/rust/fatcat-cli/src/specifier.rs index c1d5b9f..c7a7b57 100644 --- a/rust/fatcat-cli/src/specifier.rs +++ b/rust/fatcat-cli/src/specifier.rs @@ -116,7 +116,8 @@ impl Specifier { ); // doi, wikidata, isbn13, pmid, pmcid, core, arxiv, jstor, ark, mag let result = api_client.rt.block_on(api_client.api.lookup_release( - doi, None, None, pmid, pmcid, None, arxiv, None, None, None, None, None, None, expand, hide, + doi, None, None, pmid, pmcid, None, arxiv, None, None, None, None, None, None, + expand, hide, ))?; match result { fatcat_openapi::LookupReleaseResponse::FoundEntity(model) => { @@ -355,6 +356,113 @@ impl Specifier { Err(_) => ret.with_context(|| format!("Failed to GET {:?}", self)), } } + + pub fn get_history( + &self, + api_client: &mut FatcatApiClient, + limit: Option<u64>, + ) -> Result<Vec<fatcat_openapi::models::EntityHistoryEntry>> { + let limit: Option<i64> = limit.map(|v| v as i64); + use Specifier::*; + let ret: Result<Vec<fatcat_openapi::models::EntityHistoryEntry>> = match self { + Release(fcid) => match api_client + .rt + .block_on(api_client.api.get_release_history(fcid.to_string(), limit))? + { + fatcat_openapi::GetReleaseHistoryResponse::FoundEntityHistory(entries) => { + Ok(entries) + } + fatcat_openapi::GetReleaseHistoryResponse::NotFound(err) => { + Err(anyhow!("Not Found: {}", err.message)) + } + resp => Err(anyhow!("{:?}", resp)) + .with_context(|| format!("API GET failed: {:?}", self)), + }, + Work(fcid) => match api_client + .rt + .block_on(api_client.api.get_work_history(fcid.to_string(), limit))? + { + 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))? + { + fatcat_openapi::GetContainerHistoryResponse::FoundEntityHistory(entries) => { + Ok(entries) + } + fatcat_openapi::GetContainerHistoryResponse::NotFound(err) => { + Err(anyhow!("Not Found: {}", err.message)) + } + resp => Err(anyhow!("{:?}", resp)) + .with_context(|| format!("API GET failed: {:?}", self)), + }, + Creator(fcid) => match api_client + .rt + .block_on(api_client.api.get_creator_history(fcid.to_string(), limit))? + { + fatcat_openapi::GetCreatorHistoryResponse::FoundEntityHistory(entries) => { + Ok(entries) + } + fatcat_openapi::GetCreatorHistoryResponse::NotFound(err) => { + Err(anyhow!("Not Found: {}", err.message)) + } + resp => Err(anyhow!("{:?}", resp)) + .with_context(|| format!("API GET failed: {:?}", self)), + }, + File(fcid) => match api_client + .rt + .block_on(api_client.api.get_file_history(fcid.to_string(), limit))? + { + fatcat_openapi::GetFileHistoryResponse::FoundEntityHistory(entries) => { + Ok(entries) + } + fatcat_openapi::GetFileHistoryResponse::NotFound(err) => { + Err(anyhow!("Not Found: {}", err.message)) + } + resp => Err(anyhow!("{:?}", resp)) + .with_context(|| format!("API GET failed: {:?}", self)), + }, + FileSet(fcid) => match api_client + .rt + .block_on(api_client.api.get_fileset_history(fcid.to_string(), limit))? + { + fatcat_openapi::GetFilesetHistoryResponse::FoundEntityHistory(entries) => { + Ok(entries) + } + fatcat_openapi::GetFilesetHistoryResponse::NotFound(err) => { + Err(anyhow!("Not Found: {}", err.message)) + } + 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))? + { + fatcat_openapi::GetWebcaptureHistoryResponse::FoundEntityHistory(entries) => { + Ok(entries) + } + fatcat_openapi::GetWebcaptureHistoryResponse::NotFound(err) => { + Err(anyhow!("Not Found: {}", err.message)) + } + resp => Err(anyhow!("{:?}", resp)) + .with_context(|| format!("API GET failed: {:?}", self)), + }, + _ => Err(anyhow!("Don't know how to look up history for: {:?}", self)), + }; + match ret { + Ok(_) => ret, + Err(_) => ret.with_context(|| format!("Failed to GET history: {:?}", self)), + } + } } impl FromStr for Specifier { |