diff --git a/ark-cli/Cargo.toml b/ark-cli/Cargo.toml index d6550931..08655206 100644 --- a/ark-cli/Cargo.toml +++ b/ark-cli/Cargo.toml @@ -9,7 +9,7 @@ bench = false [dependencies] tokio = { version = "1.35.1", features = ["full"] } -clap = { version = "3.0.10", features = ["derive"] } +clap = { version = "4.5", features = ["derive"] } env_logger = "0.9.0" fs_extra = "1.2.0" home = "0.5.3" diff --git a/ark-cli/src/cli.rs b/ark-cli/src/cli.rs new file mode 100644 index 00000000..6f89bfa1 --- /dev/null +++ b/ark-cli/src/cli.rs @@ -0,0 +1,18 @@ +use crate::commands::Commands; + +use clap::{builder::styling::AnsiColor, Parser}; + +#[derive(Parser, Debug)] +#[clap(name = "ark-cli")] +#[clap(about = "Manage ARK tag storages and indexes", styles=styles())] +pub struct Cli { + #[clap(subcommand)] + pub command: Commands, +} + +pub fn styles() -> clap::builder::Styles { + clap::builder::Styles::styled() + .header(AnsiColor::Yellow.on_default()) + .usage(AnsiColor::Yellow.on_default()) + .literal(AnsiColor::Green.on_default()) +} diff --git a/ark-cli/src/commands/backup.rs b/ark-cli/src/commands/backup.rs new file mode 100644 index 00000000..398220bd --- /dev/null +++ b/ark-cli/src/commands/backup.rs @@ -0,0 +1,89 @@ +use std::io::Write; +use std::path::PathBuf; + +use crate::{ + create_dir_all, dir, discover_roots, home_dir, storages_exists, timestamp, + AppError, CopyOptions, File, ARK_BACKUPS_PATH, ARK_FOLDER, + ROOTS_CFG_FILENAME, +}; + +#[derive(Clone, Debug, clap::Args)] +#[clap(name = "backup", about = "Backup the ark managed folder")] +pub struct Backup { + #[clap(value_parser, help = "Path to the root directory")] + roots_cfg: Option, +} + +impl Backup { + pub fn run(&self) -> Result<(), AppError> { + let timestamp = timestamp().as_secs(); + let backup_dir = home_dir() + .ok_or(AppError::HomeDirNotFound)? + .join(ARK_BACKUPS_PATH) + .join(timestamp.to_string()); + + if backup_dir.is_dir() { + println!("Wait at least 1 second, please!"); + std::process::exit(0) + } + + println!("Preparing backup:"); + let roots = discover_roots(&self.roots_cfg)?; + + let (valid, invalid): (Vec, Vec) = roots + .into_iter() + .partition(|root| storages_exists(root)); + + if !invalid.is_empty() { + println!("These folders don't contain any storages:"); + invalid + .into_iter() + .for_each(|root| println!("\t{}", root.display())); + } + + if valid.is_empty() { + println!("Nothing to backup. Bye!"); + std::process::exit(0) + } + + create_dir_all(&backup_dir).map_err(|_| { + AppError::BackupCreationError( + "Couldn't create backup directory!".to_owned(), + ) + })?; + + let mut roots_cfg_backup = + File::create(backup_dir.join(ROOTS_CFG_FILENAME))?; + + valid.iter().for_each(|root| { + let res = writeln!(roots_cfg_backup, "{}", root.display()); + if let Err(e) = res { + println!("Failed to write root to backup file: {}", e); + } + }); + + println!("Performing backups:"); + valid + .into_iter() + .enumerate() + .for_each(|(i, root)| { + println!("\tRoot {}", root.display()); + let storage_backup = backup_dir.join(i.to_string()); + + let mut options = CopyOptions::new(); + options.overwrite = true; + options.copy_inside = true; + + let result = + dir::copy(root.join(ARK_FOLDER), storage_backup, &options); + + if let Err(e) = result { + println!("\t\tFailed to copy storages!\n\t\t{}", e); + } + }); + + println!("Backup created:\n\t{}", backup_dir.display()); + + Ok(()) + } +} diff --git a/ark-cli/src/commands/collisions.rs b/ark-cli/src/commands/collisions.rs new file mode 100644 index 00000000..ea3c320a --- /dev/null +++ b/ark-cli/src/commands/collisions.rs @@ -0,0 +1,20 @@ +use std::path::PathBuf; + +use crate::{monitor_index, AppError}; + +#[derive(Clone, Debug, clap::Args)] +#[clap( + name = "collisions", + about = "Find collisions in the ark managed folder" +)] +pub struct Collisions { + #[clap(value_parser, help = "Path to the root directory")] + root_dir: Option, +} + +impl Collisions { + pub fn run(&self) -> Result<(), AppError> { + // FIXME: How does `monitor_index` handle `ark-cli collisions`? + monitor_index(&self.root_dir, None) + } +} diff --git a/ark-cli/src/commands/file/append.rs b/ark-cli/src/commands/file/append.rs new file mode 100644 index 00000000..46d0b132 --- /dev/null +++ b/ark-cli/src/commands/file/append.rs @@ -0,0 +1,60 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use crate::{ + models::storage::Storage, models::storage::StorageType, translate_storage, + AppError, Format, ResourceId, +}; + +use data_error::ArklibError; + +#[derive(Clone, Debug, clap::Args)] +#[clap(name = "append", about = "Append content to a resource")] +pub struct Append { + #[clap( + value_parser, + default_value = ".", + help = "Root directory of the ark managed folder" + )] + root_dir: PathBuf, + #[clap(help = "Storage name")] + storage: String, + #[clap(help = "ID of the resource to append to")] + id: String, + #[clap(help = "Content to append to the resource")] + content: String, + #[clap( + short, + long, + value_enum, + default_value = "raw", + help = "Format of the resource" + )] + format: Option, + #[clap(short, long, value_enum, help = "Storage kind of the resource")] + kind: Option, +} + +impl Append { + pub fn run(&self) -> Result<(), AppError> { + let (file_path, storage_type) = + translate_storage(&Some(self.root_dir.to_owned()), &self.storage) + .ok_or(AppError::StorageNotFound(self.storage.to_owned()))?; + + let storage_type = storage_type.unwrap_or(match self.kind { + Some(t) => t, + None => StorageType::File, + }); + + let format = self.format.unwrap(); + + let mut storage = Storage::new(file_path, storage_type)?; + + let resource_id = ResourceId::from_str(&self.id) + .map_err(|_e| AppError::ArklibError(ArklibError::Parse))?; + + storage.append(resource_id, &self.content, format)?; + + Ok(()) + } +} diff --git a/ark-cli/src/commands/file/insert.rs b/ark-cli/src/commands/file/insert.rs new file mode 100644 index 00000000..ff9b1ac9 --- /dev/null +++ b/ark-cli/src/commands/file/insert.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use crate::{ + models::storage::Storage, models::storage::StorageType, translate_storage, + AppError, Format, ResourceId, +}; + +use data_error::ArklibError; + +#[derive(Clone, Debug, clap::Args)] +#[clap(name = "insert", about = "Insert content into a resource")] +pub struct Insert { + #[clap( + value_parser, + default_value = ".", + help = "Root directory of the ark managed folder" + )] + root_dir: PathBuf, + #[clap(help = "Storage name")] + storage: String, + #[clap(help = "ID of the resource to append to")] + id: String, + #[clap(help = "Content to append to the resource")] + content: String, + #[clap(short, long, value_enum, help = "Format of the resource")] + format: Option, + #[clap(short, long, value_enum, help = "Storage kind of the resource")] + kind: Option, +} + +impl Insert { + pub fn run(&self) -> Result<(), AppError> { + let (file_path, storage_type) = + translate_storage(&Some(self.root_dir.to_owned()), &self.storage) + .ok_or(AppError::StorageNotFound(self.storage.to_owned()))?; + + let storage_type = storage_type.unwrap_or(match self.kind { + Some(t) => t, + None => StorageType::File, + }); + + let format = self.format.unwrap_or(Format::Raw); + + let mut storage = Storage::new(file_path, storage_type)?; + + let resource_id = ResourceId::from_str(&self.id) + .map_err(|_e| AppError::ArklibError(ArklibError::Parse))?; + + storage.insert(resource_id, &self.content, format)?; + + Ok(()) + } +} diff --git a/ark-cli/src/commands/file/mod.rs b/ark-cli/src/commands/file/mod.rs new file mode 100644 index 00000000..5cecfe74 --- /dev/null +++ b/ark-cli/src/commands/file/mod.rs @@ -0,0 +1,16 @@ +use clap::Subcommand; + +mod append; +mod insert; +mod read; +mod utils; + +/// Available commands for the `file` subcommand +#[derive(Subcommand, Debug)] +pub enum File { + Append(append::Append), + Insert(insert::Insert), + Read(read::Read), +} + +pub use utils::{file_append, file_insert, format_file, format_line}; diff --git a/ark-cli/src/commands/file/read.rs b/ark-cli/src/commands/file/read.rs new file mode 100644 index 00000000..8387d011 --- /dev/null +++ b/ark-cli/src/commands/file/read.rs @@ -0,0 +1,50 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use crate::{ + models::storage::Storage, models::storage::StorageType, translate_storage, + AppError, ResourceId, +}; + +use data_error::ArklibError; + +#[derive(Clone, Debug, clap::Args)] +#[clap(name = "read", about = "Read content from a resource")] +pub struct Read { + #[clap( + value_parser, + default_value = ".", + help = "Root directory of the ark managed folder" + )] + root_dir: PathBuf, + #[clap(help = "Storage name")] + storage: String, + #[clap(help = "ID of the resource to append to")] + id: String, + #[clap(short, long, value_enum, help = "Storage kind of the resource")] + kind: Option, +} + +impl Read { + pub fn run(&self) -> Result<(), AppError> { + let (file_path, storage_type) = + translate_storage(&Some(self.root_dir.to_owned()), &self.storage) + .ok_or(AppError::StorageNotFound(self.storage.to_owned()))?; + + let storage_type = storage_type.unwrap_or(match self.kind { + Some(t) => t, + None => StorageType::File, + }); + + let mut storage = Storage::new(file_path, storage_type)?; + + let resource_id = ResourceId::from_str(&self.id) + .map_err(|_e| AppError::ArklibError(ArklibError::Parse))?; + + let output = storage.read(resource_id)?; + + println!("{}", output); + + Ok(()) + } +} diff --git a/ark-cli/src/commands/file.rs b/ark-cli/src/commands/file/utils.rs similarity index 95% rename from ark-cli/src/commands/file.rs rename to ark-cli/src/commands/file/utils.rs index 4a5ec8f8..8a3c4048 100644 --- a/ark-cli/src/commands/file.rs +++ b/ark-cli/src/commands/file/utils.rs @@ -1,5 +1,6 @@ use crate::error::AppError; -use crate::models::{format, format::Format}; +use crate::models::key_value_to_str; +use crate::models::Format; use data_error::Result as ArklibResult; use fs_atomic_versions::atomic::{modify, modify_json, AtomicFile}; @@ -15,7 +16,7 @@ pub fn file_append( combined_vec })?), Format::KeyValue => { - let values = format::key_value_to_str(content)?; + let values = key_value_to_str(content)?; Ok(append_json(atomic_file, values.to_vec())?) } @@ -32,7 +33,7 @@ pub fn file_insert( Ok(modify(atomic_file, |_| content.as_bytes().to_vec())?) } Format::KeyValue => { - let values = format::key_value_to_str(content)?; + let values = key_value_to_str(content)?; modify_json( atomic_file, diff --git a/ark-cli/src/commands/link/create.rs b/ark-cli/src/commands/link/create.rs new file mode 100644 index 00000000..1164e26b --- /dev/null +++ b/ark-cli/src/commands/link/create.rs @@ -0,0 +1,39 @@ +use std::path::PathBuf; + +use crate::{commands::link::utils::create_link, provide_root, AppError}; + +#[derive(Clone, Debug, clap::Args)] +#[clap(name = "create", about = "Create a new link")] +pub struct Create { + #[clap(value_parser, help = "Root directory of the ark managed folder")] + root_dir: Option, + #[clap(help = "URL of the link")] + url: Option, + #[clap(help = "Title of the link")] + title: Option, + #[clap(help = "Description of the link")] + desc: Option, +} + +impl Create { + pub async fn run(&self) -> Result<(), AppError> { + let root = provide_root(&self.root_dir)?; + let url = self.url.as_ref().ok_or_else(|| { + AppError::LinkCreationError("Url was not provided".to_owned()) + })?; + let title = self.title.as_ref().ok_or_else(|| { + AppError::LinkCreationError("Title was not provided".to_owned()) + })?; + + println!("Saving link..."); + + match create_link(&root, url, title, self.desc.to_owned()).await { + Ok(_) => { + println!("Link saved successfully!"); + } + Err(e) => println!("{}", e), + } + + Ok(()) + } +} diff --git a/ark-cli/src/commands/link/load.rs b/ark-cli/src/commands/link/load.rs new file mode 100644 index 00000000..bee51af6 --- /dev/null +++ b/ark-cli/src/commands/link/load.rs @@ -0,0 +1,26 @@ +use std::path::PathBuf; + +use crate::{ + commands::link::utils::load_link, provide_root, AppError, ResourceId, +}; + +#[derive(Clone, Debug, clap::Args)] +#[clap(name = "create", about = "Create a new link")] +pub struct Load { + #[clap(value_parser, help = "Root directory of the ark managed folder")] + root_dir: Option, + #[clap(value_parser, help = "Path to the file to load")] + file_path: Option, + #[clap(help = "ID of the resource to load")] + id: Option, +} + +impl Load { + pub fn run(&self) -> Result<(), AppError> { + let root = provide_root(&self.root_dir)?; + let link = load_link(&root, &self.file_path, &self.id)?; + println!("Link data:\n{:?}", link); + + Ok(()) + } +} diff --git a/ark-cli/src/commands/link/mod.rs b/ark-cli/src/commands/link/mod.rs new file mode 100644 index 00000000..8c47efd0 --- /dev/null +++ b/ark-cli/src/commands/link/mod.rs @@ -0,0 +1,12 @@ +use clap::Subcommand; + +pub mod create; +mod load; +mod utils; + +/// Available commands for the `link` subcommand +#[derive(Subcommand, Debug)] +pub enum Link { + Create(create::Create), + Load(load::Load), +} diff --git a/ark-cli/src/commands/link.rs b/ark-cli/src/commands/link/utils.rs similarity index 94% rename from ark-cli/src/commands/link.rs rename to ark-cli/src/commands/link/utils.rs index 122f7f1c..4830219b 100644 --- a/ark-cli/src/commands/link.rs +++ b/ark-cli/src/commands/link/utils.rs @@ -38,6 +38,7 @@ pub fn load_link( Err(AppError::LinkLoadError(format!( "Path {:?} was requested. But id {} maps to path {:?}", path, + // FIXME: If `load_link` needs id, then it shouldn't be an optional parameter id.clone().unwrap(), path2, ))) diff --git a/ark-cli/src/commands/list.rs b/ark-cli/src/commands/list.rs new file mode 100644 index 00000000..acafc92c --- /dev/null +++ b/ark-cli/src/commands/list.rs @@ -0,0 +1,338 @@ +use std::io::Read; +use std::path::PathBuf; + +use crate::{ + provide_index, provide_root, read_storage_value, AppError, DateTime, + EntryOutput, File, Sort, StorageEntry, Utc, +}; + +#[derive(Clone, Debug, clap::Args)] +#[clap(name = "list", about = "List the resources in the ark managed folder")] +pub struct List { + #[clap(value_parser, help = "The path to the root directory")] + root_dir: Option, + #[clap(long, short = 'i', long = "id", action, help = "Show entries' IDs")] + entry_id: bool, + #[clap( + long, + short = 'p', + long = "path", + action, + help = "Show entries' paths" + )] + entry_path: bool, + #[clap( + long, + short = 'l', + long = "link", + action, + help = "Show entries' links" + )] + entry_link: bool, + #[clap(long, short, action, help = "Show entries' last modified times")] + modified: bool, + #[clap(long, short, action, help = "Show entries' tags")] + tags: bool, + #[clap(long, short, action, help = "Show entries' scores")] + scores: bool, + #[clap(long, value_enum, help = "Sort the entries by score")] + sort: Option, + #[clap(long, help = "Filter the entries by tag")] + filter: Option, +} + +impl List { + /// Get the entry output format + /// Default to Id + pub fn entry(&self) -> Result { + // Link can only be used alone + if self.entry_link { + if self.entry_id || self.entry_path { + return Err(AppError::InvalidEntryOption)?; + } else { + return Ok(EntryOutput::Link); + } + } + + if self.entry_id && self.entry_path { + Ok(EntryOutput::Both) + } else if self.entry_path { + Ok(EntryOutput::Path) + } else { + // Default to id + Ok(EntryOutput::Id) + } + } + + pub fn run(&self) -> Result<(), AppError> { + let root = provide_root(&self.root_dir)?; + let entry_output = self.entry()?; + + let mut storage_entries: Vec = provide_index(&root) + .map_err(|_| { + AppError::IndexError("Could not provide index".to_owned()) + })? + .read() + .map_err(|_| { + AppError::IndexError("Could not read index".to_owned()) + })? + .path2id + .iter() + .filter_map(|(path, resource)| { + let tags = if self.tags { + Some( + read_storage_value( + &root, + "tags", + &resource.id.to_string(), + &None, + ) + .map_or(vec![], |s| { + s.split(',') + .map(|s| s.trim().to_string()) + .collect::>() + }), + ) + } else { + None + }; + + let scores = if self.scores { + Some( + read_storage_value( + &root, + "scores", + &resource.id.to_string(), + &None, + ) + .map_or(0, |s| s.parse::().unwrap_or(0)), + ) + } else { + None + }; + + let datetime = if self.modified { + let format = "%b %e %H:%M %Y"; + Some( + DateTime::::from(resource.modified) + .format(format) + .to_string(), + ) + } else { + None + }; + + let (path, resource, content) = match entry_output { + EntryOutput::Both => ( + Some(path.to_owned().into_path_buf()), + Some(resource.clone().id), + None, + ), + EntryOutput::Path => { + (Some(path.to_owned().into_path_buf()), None, None) + } + EntryOutput::Id => (None, Some(resource.clone().id), None), + EntryOutput::Link => match File::open(path) { + Ok(mut file) => { + let mut contents = String::new(); + match file.read_to_string(&mut contents) { + Ok(_) => (None, None, Some(contents)), + Err(_) => return None, + } + } + Err(_) => return None, + }, + }; + + Some(StorageEntry { + path, + resource, + content, + tags, + scores, + datetime, + }) + }) + .collect::>(); + + match self.sort { + Some(Sort::Asc) => { + storage_entries.sort_by(|a, b| a.datetime.cmp(&b.datetime)) + } + + Some(Sort::Desc) => { + storage_entries.sort_by(|a, b| b.datetime.cmp(&a.datetime)) + } + None => (), + }; + + if let Some(filter) = &self.filter { + storage_entries.retain(|entry| { + entry + .tags + .as_ref() + .map(|tags| tags.contains(filter)) + .unwrap_or(false) + }); + } + + let no_tags = "NO_TAGS"; + let no_scores = "NO_SCORE"; + + let longest_path = storage_entries + .iter() + .map(|entry| { + if let Some(path) = entry.path.as_ref() { + path.display().to_string().len() + } else { + 0 + } + }) + .max_by(|a, b| a.cmp(b)) + .unwrap_or(0); + + let longest_id = storage_entries.iter().fold(0, |acc, entry| { + if let Some(resource) = &entry.resource { + let id_len = resource.to_string().len(); + if id_len > acc { + id_len + } else { + acc + } + } else { + acc + } + }); + + let longest_tags = storage_entries.iter().fold(0, |acc, entry| { + let tags_len = entry + .tags + .as_ref() + .map(|tags| { + if tags.is_empty() { + no_tags.len() + } else { + tags.join(", ").len() + } + }) + .unwrap_or(0); + if tags_len > acc { + tags_len + } else { + acc + } + }); + + let longest_scores = storage_entries.iter().fold(0, |acc, entry| { + let scores_len = entry + .scores + .as_ref() + .map(|score| { + if *score == 0 { + no_scores.len() + } else { + score.to_string().len() + } + }) + .unwrap_or(0); + if scores_len > acc { + scores_len + } else { + acc + } + }); + + let longest_datetime = storage_entries.iter().fold(0, |acc, entry| { + let datetime_len = entry + .datetime + .as_ref() + .map(|datetime| datetime.len()) + .unwrap_or(0); + if datetime_len > acc { + datetime_len + } else { + acc + } + }); + + let longest_content = storage_entries.iter().fold(0, |acc, entry| { + let content_len = entry + .content + .as_ref() + .map(|content| content.len()) + .unwrap_or(0); + if content_len > acc { + content_len + } else { + acc + } + }); + + for entry in &storage_entries { + let mut output = String::new(); + + if let Some(content) = &entry.content { + output.push_str(&format!( + "{:width$} ", + content, + width = longest_content + )); + } + + if let Some(path) = &entry.path { + output.push_str(&format!( + "{:width$} ", + path.display(), + width = longest_path + )); + } + + if let Some(resource) = &entry.resource { + output.push_str(&format!( + "{:width$} ", + resource.to_string(), + width = longest_id + )); + } + + if let Some(tags) = &entry.tags { + let tags_out = if tags.is_empty() { + no_tags.to_owned() + } else { + tags.join(", ") + }; + + output.push_str(&format!( + "{:width$} ", + tags_out, + width = longest_tags + )); + } + + if let Some(scores) = &entry.scores { + let scores_out = if *scores == 0 { + no_scores.to_owned() + } else { + scores.to_string() + }; + + output.push_str(&format!( + "{:width$} ", + scores_out, + width = longest_scores + )); + } + + if let Some(datetime) = &entry.datetime { + output.push_str(&format!( + "{:width$} ", + datetime, + width = longest_datetime + )); + } + + println!("{}", output); + } + Ok(()) + } +} diff --git a/ark-cli/src/commands/mod.rs b/ark-cli/src/commands/mod.rs index 7ee11691..eab0cc1a 100644 --- a/ark-cli/src/commands/mod.rs +++ b/ark-cli/src/commands/mod.rs @@ -1,2 +1,36 @@ +use clap::Subcommand; + +mod backup; +mod collisions; pub mod file; pub mod link; +mod list; +mod monitor; +mod render; +pub mod storage; + +pub use file::{file_append, file_insert, format_file, format_line}; + +#[derive(Debug, Subcommand)] +pub enum Commands { + Backup(backup::Backup), + Collisions(collisions::Collisions), + Monitor(monitor::Monitor), + Render(render::Render), + List(list::List), + #[command(about = "Manage links")] + Link { + #[clap(subcommand)] + subcommand: link::Link, + }, + #[command(about = "Manage files")] + File { + #[clap(subcommand)] + subcommand: file::File, + }, + #[command(about = "Manage storage")] + Storage { + #[clap(subcommand)] + subcommand: storage::Storage, + }, +} diff --git a/ark-cli/src/commands/monitor.rs b/ark-cli/src/commands/monitor.rs new file mode 100644 index 00000000..60d79b36 --- /dev/null +++ b/ark-cli/src/commands/monitor.rs @@ -0,0 +1,23 @@ +use std::path::PathBuf; + +use crate::{monitor_index, AppError}; + +#[derive(Clone, Debug, clap::Args)] +#[clap(name = "monitor", about = "Monitor the ark managed folder")] +pub struct Monitor { + #[clap(value_parser, help = "Path to the root directory")] + root_dir: Option, + #[clap( + default_value = "1000", + help = "Interval to check for changes in milliseconds" + )] + interval: Option, +} + +impl Monitor { + pub fn run(&self) -> Result<(), AppError> { + // SAFETY: interval is always Some since it has a default value in clap + let millis = self.interval.unwrap(); + monitor_index(&self.root_dir, Some(millis)) + } +} diff --git a/ark-cli/src/commands/render.rs b/ark-cli/src/commands/render.rs new file mode 100644 index 00000000..7f3fa9e6 --- /dev/null +++ b/ark-cli/src/commands/render.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use crate::{render_preview_page, AppError, File, PDFQuality}; + +#[derive(Clone, Debug, clap::Args)] +#[clap(name = "render", about = "Render a PDF file to an image")] +pub struct Render { + #[clap(value_parser, help = "The path to the file to render")] + path: PathBuf, + #[clap(help = "The quality of the rendering")] + quality: Option, +} + +impl Render { + pub fn run(&self) -> Result<(), AppError> { + let filepath = self.path.to_owned(); + let quality = match self.quality.to_owned().unwrap().as_str() { + "high" => Ok(PDFQuality::High), + "medium" => Ok(PDFQuality::Medium), + "low" => Ok(PDFQuality::Low), + _ => Err(AppError::InvalidRenderOption), + }?; + let buf = File::open(&filepath).map_err(|e| { + AppError::FileOperationError(format!("Failed to open file: {}", e)) + })?; + let dest_path = filepath.with_file_name( + filepath + .file_stem() + // SAFETY: we know that the file stem is valid UTF-8 because it is a file name + .unwrap() + .to_str() + .unwrap() + .to_owned() + + ".png", + ); + let img = render_preview_page(buf, quality); + img.save(dest_path).map_err(|e| { + AppError::FileOperationError(format!("Failed to save image: {}", e)) + })?; + Ok(()) + } +} diff --git a/ark-cli/src/commands/storage/list.rs b/ark-cli/src/commands/storage/list.rs new file mode 100644 index 00000000..6b0c6c4e --- /dev/null +++ b/ark-cli/src/commands/storage/list.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; + +use crate::{ + models::storage::Storage, models::storage::StorageType, translate_storage, + AppError, +}; + +#[derive(Clone, Debug, clap::Args)] +#[clap(name = "list", about = "List resources in a storage")] +pub struct List { + #[clap(value_parser, help = "Root directory of the ark managed folder")] + root_dir: Option, + #[clap(help = "Storage name")] + storage: Option, + #[clap(short, long, action = clap::ArgAction::SetTrue, help = "Print previous versions of the list")] + versions: bool, + #[clap(short, long, value_enum, help = "Storage kind of the resource")] + kind: Option, +} + +impl List { + pub fn run(&self) -> Result<(), AppError> { + let storage = + self.storage + .as_ref() + .ok_or(AppError::StorageCreationError( + "Storage was not provided".to_owned(), + ))?; + + let versions = self.versions; + + let (file_path, storage_type) = + translate_storage(&self.root_dir, storage) + .ok_or(AppError::StorageNotFound(storage.to_owned()))?; + + let storage_type = storage_type.unwrap_or(match self.kind { + Some(t) => t, + None => StorageType::File, + }); + + let mut storage = Storage::new(file_path, storage_type)?; + + storage.load()?; + + let output = storage.list(versions)?; + + println!("{}", output); + + Ok(()) + } +} diff --git a/ark-cli/src/commands/storage/mod.rs b/ark-cli/src/commands/storage/mod.rs new file mode 100644 index 00000000..dfef464a --- /dev/null +++ b/ark-cli/src/commands/storage/mod.rs @@ -0,0 +1,11 @@ +use clap::Subcommand; + +mod list; + +// FIXME: We should use new `fs-storage` crate to handle storage operations + +/// Available commands for the `storage` subcommand +#[derive(Subcommand, Debug)] +pub enum Storage { + List(list::List), +} diff --git a/ark-cli/src/index_registrar.rs b/ark-cli/src/index_registrar.rs index 0ab5678a..fc6a2e5b 100644 --- a/ark-cli/src/index_registrar.rs +++ b/ark-cli/src/index_registrar.rs @@ -1,7 +1,7 @@ use lazy_static::lazy_static; extern crate canonical_path; -use data_error::Result; +use data_error::{ArklibError, Result}; use fs_index::ResourceIndex; use std::collections::HashMap; @@ -25,7 +25,7 @@ pub fn provide_index>( let root_path = CanonicalPathBuf::canonicalize(root_path)?; { - let registrar = REGISTRAR.read().unwrap(); + let registrar = REGISTRAR.read().map_err(|_| ArklibError::Parse)?; if let Some(index) = registrar.get(&root_path) { log::info!("Index has been registered before"); @@ -36,7 +36,9 @@ pub fn provide_index>( log::info!("Index has not been registered before"); match ResourceIndex::provide(&root_path) { Ok(index) => { - let mut registrar = REGISTRAR.write().unwrap(); + let mut registrar = REGISTRAR.write().map_err(|_| { + ArklibError::Other(anyhow::anyhow!("Failed to lock registrar")) + })?; let arc = Arc::new(RwLock::new(index)); registrar.insert(root_path, arc.clone()); diff --git a/ark-cli/src/main.rs b/ark-cli/src/main.rs index a31cbc71..c8c718ca 100644 --- a/ark-cli/src/main.rs +++ b/ark-cli/src/main.rs @@ -1,7 +1,5 @@ use std::fs::{create_dir_all, File}; -use std::io::{Read, Write}; use std::path::PathBuf; -use std::str::FromStr; use crate::index_registrar::provide_index; use data_pdf::{render_preview_page, PDFQuality}; @@ -15,20 +13,27 @@ pub(crate) use dev_hash::Crc32 as ResourceId; use fs_atomic_versions::app_id; use fs_storage::ARK_FOLDER; +use anyhow::Result; + use chrono::prelude::DateTime; use chrono::Utc; -use clap::Parser; +use clap::CommandFactory; +use clap::FromArgMatches; use fs_extra::dir::{self, CopyOptions}; use home::home_dir; -use crate::models::cli::{Command, FileCommand, Link, StorageCommand}; -use crate::models::entry::EntryOutput; -use crate::models::format::Format; -use crate::models::sort::Sort; -use crate::models::storage::{Storage, StorageType}; +use crate::cli::Cli; +use crate::commands::file::File::{Append, Insert, Read}; +use crate::commands::link::Link::{Create, Load}; +use crate::commands::Commands::Link; +use crate::commands::Commands::Storage; +use crate::commands::Commands::*; +use crate::models::EntryOutput; +use crate::models::Format; +use crate::models::Sort; use crate::error::AppError; @@ -37,6 +42,7 @@ use util::{ storages_exists, timestamp, translate_storage, }; +mod cli; mod commands; mod error; mod index_registrar; @@ -56,569 +62,55 @@ struct StorageEntry { datetime: Option, } +async fn run() -> Result<()> { + let matches = Cli::command().get_matches(); + let cli = Cli::from_arg_matches(&matches)?; + match cli.command { + Backup(backup) => backup.run()?, + Collisions(collisions) => collisions.run()?, + Monitor(monitor) => monitor.run()?, + Render(render) => render.run()?, + List(list) => list.run()?, + Link { subcommand } => match subcommand { + Create(create) => create.run().await?, + Load(load) => load.run()?, + }, + crate::commands::Commands::File { subcommand } => match subcommand { + Append(append) => append.run()?, + Insert(insert) => insert.run()?, + Read(read) => read.run()?, + }, + Storage { subcommand } => match subcommand { + crate::commands::storage::Storage::List(list) => list.run()?, + }, + }; + + Ok(()) +} + #[tokio::main] async fn main() -> anyhow::Result<()> { - env_logger::init(); - - let args = models::cli::Cli::parse(); + env_logger::init_from_env( + env_logger::Env::default().default_filter_or("info"), + ); let app_id_dir = home_dir().ok_or(AppError::HomeDirNotFound)?; - let ark_dir = app_id_dir.join(".ark"); - if !ark_dir.exists() { std::fs::create_dir(&ark_dir) .map_err(|e| AppError::ArkDirectoryCreationError(e.to_string()))?; } println!("Loading app id at {}...", ark_dir.display()); - let _ = app_id::load(ark_dir) .map_err(|e| AppError::AppIdLoadError(e.to_string()))?; - match &args.command { - Command::List { - entry_id: _, - entry_path: _, - entry_link: _, - - root_dir, - modified, - tags, - scores, - sort, - filter, - } => { - let root = provide_root(root_dir)?; - let entry_output = &args.command.entry()?; - - let mut storage_entries: Vec = provide_index(&root) - .map_err(|_| { - AppError::IndexError("Could not provide index".to_owned()) - })? - .read() - .map_err(|_| { - AppError::IndexError("Could not read index".to_owned()) - })? - .path2id - .iter() - .filter_map(|(path, resource)| { - let tags = if *tags { - Some( - read_storage_value( - &root, - "tags", - &resource.id.to_string(), - &None, - ) - .map_or(vec![], |s| { - s.split(',') - .map(|s| s.trim().to_string()) - .collect::>() - }), - ) - } else { - None - }; - - let scores = if *scores { - Some( - read_storage_value( - &root, - "scores", - &resource.id.to_string(), - &None, - ) - .map_or(0, |s| s.parse::().unwrap_or(0)), - ) - } else { - None - }; - - let datetime = if *modified { - let format = "%b %e %H:%M %Y"; - Some( - DateTime::::from(resource.modified) - .format(format) - .to_string(), - ) - } else { - None - }; - - let (path, resource, content) = match entry_output { - EntryOutput::Both => ( - Some(path.to_owned().into_path_buf()), - Some(resource.clone().id), - None, - ), - EntryOutput::Path => { - (Some(path.to_owned().into_path_buf()), None, None) - } - EntryOutput::Id => { - (None, Some(resource.clone().id), None) - } - EntryOutput::Link => match File::open(path) { - Ok(mut file) => { - let mut contents = String::new(); - match file.read_to_string(&mut contents) { - Ok(_) => (None, None, Some(contents)), - Err(_) => return None, - } - } - Err(_) => return None, - }, - }; - - Some(StorageEntry { - path, - resource, - content, - tags, - scores, - datetime, - }) - }) - .collect::>(); - - match sort { - Some(Sort::Asc) => { - storage_entries.sort_by(|a, b| a.datetime.cmp(&b.datetime)) - } - - Some(Sort::Desc) => { - storage_entries.sort_by(|a, b| b.datetime.cmp(&a.datetime)) - } - None => (), - }; - - if let Some(filter) = filter { - storage_entries.retain(|entry| { - entry - .tags - .as_ref() - .map(|tags| tags.contains(filter)) - .unwrap_or(false) - }); - } - - let no_tags = "NO_TAGS"; - let no_scores = "NO_SCORE"; - - let longest_path = storage_entries - .iter() - .map(|entry| { - if let Some(path) = entry.path.as_ref() { - path.display().to_string().len() - } else { - 0 - } - }) - .max_by(|a, b| a.cmp(b)) - .unwrap_or(0); - - let longest_id = storage_entries.iter().fold(0, |acc, entry| { - if let Some(resource) = &entry.resource { - let id_len = resource.to_string().len(); - if id_len > acc { - id_len - } else { - acc - } - } else { - acc - } - }); - - let longest_tags = storage_entries.iter().fold(0, |acc, entry| { - let tags_len = entry - .tags - .as_ref() - .map(|tags| { - if tags.is_empty() { - no_tags.len() - } else { - tags.join(", ").len() - } - }) - .unwrap_or(0); - if tags_len > acc { - tags_len - } else { - acc - } - }); - - let longest_scores = - storage_entries.iter().fold(0, |acc, entry| { - let scores_len = entry - .scores - .as_ref() - .map(|score| { - if *score == 0 { - no_scores.len() - } else { - score.to_string().len() - } - }) - .unwrap_or(0); - if scores_len > acc { - scores_len - } else { - acc - } - }); - - let longest_datetime = - storage_entries.iter().fold(0, |acc, entry| { - let datetime_len = entry - .datetime - .as_ref() - .map(|datetime| datetime.len()) - .unwrap_or(0); - if datetime_len > acc { - datetime_len - } else { - acc - } - }); - - let longest_content = - storage_entries.iter().fold(0, |acc, entry| { - let content_len = entry - .content - .as_ref() - .map(|content| content.len()) - .unwrap_or(0); - if content_len > acc { - content_len - } else { - acc - } - }); - - for entry in &storage_entries { - let mut output = String::new(); - - if let Some(content) = &entry.content { - output.push_str(&format!( - "{:width$} ", - content, - width = longest_content - )); - } - - if let Some(path) = &entry.path { - output.push_str(&format!( - "{:width$} ", - path.display(), - width = longest_path - )); - } - - if let Some(resource) = &entry.resource { - output.push_str(&format!( - "{:width$} ", - resource.to_string(), - width = longest_id - )); - } - - if let Some(tags) = &entry.tags { - let tags_out = if tags.is_empty() { - no_tags.to_owned() - } else { - tags.join(", ") - }; - - output.push_str(&format!( - "{:width$} ", - tags_out, - width = longest_tags - )); - } - - if let Some(scores) = &entry.scores { - let scores_out = if *scores == 0 { - no_scores.to_owned() - } else { - scores.to_string() - }; - - output.push_str(&format!( - "{:width$} ", - scores_out, - width = longest_scores - )); - } - - if let Some(datetime) = &entry.datetime { - output.push_str(&format!( - "{:width$} ", - datetime, - width = longest_datetime - )); - } - - println!("{}", output); - } - } - Command::Backup { roots_cfg } => { - let timestamp = timestamp().as_secs(); - let backup_dir = home_dir() - .ok_or(AppError::HomeDirNotFound)? - .join(ARK_BACKUPS_PATH) - .join(timestamp.to_string()); - - if backup_dir.is_dir() { - println!("Wait at least 1 second, please!"); - std::process::exit(0) - } - - println!("Preparing backup:"); - let roots = discover_roots(roots_cfg)?; - - let (valid, invalid): (Vec, Vec) = roots - .into_iter() - .partition(|root| storages_exists(root)); - - if !invalid.is_empty() { - println!("These folders don't contain any storages:"); - invalid - .into_iter() - .for_each(|root| println!("\t{}", root.display())); - } - - if valid.is_empty() { - println!("Nothing to backup. Bye!"); - std::process::exit(0) - } - - create_dir_all(&backup_dir).map_err(|_| { - AppError::BackupCreationError( - "Couldn't create backup directory!".to_owned(), - ) - })?; - - let mut roots_cfg_backup = - File::create(backup_dir.join(ROOTS_CFG_FILENAME))?; - - valid.iter().for_each(|root| { - let res = writeln!(roots_cfg_backup, "{}", root.display()); - if let Err(e) = res { - println!("Failed to write root to backup file: {}", e); - } - }); - - println!("Performing backups:"); - valid - .into_iter() - .enumerate() - .for_each(|(i, root)| { - println!("\tRoot {}", root.display()); - let storage_backup = backup_dir.join(i.to_string()); - - let mut options = CopyOptions::new(); - options.overwrite = true; - options.copy_inside = true; - - let result = dir::copy( - root.join(ARK_FOLDER), - storage_backup, - &options, - ); - - if let Err(e) = result { - println!("\t\tFailed to copy storages!\n\t\t{}", e); - } - }); - - println!("Backup created:\n\t{}", backup_dir.display()); - } - Command::Collisions { root_dir } => monitor_index(root_dir, None)?, - Command::Monitor { root_dir, interval } => { - let millis = interval.unwrap_or(1000); - monitor_index(root_dir, Some(millis))? - } - Command::Render { path, quality } => { - let filepath = path.to_owned().unwrap(); - let quality = match quality.to_owned().unwrap().as_str() { - "high" => Ok(PDFQuality::High), - "medium" => Ok(PDFQuality::Medium), - "low" => Ok(PDFQuality::Low), - _ => Err(AppError::InvalidRenderOption), - }?; - let buf = File::open(&filepath).unwrap(); - let dest_path = filepath.with_file_name( - filepath - .file_stem() - .unwrap() - .to_str() - .unwrap() - .to_owned() - + ".png", - ); - let img = render_preview_page(buf, quality); - img.save(dest_path).unwrap(); - } - Command::Link(link) => match &link { - Link::Create { - root_dir, - url, - title, - desc, - } => { - let root = provide_root(root_dir)?; - let url = url.as_ref().ok_or_else(|| { - AppError::LinkCreationError( - "Url was not provided".to_owned(), - ) - })?; - let title = title.as_ref().ok_or_else(|| { - AppError::LinkCreationError( - "Title was not provided".to_owned(), - ) - })?; - - println!("Saving link..."); - - match commands::link::create_link( - &root, - url, - title, - desc.to_owned(), - ) - .await - { - Ok(_) => { - println!("Link saved successfully!"); - } - Err(e) => println!("{}", e), - } - } - - Link::Load { - root_dir, - file_path, - id, - } => { - let root = provide_root(root_dir)?; - let link = commands::link::load_link(&root, file_path, id)?; - println!("Link data:\n{:?}", link); - } - }, - Command::File(file) => match &file { - FileCommand::Append { - root_dir, - storage, - id, - content, - format, - type_, - } => { - let (file_path, storage_type) = - translate_storage(&Some(root_dir.to_owned()), storage) - .ok_or(AppError::StorageNotFound(storage.to_owned()))?; - - let storage_type = storage_type.unwrap_or(match type_ { - Some(t) => *t, - None => StorageType::File, - }); - - let format = format.unwrap_or(Format::Raw); - - let mut storage = Storage::new(file_path, storage_type)?; - - let resource_id = ResourceId::from_str(id)?; - - storage.append(resource_id, content, format)?; - } - - FileCommand::Insert { - root_dir, - storage, - id, - content, - format, - type_, - } => { - let (file_path, storage_type) = - translate_storage(&Some(root_dir.to_owned()), storage) - .ok_or(AppError::StorageNotFound(storage.to_owned()))?; - - let storage_type = storage_type.unwrap_or(match type_ { - Some(t) => *t, - None => StorageType::File, - }); - - let format = format.unwrap_or(Format::Raw); - - let mut storage = Storage::new(file_path, storage_type)?; - - let resource_id = ResourceId::from_str(id)?; - - storage.insert(resource_id, content, format)?; - } - - FileCommand::Read { - root_dir, - storage, - id, - type_, - } => { - let (file_path, storage_type) = - translate_storage(&Some(root_dir.to_owned()), storage) - .ok_or(AppError::StorageNotFound(storage.to_owned()))?; - - let storage_type = storage_type.unwrap_or(match type_ { - Some(t) => *t, - None => StorageType::File, - }); - - let mut storage = Storage::new(file_path, storage_type)?; - - let resource_id = ResourceId::from_str(id)?; - - let output = storage.read(resource_id)?; - - println!("{}", output); - } - }, - Command::Storage(cmd) => match &cmd { - StorageCommand::List { - root_dir, - storage, - type_, - versions, - } => { - let storage = - storage - .as_ref() - .ok_or(AppError::StorageCreationError( - "Storage was not provided".to_owned(), - ))?; - - let versions = versions.unwrap_or(false); - - let (file_path, storage_type) = - translate_storage(root_dir, storage) - .ok_or(AppError::StorageNotFound(storage.to_owned()))?; - - let storage_type = storage_type.unwrap_or(match type_ { - Some(t) => *t, - None => StorageType::File, - }); - - let mut storage = Storage::new(file_path, storage_type)?; - - storage.load()?; - - let output = storage.list(versions)?; - - println!("{}", output); - } - }, - }; + // Having a separate function for the main logic allows for easier + // error handling and testing. + if let Err(err) = run().await { + eprintln!("Error: {:#}", err); + std::process::exit(1); + } Ok(()) } diff --git a/ark-cli/src/models/cli.rs b/ark-cli/src/models/cli.rs deleted file mode 100644 index 83e52e18..00000000 --- a/ark-cli/src/models/cli.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::AppError; -use anyhow::Result; -use std::path::PathBuf; - -use crate::ResourceId; -use clap::{Parser, Subcommand}; - -use super::{ - entry::EntryOutput, format::Format, sort::Sort, storage::StorageType, -}; - -#[derive(Parser, Debug)] -#[clap(name = "ark-cli")] -#[clap(about = "Manage ARK tag storages and indexes", long_about = None)] -pub struct Cli { - #[clap(subcommand)] - pub command: Command, -} - -#[derive(Subcommand, Debug)] -pub enum Command { - Backup { - #[clap(parse(from_os_str))] - roots_cfg: Option, - }, - - Collisions { - #[clap(parse(from_os_str))] - root_dir: Option, - }, - - Monitor { - #[clap(parse(from_os_str))] - root_dir: Option, - interval: Option, - }, - - Render { - #[clap(parse(from_os_str))] - path: Option, - quality: Option, - }, - - List { - #[clap(parse(from_os_str))] - root_dir: Option, - - #[clap( - long, - short = 'i', - long = "id", - action, - help = "Show entries' IDs" - )] - entry_id: bool, - - #[clap( - long, - short = 'p', - long = "path", - action, - help = "Show entries' paths" - )] - entry_path: bool, - - #[clap( - long, - short = 'l', - long = "link", - action, - help = "Show entries' links" - )] - entry_link: bool, - - #[clap(long, short, action)] - modified: bool, - - #[clap(long, short, action)] - tags: bool, - - #[clap(long, short, action)] - scores: bool, - - #[clap(long, value_enum)] - sort: Option, - - #[clap(long)] - filter: Option, - }, - - #[clap(subcommand)] - Link(Link), - - #[clap(subcommand)] - File(FileCommand), - - #[clap(subcommand)] - Storage(StorageCommand), -} - -impl Command { - /// Get the entry output format - /// Default to Id - pub fn entry(&self) -> Result { - match self { - Command::List { - entry_id, - entry_path, - entry_link, - .. - } => { - // Link can only be used alone - if *entry_link { - if *entry_id || *entry_path { - return Err(AppError::InvalidEntryOption)?; - } else { - return Ok(EntryOutput::Link); - } - } - - if *entry_id && *entry_path { - Ok(EntryOutput::Both) - } else if *entry_path { - Ok(EntryOutput::Path) - } else { - // Default to id - Ok(EntryOutput::Id) - } - } - _ => Ok(EntryOutput::Id), - } - } -} - -#[derive(Subcommand, Debug)] -pub enum StorageCommand { - List { - #[clap(parse(from_os_str))] - root_dir: Option, - - storage: Option, - - #[clap(short, long)] - versions: Option, - - #[clap(short, long, value_enum)] - type_: Option, - }, -} - -#[derive(Subcommand, Debug)] -pub enum FileCommand { - Append { - #[clap(parse(from_os_str))] - root_dir: PathBuf, - - storage: String, - - id: String, - - content: String, - - #[clap(short, long, value_enum)] - format: Option, - - #[clap(short, long, value_enum)] - type_: Option, - }, - - Insert { - #[clap(parse(from_os_str))] - root_dir: PathBuf, - - storage: String, - - id: String, - - content: String, - - #[clap(short, long, value_enum)] - format: Option, - - #[clap(short, long, value_enum)] - type_: Option, - }, - - Read { - #[clap(parse(from_os_str))] - root_dir: PathBuf, - - storage: String, - - id: String, - - #[clap(short, long, value_enum)] - type_: Option, - }, -} - -#[derive(Subcommand, Debug)] -pub enum Link { - Create { - #[clap(parse(from_os_str))] - root_dir: Option, - - url: Option, - title: Option, - desc: Option, - }, - - Load { - #[clap(parse(from_os_str))] - root_dir: Option, - - #[clap(parse(from_os_str))] - file_path: Option, - - id: Option, - }, -} diff --git a/ark-cli/src/models/entry.rs b/ark-cli/src/models/entry.rs deleted file mode 100644 index 7b75fd9b..00000000 --- a/ark-cli/src/models/entry.rs +++ /dev/null @@ -1,9 +0,0 @@ -use clap::Parser; - -#[derive(Parser, Debug, Clone, Copy, PartialEq, Eq)] -pub enum EntryOutput { - Link, - Id, - Path, - Both, -} diff --git a/ark-cli/src/models/format.rs b/ark-cli/src/models/format.rs deleted file mode 100644 index 31c69f5e..00000000 --- a/ark-cli/src/models/format.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::error::InlineJsonParseError; - -#[derive(Debug, Clone, Copy, clap::ValueEnum)] -pub enum Format { - #[clap(name = "json")] - KeyValue, - #[clap(name = "raw")] - Raw, -} - -pub fn key_value_to_str( - s: &str, -) -> Result, InlineJsonParseError> { - let pairs: Vec<&str> = s.split(',').collect(); - - let mut values = Vec::new(); - - for pair in pairs { - let key_value: Vec<&str> = pair.split(':').collect(); - if key_value.len() == 2 { - let key = key_value[0].trim().to_string(); - let value = key_value[1].trim().to_string(); - values.push((key, value)); - } else { - return Err(InlineJsonParseError::InvalidKeyValPair); - } - } - - Ok(values) -} diff --git a/ark-cli/src/models/mod.rs b/ark-cli/src/models/mod.rs index bc37c45a..946a42ee 100644 --- a/ark-cli/src/models/mod.rs +++ b/ark-cli/src/models/mod.rs @@ -1,5 +1,48 @@ -pub mod cli; -pub mod entry; -pub mod format; -pub mod sort; pub mod storage; + +use clap::Parser; + +use crate::error::InlineJsonParseError; + +#[derive(Parser, Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntryOutput { + Link, + Id, + Path, + Both, +} + +#[derive(Parser, Debug, clap::ValueEnum, Clone)] +pub enum Sort { + Asc, + Desc, +} + +#[derive(Debug, Clone, Copy, clap::ValueEnum)] +pub enum Format { + #[clap(name = "json")] + KeyValue, + #[clap(name = "raw")] + Raw, +} + +pub fn key_value_to_str( + s: &str, +) -> Result, InlineJsonParseError> { + let pairs: Vec<&str> = s.split(',').collect(); + + let mut values = Vec::new(); + + for pair in pairs { + let key_value: Vec<&str> = pair.split(':').collect(); + if key_value.len() == 2 { + let key = key_value[0].trim().to_string(); + let value = key_value[1].trim().to_string(); + values.push((key, value)); + } else { + return Err(InlineJsonParseError::InvalidKeyValPair); + } + } + + Ok(values) +} diff --git a/ark-cli/src/models/sort.rs b/ark-cli/src/models/sort.rs deleted file mode 100644 index ddd6315c..00000000 --- a/ark-cli/src/models/sort.rs +++ /dev/null @@ -1,7 +0,0 @@ -use clap::Parser; - -#[derive(Parser, Debug, clap::ValueEnum, Clone)] -pub enum Sort { - Asc, - Desc, -} diff --git a/ark-cli/src/models/storage.rs b/ark-cli/src/models/storage.rs index 0519e9ed..48fc0464 100644 --- a/ark-cli/src/models/storage.rs +++ b/ark-cli/src/models/storage.rs @@ -4,12 +4,9 @@ use std::fmt::Write; use std::path::PathBuf; use crate::{ - commands::{ - self, - file::{format_file, format_line}, - }, + commands::{file_append, file_insert, format_file, format_line}, error::AppError, - models::format::Format, + models::Format, }; #[derive(Debug, Clone, Copy, clap::ValueEnum)] @@ -131,11 +128,7 @@ impl Storage { Format::Raw => format!("{}:{}\n", id, content), }; - match commands::file::file_append( - &atomic_file, - &content, - Format::Raw, - ) { + match file_append(&atomic_file, &content, Format::Raw) { Ok(_) => Ok(()), Err(e) => Err(e), } @@ -159,8 +152,7 @@ impl Storage { )) })?; - match commands::file::file_append(&atomic_file, content, format) - { + match file_append(&atomic_file, content, format) { Ok(_) => Ok(()), Err(e) => Err(e), } @@ -278,11 +270,7 @@ impl Storage { Format::Raw => format!("{}:{}\n", id, content), }; - match commands::file::file_insert( - &atomic_file, - &content, - Format::Raw, - ) { + match file_insert(&atomic_file, &content, Format::Raw) { Ok(_) => Ok(()), Err(e) => Err(e), } @@ -306,8 +294,7 @@ impl Storage { )) })?; - match commands::file::file_insert(&atomic_file, content, format) - { + match file_insert(&atomic_file, content, format) { Ok(_) => Ok(()), Err(e) => Err(e), } diff --git a/ark-cli/src/util.rs b/ark-cli/src/util.rs index 2b1a6d16..9a370167 100644 --- a/ark-cli/src/util.rs +++ b/ark-cli/src/util.rs @@ -73,7 +73,7 @@ pub fn provide_root(root_dir: &Option) -> Result { pub fn provide_index(root_dir: &PathBuf) -> ResourceIndex { let rwlock = crate::provide_index(root_dir).expect("Failed to retrieve index"); - let index = &*rwlock.read().unwrap(); + let index = &*rwlock.read().expect("Failed to lock index"); index.clone() } @@ -94,7 +94,11 @@ pub fn monitor_index( println!("Build succeeded in {:?}\n", duration); if let Some(millis) = interval { - let mut index = rwlock.write().unwrap(); + let mut index = rwlock.write().map_err(|_| { + AppError::StorageCreationError( + "Failed to write lock index".to_owned(), + ) + })?; loop { let pause = Duration::from_millis(millis); thread::sleep(pause); @@ -117,7 +121,11 @@ pub fn monitor_index( } } } else { - let index = rwlock.read().unwrap(); + let index = rwlock.read().map_err(|_| { + AppError::StorageCreationError( + "Failed to read lock index".to_owned(), + ) + })?; println!("Here are {} entries in the index", index.size());