aboutsummaryrefslogtreecommitdiffstats
path: root/rust
diff options
context:
space:
mode:
Diffstat (limited to 'rust')
-rw-r--r--rust/fatcat-cli/src/api.rs66
-rw-r--r--rust/fatcat-cli/src/commands.rs272
-rw-r--r--rust/fatcat-cli/src/download.rs17
-rw-r--r--rust/fatcat-cli/src/entities.rs38
-rw-r--r--rust/fatcat-cli/src/lib.rs190
-rw-r--r--rust/fatcat-cli/src/main.rs241
-rw-r--r--rust/fatcat-cli/src/specifier.rs110
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 {