diff --git a/Cargo.toml b/Cargo.toml index baba308d..b1942fc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "ark-cli", "data-error", "data-json", "data-link", @@ -14,6 +15,7 @@ members = [ ] default-members = [ + "ark-cli", "data-error", "data-json", "data-link", diff --git a/README.md b/README.md index 9f7b003e..073ee50b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The purpose of the library is to manage _resource index_ of folders with various | Package | Description | | --------------- | ---------------------------------------- | +| `ark-cli` | The CLI tool to interact with ark crates | | `data-resource` | Resource hashing and ID construction | | `fs-index` | Resource Index construction and updating | | `fs-storage` | Filesystem storage for resources | diff --git a/ark-cli/Cargo.toml b/ark-cli/Cargo.toml new file mode 100644 index 00000000..a40c119a --- /dev/null +++ b/ark-cli/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ark-cli" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "ark-cli" + +[dependencies] +tokio = { version = "1.35.1", features = ["full"] } +arklib = { git = "https://github.com/ARK-Builders/arklib", branch = "ark-rust_hot_fix" } +clap = { version = "3.0.10", features = ["derive"] } +env_logger = "0.9.0" +fs_extra = "1.2.0" +walkdir = "2.3.2" +home = "0.5.3" +url = { version = "2.2.2", features = ["serde"] } +serde_json = "1.0.82" +serde = { version = "1.0.138", features = ["derive"] } +chrono = "0.4.34" +anyhow = "1.0.80" +thiserror = "1.0.57" diff --git a/ark-cli/README.md b/ark-cli/README.md new file mode 100644 index 00000000..da191e1e --- /dev/null +++ b/ark-cli/README.md @@ -0,0 +1,111 @@ +# Ark-CLI + +### Installation + +To compile you will need openssl libraries and headers: + +```shell +# macOS (Homebrew) +$ brew install openssl@3 + +# macOS (MacPorts) +$ sudo port install openssl + +# macOS (pkgsrc) +$ sudo pkgin install openssl + +# Arch Linux +$ sudo pacman -S pkg-config openssl + +# Debian and Ubuntu +$ sudo apt-get install pkg-config libssl-dev + +# Fedora +$ sudo dnf install pkg-config perl-FindBin openssl-devel + +# Alpine Linux +$ apk add pkgconfig openssl-dev +``` + +### Usage + +```shell +ark-cli + +OPTIONS: + -h, --help Print help information + +SUBCOMMANDS: + backup + collisions + help Print this message or the help of the given subcommand(s) + link + monitor + render + +``` + +#### Backup +```shell +USAGE: + ark-cli backup [ROOTS_CFG] + +ARGS: + + +OPTIONS: + -h, --help Print help information +``` + +#### Collisions +```shell +USAGE: + ark-cli collisions [ROOT_DIR] + +ARGS: + + +OPTIONS: + -h, --help Print help information +``` + +#### Link +```shell +USAGE: + ark-cli link + +OPTIONS: + -h, --help Print help information + +SUBCOMMANDS: + create + help Print this message or the help of the given subcommand(s) + load +``` + +#### Monitor +```shell +USAGE: + ark-cli monitor [ARGS] + +ARGS: + + + +OPTIONS: + -h, --help Print help information +``` + +#### Render +```shell +USAGE: + ark-cli render [ARGS] + +ARGS: + + + +OPTIONS: + -h, --help Print help information + +``` \ No newline at end of file diff --git a/ark-cli/USAGE.md b/ark-cli/USAGE.md new file mode 100644 index 00000000..fe48aac2 --- /dev/null +++ b/ark-cli/USAGE.md @@ -0,0 +1,192 @@ +# Usage + +## Get started + +Create an empty dir: +``` +mkdir /tmp/test +cd /tmp/test +``` + +Let's fill it with something. One of the simplest ways to create resources it is to save a link to web page using `ark-cli link` command: +``` +$ ark-cli link create . http://google.com goo +$ ark-cli link create . http://duckduckgo.com duck +``` + +We can use `ark-cli list` to see just created resources: +``` +22-207093268 +18-1909444406 +``` + +These are just ids, derived from the URLs themselves. + +Now, the dir structure should resemble this: +``` +/tmp/test +└───.ark + ├───cache + │ ├───metadata + │ └───previews + │ + └───user + ├───properties + ├───scores + └───tags +``` + +### Label your data + +You can attach various metadata to your data, e.g. tags: +``` +$ ark-cli file append . tags 22-207093268 search,engine +``` + +The same way we can append scores: +``` +$ ark-cli file append . scores 22-207093268 15 +``` + +Generic metadata is possible using JSON-based properties: +``` +$ ark-cli file append . properties 22-207093268 favorites:false,ai:true --format=json +``` + +### Navigate your data + +The simplest command to observe your resources is `list`: +``` +$ ark-cli list + +18-1909444406 +22-207093268 +``` + +You can also target this command to other folders: +``` +$ ark-cli list ~/Pictures/ + +58922-3276384608 +62591-2492670715 +723145-720506115 +125308-3041567246 +``` + +But it's a bit boring and doesn't really tell anything, right? Various flags should be used to gain more knowledge about your collections of resources: +* `--entry=id|path|both|link` to show the path,the id or both of a resource +* `--modified` to show or not the last modified timestamp of a resource +* `--tags=true` to show or not the tags for every resource +* `--scores=true` to show or not the scores for every resource +* `--sort=asc|desc` to sort resources by asc or dsc order of scores +* `--filter=query` to filter resources by their tags + +For instance, you can list files with their paths and attached tags: +``` +$ ark-cli list -pt + +30-4257856154 search +18-1909444406 hello +22-207093268 search,engine +38-103010298 NO_TAGS +``` + +You Can list the links of the files + +``` +$ark-cli list -l + +https://google.com +https://news.ycombinator.com +https://youtube.com +https://github.com + +``` + +Or, sort by score: +``` +$ ark-cli list -s --sort=asc + +30-4257856154 NO_SCORE +18-1909444406 2 +38-103010298 10 +22-207093268 15 +``` + +Finally, you can filter resources using their tags: +``` +$ /tmp/ark-cli list -t --filter=search + +30-4257856154 search +22-207093268 search,engine +``` + +## :zap: Low-level utilities :zap: + +There are commands which could be useful with time, when you grasp the basic concepts. Some of these commands also can be useful for debugging [ArkLib](https://github.com/ARK-Builders/ark-rust). + +### Retrieve the metadata + +You can read these properties: +``` +$ ark-cli file read . properties 22-207093268 +{"ai":"true","desc":null,"favorites":"false","title":"duck"} +``` + +As well as scores or tags: +``` +$ ark-cli file read . scores 22-207093268 +15 +$ ark-cli file read . tags 22-207093268 +search,engine +``` + +### Inspect storages + +It's also possible to list resources having some metadata in a particular storage: +``` +$ ark-cli storage list . properties +22-207093268 +18-1909444406 + +$ ark-cli storage list . tags +22-207093268 + +$ ark-cli storage list . scores +22-207093268 +``` + +Note that, in this example, resource with id `18-1909444406` is listed only in `properties` storage since it lacks any metadata in `tags` and `scores` storages. The `ark-cli storage list` command only lists entries of a particular storage, not all resources. + +### Inspect versions + +For delving into history of storage mutations, we made `--versions` flag: +``` +$ ark-cli storage list . properties --versions=true +version name machine path +2 22-207093268 0592a937-a5d1-4843-8f03-ae0d6a9e77b5 ./.ark/user/properties/22-207093268/22-207093268_0592a937-a5d1-4843-8f03-ae0d6a9e77b5.2 +1 18-1909444406 0592a937-a5d1-4843-8f03-ae0d6a9e77b5 ./.ark/user/properties/18-1909444406/18-1909444406_0592a937-a5d1-4843-8f03-ae0d6a9e77b5.1 +``` + +Each storage mutation made by `ark-cli file append` or `ark-cli file insert` commands increases the number in `version` column. Versions help to prevent dirty-writes caused by using same storages by separate apps, or devices. + +The `properties` storage is _folder-based_, but same command can be used with _file-based_ storages like `tags`: +``` +$ ark-cli storage list . tags --versions=true +Loading app id at /home/kirill/.ark... +id value +22-207093268 search,engine + +$ ark-cli file append . tags 22-207093268 wow +$ ark-cli storage list . tags --versions=true +id value +22-207093268 search,engine +22-207093268 wow + +$ ark-cli file append . tags 22-207093268 one_more_time +$ ark-cli storage list . tags --versions=true +id value +22-207093268 search,engine +22-207093268 wow +22-207093268 one_more_time +``` diff --git a/ark-cli/ark-shelf/16-720383087 b/ark-cli/ark-shelf/16-720383087 new file mode 100644 index 00000000..4b23f03e --- /dev/null +++ b/ark-cli/ark-shelf/16-720383087 @@ -0,0 +1 @@ +http://bing.com/ \ No newline at end of file diff --git a/ark-cli/ark-shelf/18-1909444406 b/ark-cli/ark-shelf/18-1909444406 new file mode 100644 index 00000000..d2a553a6 --- /dev/null +++ b/ark-cli/ark-shelf/18-1909444406 @@ -0,0 +1 @@ +http://google.com/ \ No newline at end of file diff --git a/ark-cli/src/commands/file.rs b/ark-cli/src/commands/file.rs new file mode 100644 index 00000000..a9149137 --- /dev/null +++ b/ark-cli/src/commands/file.rs @@ -0,0 +1,142 @@ +use crate::error::AppError; +use crate::models::{format, format::Format}; +use arklib::{modify, modify_json, AtomicFile, Result as ArklibResult}; + +pub fn file_append( + atomic_file: &AtomicFile, + content: &str, + format: Format, +) -> Result<(), AppError> { + match format { + Format::Raw => Ok(modify(atomic_file, |current| { + let mut combined_vec: Vec = current.to_vec(); + combined_vec.extend_from_slice(content.as_bytes()); + combined_vec + })?), + Format::KeyValue => { + let values = format::key_value_to_str(content)?; + + Ok(append_json(atomic_file, values.to_vec())?) + } + } +} + +pub fn file_insert( + atomic_file: &AtomicFile, + content: &str, + format: Format, +) -> Result<(), AppError> { + match format { + Format::Raw => { + Ok(modify(atomic_file, |_| content.as_bytes().to_vec())?) + } + Format::KeyValue => { + let values = format::key_value_to_str(content)?; + + modify_json( + atomic_file, + |current: &mut Option| { + let mut new = serde_json::Map::new(); + for (key, value) in &values { + new.insert( + key.clone(), + serde_json::Value::String(value.clone()), + ); + } + *current = Some(serde_json::Value::Object(new)); + }, + ) + .map_err(|e| AppError::FileOperationError(e.to_string())) + } + } +} + +fn append_json( + atomic_file: &AtomicFile, + data: Vec<(String, String)>, +) -> ArklibResult<()> { + modify_json(atomic_file, |current: &mut Option| { + let current_data = match current { + Some(current) => { + if let Ok(value) = serde_json::to_value(current) { + match value { + serde_json::Value::Object(map) => Some(map), + _ => None, + } + } else { + None + } + } + + None => None, + }; + let mut new = serde_json::Map::new(); + + if current_data.is_none() { + for (key, value) in &data { + new.insert( + key.clone(), + serde_json::Value::String(value.clone()), + ); + } + *current = Some(serde_json::Value::Object(new)); + } else if let Some(values) = current_data { + for (key, value) in &values { + new.insert(key.clone(), value.clone()); + } + + for (key, value) in &data { + new.insert( + key.clone(), + serde_json::Value::String(value.clone()), + ); + } + *current = Some(serde_json::Value::Object(new)); + } + })?; + + Ok(()) +} + +pub fn format_line( + version: A, + name: B, + machine: C, + path: D, +) -> String +where + A: std::fmt::Display, + B: std::fmt::Display, + C: std::fmt::Display, + D: std::fmt::Display, +{ + format!("{: <8} {: <14} {: <36} {}", version, name, machine, path) +} + +pub fn format_file(file: &AtomicFile) -> Option { + let current = file.load().ok()?; + + if current.version == 0 { + return None; + } + + let mut split = current + .path + .file_name() + .expect("Not a file") + .to_str() + .unwrap() + .split('_'); + + let name = split.next().unwrap(); + + let machine = split.next().unwrap(); + let machine = &machine[..machine.len() - 2]; + + Some(format_line( + current.version, + name, + machine, + current.path.display(), + )) +} diff --git a/ark-cli/src/commands/link.rs b/ark-cli/src/commands/link.rs new file mode 100644 index 00000000..fee36cc5 --- /dev/null +++ b/ark-cli/src/commands/link.rs @@ -0,0 +1,54 @@ +use arklib::{id::ResourceId, link::Link}; +use std::path::PathBuf; +use url::Url; + +use crate::error::AppError; +use crate::util::provide_index; // Import your custom AppError type + +pub async fn create_link( + root: &PathBuf, + url: &str, + title: &str, + desc: Option, +) -> Result<(), AppError> { + let url = Url::parse(url) + .map_err(|_| AppError::LinkCreationError("Invalid url".to_owned()))?; + let link: Link = Link::new(url, title.to_owned(), desc.to_owned()); + link.save(root, true) + .await + .map_err(|e| AppError::LinkCreationError(e.to_string())) +} + +pub fn load_link( + root: &PathBuf, + file_path: &Option, + id: &Option, +) -> Result { + let path_from_index = id.map(|id| { + let index = provide_index(root); + index.id2path[&id].as_path().to_path_buf() + }); + let path_from_user = file_path; + + let path = match (path_from_user, path_from_index) { + (Some(path), Some(path2)) => { + if path.canonicalize()? != path2 { + Err(AppError::LinkLoadError(format!( + "Path {:?} was requested. But id {} maps to path {:?}", + path, + id.unwrap(), + path2, + ))) + } else { + Ok(path.to_path_buf()) + } + } + (Some(path), None) => Ok(path.to_path_buf()), + (None, Some(path)) => Ok(path), + (None, None) => Err(AppError::LinkLoadError( + "Provide a path or id for request.".to_owned(), + ))?, + }?; + + Ok(arklib::link::Link::load(root, &path)?) +} diff --git a/ark-cli/src/commands/mod.rs b/ark-cli/src/commands/mod.rs new file mode 100644 index 00000000..7ee11691 --- /dev/null +++ b/ark-cli/src/commands/mod.rs @@ -0,0 +1,2 @@ +pub mod file; +pub mod link; diff --git a/ark-cli/src/error.rs b/ark-cli/src/error.rs new file mode 100644 index 00000000..59bd92d6 --- /dev/null +++ b/ark-cli/src/error.rs @@ -0,0 +1,57 @@ +use arklib::ArklibError; +use std::io; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum InlineJsonParseError { + #[error("Invalid JSON: entries must be key-value pairs seperated by ':'")] + InvalidKeyValPair, +} + +#[derive(Debug, Error)] +pub enum AppError { + #[error("Couldn't retrieve home directory!")] + HomeDirNotFound, + + #[error("Couldn't create .ark directory: {0}")] + ArkDirectoryCreationError(String), + + #[error("Couldn't load app id: {0}")] + AppIdLoadError(String), + + #[error("Could not provide/read index: {0}")] + IndexError(String), + + #[error("Could not create storage: {0}")] + StorageCreationError(String), + + #[error("Failed to create link: {0}")] + LinkCreationError(String), + + #[error("Could not load link: {0}")] + LinkLoadError(String), + + #[error("File operation error: {0}")] + FileOperationError(String), + + #[error("Failed to create backup: {0}")] + BackupCreationError(String), + + #[error("Unknown render option")] + InvalidRenderOption, + + #[error("Storage not found: {0}")] + StorageNotFound(String), + + #[error("Invalid entry option")] + InvalidEntryOption, + + #[error(transparent)] + IoError(#[from] io::Error), + + #[error(transparent)] + ArklibError(#[from] ArklibError), + + #[error(transparent)] + InlineJsonParseError(#[from] InlineJsonParseError), +} diff --git a/ark-cli/src/main.rs b/ark-cli/src/main.rs new file mode 100644 index 00000000..51b441ea --- /dev/null +++ b/ark-cli/src/main.rs @@ -0,0 +1,623 @@ +use std::fs::{create_dir_all, File}; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::str::FromStr; + +use arklib::id::ResourceId; +use arklib::pdf::PDFQuality; +use arklib::{app_id, provide_index}; + +use chrono::prelude::DateTime; +use chrono::Utc; + +use clap::Parser; + +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::error::AppError; + +use util::{ + discover_roots, monitor_index, provide_root, read_storage_value, + storages_exists, timestamp, translate_storage, +}; + +mod commands; +mod error; +mod models; +mod util; + +const ARK_CONFIG: &str = ".config/ark"; +const ARK_BACKUPS_PATH: &str = ".ark-backups"; +const ROOTS_CFG_FILENAME: &str = "roots"; + +struct StorageEntry { + path: Option, + resource: Option, + content: Option, + tags: Option>, + scores: Option, + datetime: Option, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + let args = models::cli::Cli::parse(); + + 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, + entry_id, + entry_path, + entry_link, + + root_dir, + modified, + tags, + scores, + sort, + filter, + } => { + let root = provide_root(root_dir)?; + + let entry_output = match (entry, entry_id, entry_path, entry_link) { + (Some(e), false, false, false) => Ok(*e), + (None, true, false, false) => Ok(EntryOutput::Id), + (None, false, true, false) => Ok(EntryOutput::Path), + (None, true, true, false) => Ok(EntryOutput::Both), + (None, false, false, false) => Ok(EntryOutput::Id), + (None, false, false, true) => Ok(EntryOutput::Link), + _ => Err(AppError::InvalidEntryOption), + }?; + + 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.id), + None, + ), + EntryOutput::Path => { + (Some(path.to_owned().into_path_buf()), None, None) + } + EntryOutput::Id => (None, Some(resource.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(arklib::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 = arklib::pdf::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); + } + }, + }; + + Ok(()) +} diff --git a/ark-cli/src/models/cli.rs b/ark-cli/src/models/cli.rs new file mode 100644 index 00000000..24f3d485 --- /dev/null +++ b/ark-cli/src/models/cli.rs @@ -0,0 +1,169 @@ +use std::path::PathBuf; + +use arklib::id::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)] + entry: Option, + + #[clap(long, short = 'i', action)] + entry_id: bool, + + #[clap(long, short = 'p', action)] + entry_path: bool, + + #[clap(long, short = 'l', action)] + entry_link: bool, + + #[clap(long, short, action)] + modified: bool, + + #[clap(long, short, action)] + tags: bool, + + #[clap(long, short, action)] + scores: bool, + + #[clap(long)] + sort: Option, + + #[clap(long)] + filter: Option, + }, + + #[clap(subcommand)] + Link(Link), + + #[clap(subcommand)] + File(FileCommand), + + #[clap(subcommand)] + Storage(StorageCommand), +} + +#[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)] + 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)] + format: Option, + + #[clap(short, long)] + type_: Option, + }, + + Insert { + #[clap(parse(from_os_str))] + root_dir: PathBuf, + + storage: String, + + id: String, + + content: String, + + #[clap(short, long)] + format: Option, + + #[clap(short, long)] + type_: Option, + }, + + Read { + #[clap(parse(from_os_str))] + root_dir: PathBuf, + + storage: String, + + id: String, + + #[clap(short, long)] + 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 new file mode 100644 index 00000000..475909bf --- /dev/null +++ b/ark-cli/src/models/entry.rs @@ -0,0 +1,23 @@ +use clap::Parser; + +#[derive(Parser, Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntryOutput { + Link, + Id, + Path, + Both, +} + +impl std::str::FromStr for EntryOutput { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "id" => Ok(EntryOutput::Id), + "path" => Ok(EntryOutput::Path), + "both" => Ok(EntryOutput::Both), + "link" => Ok(EntryOutput::Link), + _ => Err("Entry output must be either 'id', 'path' or 'both'"), + } + } +} diff --git a/ark-cli/src/models/format.rs b/ark-cli/src/models/format.rs new file mode 100644 index 00000000..c8d6fb55 --- /dev/null +++ b/ark-cli/src/models/format.rs @@ -0,0 +1,40 @@ +use crate::error::InlineJsonParseError; + +#[derive(Debug, Clone, Copy)] +pub enum Format { + KeyValue, + Raw, +} + +impl std::str::FromStr for Format { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "json" => Ok(Format::KeyValue), + "raw" => Ok(Format::Raw), + _ => Err("Invalid format".to_owned()), + } + } +} + +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 new file mode 100644 index 00000000..bc37c45a --- /dev/null +++ b/ark-cli/src/models/mod.rs @@ -0,0 +1,5 @@ +pub mod cli; +pub mod entry; +pub mod format; +pub mod sort; +pub mod storage; diff --git a/ark-cli/src/models/sort.rs b/ark-cli/src/models/sort.rs new file mode 100644 index 00000000..000da162 --- /dev/null +++ b/ark-cli/src/models/sort.rs @@ -0,0 +1,19 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +pub enum Sort { + Asc, + Desc, +} + +impl std::str::FromStr for Sort { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "asc" => Ok(Sort::Asc), + "desc" => Ok(Sort::Desc), + _ => Err("Sort must be either 'asc' or 'desc'"), + } + } +} diff --git a/ark-cli/src/models/storage.rs b/ark-cli/src/models/storage.rs new file mode 100644 index 00000000..6e49d32b --- /dev/null +++ b/ark-cli/src/models/storage.rs @@ -0,0 +1,435 @@ +use arklib::{id::ResourceId, AtomicFile}; +use std::fmt::Write; +use std::path::PathBuf; + +use crate::{ + commands::{ + self, + file::{format_file, format_line}, + }, + error::AppError, + models::format::Format, +}; + +#[derive(Debug, Clone, Copy)] +pub enum StorageType { + File, + Folder, +} + +impl std::str::FromStr for StorageType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "file" => Ok(StorageType::File), + "folder" => Ok(StorageType::Folder), + _ => Err(format!("Invalid storage type: {}", s)), + } + } +} + +pub struct Storage { + path: PathBuf, + storage_type: StorageType, + files: Vec, +} + +impl Storage { + pub fn new>( + path: P, + storage_type: StorageType, + ) -> Result { + let path = path.into(); + + if !path.exists() { + std::fs::create_dir_all(&path).map_err(|e| { + AppError::StorageCreationError(format!( + "Failed to create storage folder at {:?} with error: {:?}", + path, e + )) + })?; + } + + Ok(Self { + path, + storage_type, + files: Vec::new(), + }) + } + + #[allow(dead_code)] + pub fn load(&mut self) -> Result<(), AppError> { + match self.storage_type { + StorageType::File => { + let atomic_file = AtomicFile::new(self.path.clone())?; + + let atomic_file_data = atomic_file.load()?; + + let data = atomic_file_data.read_to_string()?; + + for (i, line) in data.lines().enumerate() { + let mut line = line.split(':'); + let id = line.next().unwrap(); + match id.parse::().map_err(|_| { + AppError::IndexError(format!( + "Failed to parse ResourceId from line: {i}", + )) + }) { + Ok(id) => self.files.push(id), + Err(e) => { + eprintln!("Error parsing line {}: {}", i, e); + } + } + } + } + StorageType::Folder => { + let folder_entries = + std::fs::read_dir(&self.path).map_err(|e| { + AppError::FileOperationError(format!( + "Failed to read folder at {:?} with error: {:?}", + self.path, e + )) + })?; + + for entry in folder_entries { + let entry = entry.map_err(|e| { + AppError::FileOperationError(format!( + "Error reading folder entry: {:?}", + e + )) + })?; + + if let Some(file_name) = entry.file_name().to_str() { + let id = file_name.parse::().map_err(|_| { + AppError::IndexError(format!( + "Failed to parse ResourceId from folder entry: {:?}", + file_name + )) + })?; + self.files.push(id); + } + } + } + }; + + Ok(()) + } + + pub fn append( + &mut self, + id: ResourceId, + content: &str, + format: Format, + ) -> Result<(), AppError> { + match self.storage_type { + StorageType::File => { + let atomic_file = AtomicFile::new(&self.path).map_err(|e| { + AppError::FileOperationError(format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), + e + )) + })?; + + let content = match format { + Format::KeyValue => return Err( + AppError::StorageCreationError( + "Key value format is not supported for file storage" + .to_owned(), + ), + ), + Format::Raw => format!("{}:{}\n", id, content), + }; + + match commands::file::file_append( + &atomic_file, + &content, + Format::Raw, + ) { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + StorageType::Folder => { + let folder_path = self.path.join(id.to_string()); + if !folder_path.exists() { + std::fs::create_dir_all(&folder_path).map_err(|e| { + AppError::StorageCreationError(format!( + "Failed to create folder at {:?} with error: {:?}", + folder_path, e + )) + })?; + } + + let atomic_file = AtomicFile::new(&folder_path) + .map_err(|e| { + AppError::FileOperationError(format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + )) + })?; + + match commands::file::file_append(&atomic_file, content, format) + { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + } + } + + pub fn read(&mut self, id: ResourceId) -> Result { + match self.storage_type { + StorageType::File => { + let atomic_file = AtomicFile::new(&self.path).map_err(|e| { + AppError::FileOperationError(format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), + e + )) + })?; + + let atomic_file_data = atomic_file.load().map_err(|e| { + AppError::FileOperationError(format!( + "Failed to load atomic file at {:?} with error: {:?}", + self.path, e + )) + })?; + + let data = atomic_file_data.read_to_string().map_err(|_| { + AppError::FileOperationError( + "Could not read atomic file content.".to_string(), + ) + })?; + + for (i, line) in data.lines().enumerate() { + let mut line = line.split(':'); + let line_id: &str = line.next().unwrap(); + match line_id.parse::().map_err(|_| { + AppError::IndexError(format!( + "Failed to parse ResourceId from line: {i}", + )) + }) { + Ok(line_id) => { + if id == line_id { + let data = line.next().unwrap(); + return Ok(data.to_string()); + } + } + Err(e) => { + eprintln!("Error parsing line {}: {}", i, e); + } + } + } + + Err(AppError::StorageNotFound(format!( + "Resource with id {} not found", + id + ))) + } + StorageType::Folder => { + let folder_path = self.path.join(id.to_string()); + if !folder_path.exists() { + return Err(AppError::StorageNotFound(format!( + "Resource with id {} not found", + id + ))); + } + + let atomic_file = AtomicFile::new(&folder_path) + .map_err(|e| { + AppError::FileOperationError(format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + )) + })?; + + let atomic_file_data = atomic_file.load().map_err(|e| { + AppError::FileOperationError(format!( + "Failed to load atomic file at {:?} with error: {:?}", + self.path, e + )) + })?; + + let data = atomic_file_data.read_to_string().map_err(|_| { + AppError::FileOperationError( + "Could not read atomic file content.".to_string(), + ) + })?; + + Ok(data) + } + } + } + + pub fn insert( + &mut self, + id: ResourceId, + content: &str, + format: Format, + ) -> Result<(), AppError> { + match self.storage_type { + StorageType::File => { + let atomic_file = AtomicFile::new(&self.path).map_err(|e| { + AppError::FileOperationError(format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), + e + )) + })?; + + let content = match format { + Format::KeyValue => return Err( + AppError::StorageCreationError( + "Key value format is not supported for file storage" + .to_owned(), + ), + ), + Format::Raw => format!("{}:{}\n", id, content), + }; + + match commands::file::file_insert( + &atomic_file, + &content, + Format::Raw, + ) { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + StorageType::Folder => { + let folder_path = self.path.join(id.to_string()); + if !folder_path.exists() { + std::fs::create_dir_all(&folder_path).map_err(|e| { + AppError::StorageCreationError(format!( + "Failed to create folder at {:?} with error: {:?}", + folder_path, e + )) + })?; + } + + let atomic_file = AtomicFile::new(&folder_path) + .map_err(|e| { + AppError::FileOperationError(format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + )) + })?; + + match commands::file::file_insert(&atomic_file, content, format) + { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + } + } + + pub fn list(&self, versions: bool) -> Result { + let mut output = String::new(); + + if !versions { + for id in &self.files { + writeln!(output, "{}", id).map_err(|_| { + AppError::FileOperationError( + "Could not write to output".to_string(), + ) + })?; + } + } else { + match self.storage_type { + StorageType::File => { + let atomic_file = AtomicFile::new(&self.path) + .map_err(|e| { + AppError::FileOperationError(format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + )) + })?; + + let atomic_file_data = atomic_file.load().map_err(|e| { + AppError::FileOperationError(format!( + "Failed to load atomic file at {:?} with error: {:?}", + self.path, e + )) + })?; + + writeln!(output, "{: <16} value", "id").map_err(|_| { + AppError::FileOperationError( + "Could not write to output".to_string(), + ) + })?; + + let data = + atomic_file_data.read_to_string().map_err(|_| { + AppError::FileOperationError( + "Could not read atomic file content." + .to_string(), + ) + })?; + + for line in data.lines() { + let mut line = line.split(':'); + let id = line.next(); + let data = line.next(); + + if let (Some(id), Some(data)) = (id, data) { + writeln!(output, "{: <16} {}", id, data).map_err( + |_| { + AppError::FileOperationError( + "Could not write to output".to_string(), + ) + }, + )?; + } + } + } + StorageType::Folder => { + let folder_entries = std::fs::read_dir(&self.path) + .map_err(|e| { + AppError::FileOperationError(format!( + "Failed to read folder at {:?} with error: {:?}", + self.path, e + )) + })? + .filter_map(|v| v.ok()) + .filter(|e| { + if let Ok(ftype) = e.file_type() { + ftype.is_dir() + } else { + false + } + }) + .filter_map(|e| match AtomicFile::new(e.path()) { + Ok(file) => Some(file), + Err(_) => None, + }); + + writeln!( + output, + "{}", + format_line("version", "name", "machine", "path"), + ) + .map_err(|_| { + AppError::FileOperationError( + "Could not write to output".to_string(), + ) + })?; + + for entry in folder_entries { + if let Some(file) = format_file(&entry) { + writeln!(output, "{}", file).map_err(|_| { + AppError::FileOperationError( + "Could not write to output".to_string(), + ) + })?; + } + } + } + }; + } + + Ok(output) + } +} diff --git a/ark-cli/src/util.rs b/ark-cli/src/util.rs new file mode 100644 index 00000000..65905e76 --- /dev/null +++ b/ark-cli/src/util.rs @@ -0,0 +1,251 @@ +use arklib::id::ResourceId; +use arklib::index::ResourceIndex; +use arklib::{ + ARK_FOLDER, METADATA_STORAGE_FOLDER, PREVIEWS_STORAGE_FOLDER, + PROPERTIES_STORAGE_FOLDER, SCORE_STORAGE_FILE, STATS_FOLDER, + TAG_STORAGE_FILE, THUMBNAILS_STORAGE_FOLDER, +}; +use std::env::current_dir; +use std::fs::{canonicalize, metadata}; +use std::io::BufRead; +use std::io::BufReader; +use std::path::Path; +use std::str::FromStr; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::{fs::File, path::PathBuf}; + +use crate::error::AppError; +use crate::models::storage::{Storage, StorageType}; +use crate::ARK_CONFIG; + +pub fn discover_roots( + roots_cfg: &Option, +) -> Result, AppError> { + if let Some(path) = roots_cfg { + println!( + "\tRoots config provided explicitly:\n\t\t{}", + path.display() + ); + let config = File::open(path)?; + + Ok(parse_roots(config)) + } else if let Ok(config) = File::open(ARK_CONFIG) { + println!( + "\tRoots config was found automatically:\n\t\t{}", + &ARK_CONFIG + ); + + Ok(parse_roots(config)) + } else { + println!("\tRoots config wasn't found."); + + println!("Looking for a folder containing tag storage:"); + let path = + canonicalize(current_dir().expect("Can't open current directory!")) + .expect("Couldn't canonicalize working directory!"); + + let result = path.ancestors().find(|path| { + println!("\t{}", path.display()); + storages_exists(path) + }); + + if let Some(root) = result { + println!("Root folder found:\n\t{}", root.display()); + Ok(vec![root.to_path_buf()]) + } else { + println!("Root folder wasn't found."); + Ok(vec![]) + } + } +} + +pub fn provide_root(root_dir: &Option) -> Result { + if let Some(path) = root_dir { + Ok(path.clone()) + } else { + Ok(current_dir()?) + } +} + +// Read-only structure +pub fn provide_index(root_dir: &PathBuf) -> ResourceIndex { + let rwlock = + arklib::provide_index(root_dir).expect("Failed to retrieve index"); + let index = &*rwlock.read().unwrap(); + index.clone() +} + +pub fn monitor_index( + root_dir: &Option, + interval: Option, +) -> Result<(), AppError> { + let dir_path = provide_root(root_dir)?; + + println!("Building index of folder {}", dir_path.display()); + let start = Instant::now(); + + let result = arklib::provide_index(dir_path); + let duration = start.elapsed(); + + match result { + Ok(rwlock) => { + println!("Build succeeded in {:?}\n", duration); + + if let Some(millis) = interval { + let mut index = rwlock.write().unwrap(); + loop { + let pause = Duration::from_millis(millis); + thread::sleep(pause); + + let start = Instant::now(); + match index.update_all() { + Err(msg) => println!("Oops! {}", msg), + Ok(diff) => { + index.store().expect("Could not store index"); + let duration = start.elapsed(); + println!("Updating succeeded in {:?}\n", duration); + + if !diff.deleted.is_empty() { + println!("Deleted: {:?}", diff.deleted); + } + if !diff.added.is_empty() { + println!("Added: {:?}", diff.added); + } + } + } + } + } else { + let index = rwlock.read().unwrap(); + + println!("Here are {} entries in the index", index.size()); + + for (key, count) in index.collisions.iter() { + println!("Id {:?} calculated {} times", key, count); + } + } + } + Err(err) => println!("Failure: {:?}", err), + } + + Ok(()) +} + +pub fn storages_exists(path: &Path) -> bool { + let meta = metadata(path.join(arklib::ARK_FOLDER)); + if let Ok(meta) = meta { + return meta.is_dir(); + } + + false +} + +pub fn parse_roots(config: File) -> Vec { + BufReader::new(config) + .lines() + .filter_map(|line| match line { + Ok(path) => Some(PathBuf::from(path)), + Err(msg) => { + println!("{:?}", msg); + None + } + }) + .collect() +} + +pub fn timestamp() -> Duration { + let start = SystemTime::now(); + start + .duration_since(UNIX_EPOCH) + .expect("Time went backwards!") +} + +pub fn translate_storage( + root: &Option, + storage: &str, +) -> Option<(PathBuf, Option)> { + if let Ok(path) = PathBuf::from_str(storage) { + if path.exists() && path.is_dir() { + return Some((path, None)); + } + } + + match storage.to_lowercase().as_str() { + "tags" => Some(( + provide_root(root) + .ok()? + .join(ARK_FOLDER) + .join(TAG_STORAGE_FILE), + Some(StorageType::File), + )), + "scores" => Some(( + provide_root(root) + .ok()? + .join(ARK_FOLDER) + .join(SCORE_STORAGE_FILE), + Some(StorageType::File), + )), + "stats" => Some(( + provide_root(root) + .ok()? + .join(ARK_FOLDER) + .join(STATS_FOLDER), + Some(StorageType::Folder), + )), + "properties" => Some(( + provide_root(root) + .ok()? + .join(ARK_FOLDER) + .join(PROPERTIES_STORAGE_FOLDER), + Some(StorageType::Folder), + )), + "metadata" => Some(( + provide_root(root) + .ok()? + .join(ARK_FOLDER) + .join(METADATA_STORAGE_FOLDER), + Some(StorageType::Folder), + )), + "previews" => Some(( + provide_root(root) + .ok()? + .join(ARK_FOLDER) + .join(PREVIEWS_STORAGE_FOLDER), + Some(StorageType::Folder), + )), + "thumbnails" => Some(( + provide_root(root) + .ok()? + .join(ARK_FOLDER) + .join(THUMBNAILS_STORAGE_FOLDER), + Some(StorageType::Folder), + )), + _ => None, + } +} + +pub fn read_storage_value( + root_dir: &PathBuf, + storage: &str, + id: &str, + type_: &Option, +) -> Result { + 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(type_) => match type_.to_lowercase().as_str() { + "file" => StorageType::File, + "folder" => StorageType::Folder, + _ => panic!("unknown storage type"), + }, + None => StorageType::File, + }); + + let mut storage = Storage::new(file_path, storage_type)?; + + let resource_id = ResourceId::from_str(id)?; + + storage.read(resource_id) +}