diff --git a/src/commands.rs b/src/commands.rs index d7bb0c0..e2eb32e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,2 +1,3 @@ pub mod add; pub mod init; +pub mod status; diff --git a/src/commands/status.rs b/src/commands/status.rs new file mode 100644 index 0000000..9016af2 --- /dev/null +++ b/src/commands/status.rs @@ -0,0 +1,162 @@ +use crate::config::config::Config; +use crate::store::{ + nsobject::NsObject, + object::{Obj, ObjStatus}, +}; +use crate::utils::path; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use threadpool::ThreadPool; + +pub struct StatusArgs { + pub nostyle: bool, +} + +struct ObjStatuses { + created: Arc>>, + modified: Arc>>, + deleted: Arc>>, +} + +impl ObjStatuses { + fn new() -> Self { + ObjStatuses { + created: Arc::new(Mutex::new(HashMap::new())), + modified: Arc::new(Mutex::new(HashMap::new())), + deleted: Arc::new(Mutex::new(HashMap::new())), + } + } + + fn push_created(&self, key: String, value: Obj) { + self.created.lock().unwrap().insert(key, value); + } + + fn push_modified(&self, key: String, value: Obj) { + self.modified.lock().unwrap().insert(key, value); + } + + fn push_deleted(&self, key: String, value: Obj) { + self.deleted.lock().unwrap().insert(key, value); + } +} + +pub fn exec(args: StatusArgs, config: Config) { + // use root of repo if no custom path has been set by the command + let root = if config.is_custom_execution_path { + config.execution_path.clone() + } else { + config.get_root_unsafe().to_path_buf() + }; + + let pool = ThreadPool::new(4); + let repo_root = config.get_root_unsafe().clone(); + let res = Arc::new(ObjStatuses::new()); + + let pool_clone = pool.clone(); + let res_clone = Arc::clone(&res); + pool.execute(move || { + compare_dir(pool_clone, &repo_root, &root, res_clone); + }); + + pool.join(); + + for entry in res.created.lock().unwrap().iter() { + println!("created: {}", entry.0); + } + for entry in res.modified.lock().unwrap().iter() { + println!("modified: {}", entry.0); + } + for entry in res.deleted.lock().unwrap().iter() { + println!("deleted: {}", entry.0); + } + // find moved and copied +} + +/// +/// +/// * `pool`: +/// * `root_path`: path of the repo's root +/// * `path`: path we should analyze +/// * `res`: the struct in which we should store the response +fn compare_dir(pool: ThreadPool, root_path: &PathBuf, path: &PathBuf, res: Arc) { + let entries = match fs::read_dir(path) { + Ok(entries) => entries, + Err(err) => { + eprintln!("Failed to read {} ({err})", path.display()); + return; + } + }; + + let mut local_files = HashMap::new(); + let mut local_dirs = HashMap::new(); + + // Read locals files and folders + for entry in entries { + let entry = match entry { + Ok(entry) => entry, + Err(err) => { + eprintln!("Failed to read entry {err}"); + continue; + } + }; + + let repo_relative_entry = path::to_repo_relative(&entry.path(), root_path); + let local_obj = Obj::from_local_path(&repo_relative_entry); + + if entry.path().is_dir() { + if entry.path().ends_with(".nextsync") { + continue; + } + + if local_obj.is_new() { + // TODO: opti move files in new directory + res.push_created(local_obj.cpy_path(), local_obj); + } else { + local_dirs.insert(local_obj.cpy_path(), local_obj); + + // Analyze sub folder + let pool_clone = pool.clone(); + let root_path_clone = root_path.clone(); + let res_clone = Arc::clone(&res); + pool.execute(move || { + compare_dir(pool_clone, &root_path_clone, &entry.path(), res_clone); + }); + } + } else { + if local_obj.is_new() { + res.push_created(local_obj.cpy_path(), local_obj); + } else { + local_files.insert(local_obj.cpy_path(), local_obj); + } + } + } + + // Read ns objects to find deleted + let entries = NsObject::from_local_path(&path); + for entry in entries.iter() { + if entry.is_file() { + match local_files.get(entry.get_obj_path().to_str().unwrap()) { + None => res.push_deleted(path::to_string(entry.get_obj_path()), { + let mut obj = entry.get_local_obj(); + obj.set_status(ObjStatus::Deleted); + obj + }), + Some(local_obj) if local_obj != entry => { + res.push_modified(local_obj.cpy_path(), local_obj.clone()) + } + Some(_) => (), + } + } else { + match local_dirs.get(entry.get_obj_path().to_str().unwrap()) { + None => res.push_deleted(path::to_string(entry.get_obj_path()), { + let mut obj = entry.get_local_obj(); + obj.set_status(ObjStatus::Deleted); + obj + }), + Some(_) => (), + } + } + } +} diff --git a/src/config/config.rs b/src/config/config.rs index 2fdc05d..374fb78 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -8,6 +8,7 @@ use std::sync::OnceLock; /// * `root`: path of the repo pub struct Config { pub execution_path: PathBuf, + pub is_custom_execution_path: bool, root: OnceLock>, } @@ -15,6 +16,7 @@ impl Config { pub fn new() -> Self { Config { execution_path: PathBuf::from(env::current_dir().unwrap()), + is_custom_execution_path: false, root: OnceLock::new(), } } @@ -23,6 +25,7 @@ impl Config { match exec_path { Some(path) => Config { execution_path: PathBuf::from(path), + is_custom_execution_path: true, root: OnceLock::new(), }, None => Config::new(), diff --git a/src/main.rs b/src/main.rs index e4c02f9..c82efe5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,13 +4,18 @@ mod commands; mod config; mod store; mod subcommands; +mod utils; fn main() { let app = Command::new("Nextsync") .version("1.0") .author("grimhilt") .about("A git-line command line tool to interact with nextcloud") - .subcommands([subcommands::init::create(), subcommands::add::create()]); + .subcommands([ + subcommands::init::create(), + subcommands::add::create(), + subcommands::status::create(), + ]); // .setting(clap::AppSettings::SubcommandRequiredElseHelp); let matches = app.get_matches(); @@ -18,6 +23,7 @@ fn main() { match matches.subcommand() { Some(("init", args)) => subcommands::init::handler(args), Some(("add", args)) => subcommands::add::handler(args), + Some(("status", args)) => subcommands::status::handler(args), Some((_, _)) => {} None => {} }; diff --git a/src/store.rs b/src/store.rs index 733d50c..2a9986d 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,2 +1,4 @@ pub mod ignorer; pub mod nsignore; +pub mod object; +pub mod nsobject; diff --git a/src/store/nsobject.rs b/src/store/nsobject.rs new file mode 100644 index 0000000..b1a9df0 --- /dev/null +++ b/src/store/nsobject.rs @@ -0,0 +1,90 @@ +use crate::store::object::{ObjType, Obj, ObjMetadata}; +use std::path::PathBuf; +use std::sync::OnceLock; + +type NsObjectChilds = Vec>; + +pub struct NsObject { + pub obj_type: ObjType, + obj_path: OnceLock, + childs: OnceLock, + index: usize, +} + +impl NsObject { + pub fn from_local_path(path: &PathBuf) -> Self { + NsObject { + obj_type: ObjType::Obj, + obj_path: OnceLock::from(path.to_path_buf()), + childs: OnceLock::new(), + index: 0, + } + } + + pub fn from_hash(hash: &str) -> Self { + NsObject { + obj_type: ObjType::Obj, + obj_path: OnceLock::new(), + childs: OnceLock::new(), + index: 0, + } + } + + pub fn get_childs(&self) -> &NsObjectChilds { + self.childs.get_or_init(|| { + if self.obj_type != ObjType::Tree { + Vec::new() + } else { + todo!() + } + }) + } + + pub fn get_obj_path(&self) -> &PathBuf { + self.obj_path.get_or_init(|| { + todo!() + }) + } + + pub fn is_file(&self) -> bool { + self.obj_type == ObjType::Blob + } + + pub fn is_dir(&self) -> bool { + self.obj_type == ObjType::Tree + } + + pub fn get_local_obj(&self) -> Obj { + let mut obj = Obj::from_local_path(self.get_obj_path()); + obj.set_type(self.obj_type.clone()); + obj + } + + pub fn get_metadata(&self) -> Option { + todo!() + } + + pub fn iter(&self) -> NsObjectIter<'_> { + NsObjectIter { + nsobject: self, + index: 0 + } + } +} + +pub struct NsObjectIter<'a> { + nsobject: &'a NsObject, + index: usize, +} + +impl<'a> Iterator for NsObjectIter<'a> { + type Item = &'a NsObject; + + fn next(&mut self) -> Option { + self.index += 1; + match self.nsobject.get_childs().get(self.index - 1) { + None => None, + Some(obj) => Some(&**obj) + } + } +} diff --git a/src/store/object.rs b/src/store/object.rs new file mode 100644 index 0000000..f202ea6 --- /dev/null +++ b/src/store/object.rs @@ -0,0 +1,135 @@ +use crate::store::nsobject::NsObject; +use crate::utils::path; +use std::fs; +use std::path::PathBuf; +use std::sync::OnceLock; +use std::time::SystemTime; + +const MAX_SIZE_TO_USE_HASH: u64 = 12 * 1024 * 1024; + +pub struct ObjMetadata { + size: u64, + modified: Option, +} + +#[derive(PartialEq, Clone)] +pub enum ObjType { + Obj, + Blob, + Tree, +} + +#[derive(PartialEq, Clone)] +pub enum ObjStatus { + Undefined, + Created, + Moved, + Copied, + Deleted, +} + +#[derive(Clone)] +pub struct Obj { + obj_type: ObjType, + status: OnceLock, + /// path of the object from root + obj_path: PathBuf, +} + +impl Obj { + pub fn from_local_path(path: &PathBuf) -> Self { + // todo set state + Obj { + obj_type: ObjType::Obj, + status: OnceLock::new(), + obj_path: path.to_path_buf(), + } + } + + fn get_status(&self) -> &ObjStatus { + self.status.get_or_init(|| { + // read path + ObjStatus::Created + }) + } + + pub fn set_status(&mut self, status: ObjStatus) { + todo!() + } + + pub fn set_type(&mut self, obj_type: ObjType) { + self.obj_type = obj_type; + } + + pub fn is_new(&self) -> bool { + self.get_status() == &ObjStatus::Created + } + + pub fn cpy_path(&self) -> String { + path::to_string(&self.obj_path) + } + + pub fn get_metadata(&self) -> Option { + let metadata = match fs::metadata(&self.obj_path) { + Ok(m) => m, + Err(err) => { + eprintln!( + "Failed to read metadata of {} ({})", + self.obj_path.display(), + err + ); + return None; + } + }; + + Some(ObjMetadata { + size: metadata.len(), + modified: metadata.modified().ok(), + }) + } +} + +impl PartialEq for Obj { + fn eq(&self, other: &NsObject) -> bool { + if self.obj_type != other.obj_type { + eprintln!("Trying to compare different obj type"); + return false; + } + + if self.obj_type == ObjType::Tree { + panic!("fatal: cannot comparing folders"); + } + + let obj_metadata = self.get_metadata(); + let nsobj_metadata = other.get_metadata(); + + // If not metadata exist considere it is not the same + if obj_metadata.is_none() || nsobj_metadata.is_none() { + return false; + } + + let obj_metadata = obj_metadata.unwrap(); + let nsobj_metadata = nsobj_metadata.unwrap(); + + // if we have modified and it is the same it must be the same file + if obj_metadata.modified.is_some() && nsobj_metadata.modified.is_some() { + if obj_metadata.modified.unwrap() == nsobj_metadata.modified.unwrap() { + return true; + } + } + + // if size is same considere comparing hash if file is small + if obj_metadata.size == obj_metadata.size { + if obj_metadata.size < MAX_SIZE_TO_USE_HASH + && nsobj_metadata.size < MAX_SIZE_TO_USE_HASH + { + todo!() + // return self.get_file_hash() == other.get_file_hash(); + } else { + return true; + } + } else { + return false; + } + } +} diff --git a/src/subcommands.rs b/src/subcommands.rs index d7bb0c0..e2eb32e 100644 --- a/src/subcommands.rs +++ b/src/subcommands.rs @@ -1,2 +1,3 @@ pub mod add; pub mod init; +pub mod status; diff --git a/src/subcommands/status.rs b/src/subcommands/status.rs new file mode 100644 index 0000000..b955946 --- /dev/null +++ b/src/subcommands/status.rs @@ -0,0 +1,25 @@ +use clap::{Arg, ArgMatches, Command}; + +use crate::commands; +use crate::commands::status::StatusArgs; +use crate::config::config::Config; + +pub fn create() -> Command { + Command::new("status") + .arg(Arg::new("directory").num_args(1).value_name("DIRECTORY")) + .arg( + Arg::new("nostyle") + .long("nostyle") + .help("Status with minium information and style"), + ) + .about("Show the working tree status") +} + +pub fn handler(args: &ArgMatches) { + commands::status::exec( + StatusArgs { + nostyle: args.contains_id("nostyle"), + }, + Config::from(args.get_one::("directory")), + ); +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..4da9789 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1 @@ +pub mod path; diff --git a/src/utils/path.rs b/src/utils/path.rs new file mode 100644 index 0000000..9a6ff13 --- /dev/null +++ b/src/utils/path.rs @@ -0,0 +1,9 @@ +use std::path::{Path, PathBuf}; + +pub fn to_repo_relative(path: &PathBuf, root: &PathBuf) -> PathBuf { + path.strip_prefix(root).unwrap().to_path_buf() +} + +pub fn to_string(path: &PathBuf) -> String { + path.to_str().unwrap().to_string() +}