diff --git a/src/commands/file.rs b/src/commands/file.rs index afdc605..472067b 100644 --- a/src/commands/file.rs +++ b/src/commands/file.rs @@ -1,6 +1,6 @@ use crate::parsers::{self, Format}; use arklib::{modify, modify_json, AtomicFile}; -use std::{fmt::Write, path::PathBuf, process::Output}; +use std::{fmt::Write, path::PathBuf}; use walkdir::WalkDir; pub fn file_append( @@ -15,7 +15,7 @@ pub fn file_append( combined_vec }) .map_err(|_| "ERROR: Could not append string".to_string()), - parsers::Format::Json => { + parsers::Format::KeyValue => { let values = parsers::key_value_to_str(&content) .map_err(|_| "ERROR: Could not parse json".to_string())?; @@ -35,7 +35,7 @@ pub fn file_insert( modify(&atomic_file, |_| content.as_bytes().to_vec()) .map_err(|_| "ERROR: Could not insert string".to_string()) } - parsers::Format::Json => { + parsers::Format::KeyValue => { let values = parsers::key_value_to_str(&content) .map_err(|_| "ERROR: Could not parse json".to_string())?; @@ -57,6 +57,7 @@ pub fn file_insert( } } +#[allow(dead_code)] pub fn file_read( atomic_file: &AtomicFile, key: &Option, @@ -107,6 +108,7 @@ pub fn file_read( } } +#[allow(dead_code)] pub fn file_list(path: PathBuf, versions: &bool) -> Result { let mut output = String::new(); @@ -127,11 +129,13 @@ pub fn file_list(path: PathBuf, versions: &bool) -> Result { output, "{}", format_line("version", "name", "machine", "path"), - ); + ) + .map_err(|_| "Could not write to output".to_string())?; for file in files { if let Some(file) = format_file(&file) { - writeln!(output, "{}", file); + writeln!(output, "{}", file) + .map_err(|_| "Could not write to output".to_string())?; } } } else { @@ -144,7 +148,8 @@ pub fn file_list(path: PathBuf, versions: &bool) -> Result { .unwrap() .to_str() .unwrap() - ); + ) + .map_err(|_| "Could not write to output".to_string())?; } } @@ -198,7 +203,12 @@ fn append_json( Ok(()) } -fn format_line(version: A, name: B, machine: C, path: D) -> String +pub fn format_line( + version: A, + name: B, + machine: C, + path: D, +) -> String where A: std::fmt::Display, B: std::fmt::Display, @@ -208,7 +218,7 @@ where format!("{: <8} {: <14} {: <36} {}", version, name, machine, path) } -fn format_file(file: &AtomicFile) -> Option { +pub fn format_file(file: &AtomicFile) -> Option { let current = file.load().ok()?; if current.version == 0 { diff --git a/src/main.rs b/src/main.rs index f0009ff..6375d99 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,20 +12,22 @@ use arklib::id::ResourceId; use arklib::index::ResourceIndex; use arklib::pdf::PDFQuality; use arklib::{ - modify, modify_json, AtomicFile, APP_ID_FILE, ARK_FOLDER, FAVORITES_FILE, - METADATA_STORAGE_FOLDER, PREVIEWS_STORAGE_FOLDER, + ARK_FOLDER, METADATA_STORAGE_FOLDER, PREVIEWS_STORAGE_FOLDER, PROPERTIES_STORAGE_FOLDER, SCORE_STORAGE_FILE, STATS_FOLDER, TAG_STORAGE_FILE, THUMBNAILS_STORAGE_FOLDER, }; use clap::{Parser, Subcommand}; use fs_extra::dir::{self, CopyOptions}; use home::home_dir; -use std::io::{Result, Write}; -use url::Url; -use walkdir::WalkDir; +use std::io::Write; +use storage::StorageType; + +use crate::parsers::Format; +use crate::storage::Storage; mod commands; mod parsers; +mod storage; #[derive(Parser, Debug)] #[clap(name = "ark-cli")] @@ -71,25 +73,38 @@ enum FileCommand { Append { storage: String, - content: Option, + id: String, + + content: String, #[clap(short, long)] format: Option, + + #[clap(short, long)] + type_: Option, }, Insert { storage: String, - content: Option, + id: String, + + content: String, #[clap(short, long)] format: Option, + + #[clap(short, long)] + type_: Option, }, Read { storage: String, - key: Option, + id: String, + + #[clap(short, long)] + type_: Option, }, List { @@ -97,6 +112,9 @@ enum FileCommand { #[clap(short, long)] versions: bool, + + #[clap(short, long)] + type_: Option, }, } @@ -292,151 +310,183 @@ async fn main() { Command::File(file) => match &file { FileCommand::Append { storage, + id, content, format, + type_, } => { - let file_path = translate_storage(storage) + let (file_path, storage_type) = translate_storage(storage) .expect("ERROR: Could not find storage folder"); - let atomic_file = AtomicFile::new(&file_path) - .expect("ERROR: Could not create atomic file"); + 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 format = parsers::get_format(&format) - .expect("ERROR: Format must be either 'json' or 'raw'"); + let format = + parsers::get_format(&format).unwrap_or(Format::Raw); - let content = content - .as_ref() - .expect("ERROR: Content was not provided"); + let mut storage = Storage::new(file_path, storage_type) + .expect("ERROR: Could not create storage"); - commands::file::file_append(&atomic_file, content, format) - .map_err(|e| println!("ERROR: {}", e)) + let resource_id = ResourceId::from_str(id) + .expect("ERROR: Could not parse id"); + + storage + .append(resource_id, content, format) .unwrap(); } FileCommand::Insert { storage, + id, content, format, + type_, } => { - let file_path = match translate_storage(storage) { - Some(path) => path, - None => { - let path = PathBuf::from_str(storage) - .expect("ERROR: Could not create storage path"); - create_dir_all(&path).expect( - "ERROR: Could not create storage directory", - ); - path - } - }; + let (file_path, storage_type) = translate_storage(storage) + .expect("ERROR: Could not find storage folder"); - let atomic_file = AtomicFile::new(&file_path) - .expect("ERROR: Could not create atomic file"); + 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 format = parsers::get_format(&format) - .expect("ERROR: Format must be either 'json' or 'raw'"); + let format = + parsers::get_format(&format).unwrap_or(Format::Raw); - let content = content - .as_ref() - .expect("ERROR: Content was not provided"); + let mut storage = Storage::new(file_path, storage_type) + .expect("ERROR: Could not create storage"); - match commands::file::file_insert(&atomic_file, content, format) - { - Ok(_) => { - println!("File inserted successfully!"); - } - Err(e) => println!("ERROR: {}", e), - } + let resource_id = ResourceId::from_str(id) + .expect("ERROR: Could not parse id"); + + storage + .insert(resource_id, content, format) + .unwrap(); } - FileCommand::Read { storage, key } => { - let file_path = translate_storage(storage) + FileCommand::Read { storage, id, type_ } => { + let (file_path, storage_type) = translate_storage(storage) .expect("ERROR: Could not find storage folder"); - let atomic_file = AtomicFile::new(&file_path) - .expect("ERROR: Could not create atomic file"); + 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, + }); - match commands::file::file_read(&atomic_file, key) { - Ok(output) => { - println!("{}", output); - } + let mut storage = Storage::new(file_path, storage_type) + .expect("ERROR: Could not create storage"); + + let resource_id = ResourceId::from_str(id) + .expect("ERROR: Could not parse id"); + + let output = storage.read(resource_id); + + match output { + Ok(output) => println!("{}", output), Err(e) => println!("ERROR: {}", e), } } - FileCommand::List { storage, versions } => { - let file_path = translate_storage(storage) + FileCommand::List { + storage, + type_, + versions, + } => { + let (file_path, storage_type) = translate_storage(storage) .expect("ERROR: Could not find storage folder"); - match commands::file::file_list(file_path, versions) { - Ok(output) => { - println!("{}", output); - } - Err(e) => println!("ERROR: {}", e), - } + 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) + .expect("ERROR: Could not create storage"); + + storage + .load() + .expect("ERROR: Could not load storage"); + + let output = storage + .list(*versions) + .expect("ERROR: Could not list storage content"); + + println!("{}", output); } }, } } -fn translate_storage(storage: &String) -> Option { - if let Ok(path) = PathBuf::from_str(&storage) { +fn translate_storage(storage: &str) -> Option<(PathBuf, Option)> { + if let Ok(path) = PathBuf::from_str(storage) { if path.exists() && path.is_dir() { - return Some(path); + return Some((path, None)); } } - let root = provide_root(&None); - if let Some(file) = WalkDir::new(root) - .into_iter() - .filter_entry(|e| e.file_type().is_dir()) - .filter_map(|v| v.ok()) - .find(|f| { - f.file_name().to_str().unwrap().to_lowercase() == storage.as_str() - }) - { - return Some(file.path().to_path_buf()); - } - match storage.to_lowercase().as_str() { - "tags" => Some( + "tags" => Some(( provide_root(&None) .join(ARK_FOLDER) .join(TAG_STORAGE_FILE), - ), - "scores" => Some( + Some(StorageType::File), + )), + "scores" => Some(( provide_root(&None) .join(ARK_FOLDER) .join(SCORE_STORAGE_FILE), - ), - "stats" => Some( + Some(StorageType::File), + )), + "stats" => Some(( provide_root(&None) .join(ARK_FOLDER) .join(STATS_FOLDER), - ), - "properties" => Some( + Some(StorageType::Folder), + )), + "properties" => Some(( provide_root(&None) .join(ARK_FOLDER) .join(PROPERTIES_STORAGE_FOLDER), - ), - "metadata" => Some( + Some(StorageType::Folder), + )), + "metadata" => Some(( provide_root(&None) .join(ARK_FOLDER) .join(METADATA_STORAGE_FOLDER), - ), - "previews" => Some( + Some(StorageType::Folder), + )), + "previews" => Some(( provide_root(&None) .join(ARK_FOLDER) .join(PREVIEWS_STORAGE_FOLDER), - ), - "thumbnails" => Some( + Some(StorageType::Folder), + )), + "thumbnails" => Some(( provide_root(&None) .join(ARK_FOLDER) .join(THUMBNAILS_STORAGE_FOLDER), - ), + Some(StorageType::Folder), + )), _ => None, } - .filter(|path| path.exists() && path.is_dir()) } fn discover_roots(roots_cfg: &Option) -> Vec { @@ -577,40 +627,3 @@ fn timestamp() -> Duration { .duration_since(UNIX_EPOCH) .expect("Time went backwards!"); } - -// createa test for the transalte_storage function -// Define the test module -#[cfg(test)] -mod tests { - // Import necessary items for testing - use super::*; - - // Define a test function - #[test] - fn test_translate_storage() { - let test_dir = - provide_root(&None).join(PathBuf::from_str("./test_dir").unwrap()); - let ark_dir = test_dir.join(ARK_FOLDER); - - // Creating a test atomic file - let hello_dir = ark_dir.join("hello"); - create_dir_all(&hello_dir).unwrap(); - - assert_eq!( - translate_storage(&"hello".to_string()) - .unwrap_or(PathBuf::from_str(".").unwrap()), - hello_dir - ); - - assert!( - translate_storage(&"./test_dir/.ark/hello".to_string()).is_some() - ); - - assert!(translate_storage(&"./test_dir/.ark/nonexist".to_string()) - .is_none()); - - assert!(translate_storage(&"metadata".to_string()).is_some()); - - assert!(translate_storage(&"properties".to_string()).is_some()); - } -} diff --git a/src/parsers/mod.rs b/src/parsers/mod.rs index 171672c..40727db 100644 --- a/src/parsers/mod.rs +++ b/src/parsers/mod.rs @@ -1,5 +1,5 @@ pub enum Format { - Json, + KeyValue, Raw, } @@ -26,7 +26,7 @@ pub fn get_format(s: &Option) -> Option { match s { Some(value) => { if value.to_lowercase() == "json" { - Some(Format::Json) + Some(Format::KeyValue) } else { None } diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..0c63b48 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,409 @@ +use arklib::{id::ResourceId, AtomicFile}; +use std::fmt::Write; +use std::path::PathBuf; + +use crate::{ + commands::{ + self, + file::{format_file, format_line}, + }, + parsers::Format, +}; + +#[derive(Debug)] +pub enum StorageType { + File, + Folder, +} + +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| { + format!( + "Failed to create storage folder at {:?} with error: {:?}", + path, e + ) + })?; + } + + Ok(Self { + path: path.into(), + storage_type, + files: Vec::new(), + }) + } + + #[allow(dead_code)] + pub fn load(&mut self) -> Result<(), String> { + match self.storage_type { + StorageType::File => { + let atomic_file = + AtomicFile::new(self.path.clone()).map_err(|e| { + format!( + "Failed to create atomic file at {:?} with error: {:?}", + self.path, e + ) + })?; + + let atomic_file_data = atomic_file.load().map_err(|e| { + format!( + "Failed to load atomic file at {:?} with error: {:?}", + self.path, e + ) + })?; + + let data = atomic_file_data.read_to_string().map_err(|_| { + "Could not read atomic file content.".to_string() + })?; + + for (i, line) in data.lines().enumerate() { + let mut line = line.split(':'); + let id = line.next().unwrap(); + let id = id.parse::().map_err(|_| { + format!("Failed to parse ResourceId from line: {i}",) + })?; + self.files.push(id); + } + } + StorageType::Folder => { + let folder_entries = + std::fs::read_dir(&self.path).map_err(|e| { + format!( + "Failed to read folder at {:?} with error: {:?}", + self.path, e + ) + })?; + + for entry in folder_entries { + let entry = entry.map_err(|e| { + format!("Error reading folder entry: {:?}", e) + })?; + + if let Some(file_name) = entry.file_name().to_str() { + let id = file_name.parse::().map_err(|_| { + 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<(), String> { + match self.storage_type { + StorageType::File => { + let atomic_file = AtomicFile::new(&self.path) + // .expect("ERROR: Could not create atomic file"); + .map_err(|e| { + format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + ) + })?; + + let content = match format { + Format::KeyValue => return Err( + "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, + crate::parsers::Format::Raw, + ) { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return 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| { + format!( + "Failed to create folder at {:?} with error: {:?}", + folder_path, e + ) + })?; + } + + let atomic_file = AtomicFile::new(&folder_path) + // .expect("ERROR: Could not create atomic file"); + .map_err(|e| { + format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + ) + })?; + + match commands::file::file_append( + &atomic_file, + &content, + format, + ) { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return Err(e); + } + } + } + }; + } + + pub fn read(&mut self, id: ResourceId) -> Result { + match self.storage_type { + StorageType::File => { + let atomic_file = AtomicFile::new(&self.path) + // .expect("ERROR: Could not create atomic file"); + .map_err(|e| { + format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + ) + })?; + + let atomic_file_data = atomic_file.load().map_err(|e| { + format!( + "Failed to load atomic file at {:?} with error: {:?}", + self.path, e + ) + })?; + + let data = atomic_file_data.read_to_string().map_err(|_| { + "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(); + let line_id = line_id.parse::().map_err(|_| { + format!("Failed to parse ResourceId from line: {i}",) + })?; + + if id == line_id { + let data = line.next().unwrap(); + return Ok(format!("{}", data)); + } + } + + Err(format!("Resource with id {} not found", id)) + } + StorageType::Folder => { + let folder_path = self.path.join(id.to_string()); + if !folder_path.exists() { + return Err(format!("Resource with id {} not found", id)); + } + + let atomic_file = AtomicFile::new(&folder_path) + // .expect("ERROR: Could not create atomic file"); + .map_err(|e| { + format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + ) + })?; + + let atomic_file_data = atomic_file.load().map_err(|e| { + format!( + "Failed to load atomic file at {:?} with error: {:?}", + self.path, e + ) + })?; + + let data = atomic_file_data.read_to_string().map_err(|_| { + "Could not read atomic file content.".to_string() + })?; + + Ok(data) + } + } + } + + pub fn insert( + &mut self, + id: ResourceId, + content: &str, + format: Format, + ) -> Result<(), String> { + match self.storage_type { + StorageType::File => { + let atomic_file = AtomicFile::new(&self.path) + // .expect("ERROR: Could not create atomic file"); + .map_err(|e| { + format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + ) + })?; + + let content = match format { + Format::KeyValue => return Err( + "Key value format is not supported for file storage" + .to_owned(), + ), + Format::Raw => format!("{}:{}", id, content), + }; + + match commands::file::file_insert( + &atomic_file, + &content, + crate::parsers::Format::Raw, + ) { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return 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| { + format!( + "Failed to create folder at {:?} with error: {:?}", + folder_path, e + ) + })?; + } + + let atomic_file = AtomicFile::new(&folder_path) + // .expect("ERROR: Could not create atomic file"); + .map_err(|e| { + format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + ) + })?; + + match commands::file::file_insert( + &atomic_file, + &content, + format, + ) { + Ok(_) => { + return Ok(()); + } + Err(e) => { + return 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(|_| "Could not write to output".to_string())?; + } + } else { + match self.storage_type { + StorageType::File => { + let atomic_file = AtomicFile::new(&self.path) + // .expect("ERROR: Could not create atomic file"); + .map_err(|e| { + format!( + "Failed to create atomic file at {} with error: {:?}", + self.path.display(), e + ) + })?; + + let atomic_file_data = atomic_file.load().map_err(|e| { + format!( + "Failed to load atomic file at {:?} with error: {:?}", + self.path, e + ) + })?; + + writeln!(output, "{: <16} {}", "id", "value") + .map_err(|_| "Could not write to output".to_string())?; + + let data = + atomic_file_data.read_to_string().map_err(|_| { + "Could not read atomic file content.".to_string() + })?; + + for line in data.lines() { + let mut line = line.split(':'); + let id = line.next().unwrap(); + let data = line.next().unwrap(); + writeln!(output, "{: <16} {}", id, data).map_err( + |_| "Could not write to output".to_string(), + )?; + } + } + StorageType::Folder => { + let folder_entries = std::fs::read_dir(&self.path) + .map_err(|e| { + 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(|_| "Could not write to output".to_string())?; + + for entry in folder_entries { + if let Some(file) = format_file(&entry) { + writeln!(output, "{}", file).map_err(|_| { + "Could not write to output".to_string() + })?; + } + } + } + }; + } + + Ok(output) + } +}