diff --git a/Cargo.lock b/Cargo.lock index d3c3022..8d95a43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,30 +33,30 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "a3a318f1f38d2418400f8209655bfd825785afd25aa30bb7ba6cc792e4596748" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -111,9 +111,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.4.10" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", "clap_derive", @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.9" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstream", "anstyle", @@ -514,9 +514,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "log", @@ -556,9 +556,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.96" +version = "0.9.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" +checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" dependencies = [ "cc", "libc", diff --git a/README.md b/README.md new file mode 100644 index 0000000..350efb7 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# TODO: +- [ ] split subcommands to functions +- [ ] reorganize cmd option for storage + - [ ] use subcommand diff --git a/src/inquire_filepath_completer.rs b/src/inquire_filepath_completer.rs new file mode 100644 index 0000000..36ee53e --- /dev/null +++ b/src/inquire_filepath_completer.rs @@ -0,0 +1,116 @@ +use std::io::ErrorKind; +use inquire::{autocompletion::{Autocomplete, Replacement}, CustomUserError}; + +#[derive(Clone, Default)] +pub struct FilePathCompleter { + input: String, + paths: Vec, + lcp: String, +} + +impl FilePathCompleter { + fn update_input(&mut self, input: &str) -> Result<(), CustomUserError> { + if input == self.input { + return Ok(()); + } + + self.input = input.to_owned(); + self.paths.clear(); + + let input_path = std::path::PathBuf::from(input); + + let fallback_parent = input_path + .parent() + .map(|p| { + if p.to_string_lossy() == "" { + std::path::PathBuf::from(".") + } else { + p.to_owned() + } + }) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + + let scan_dir = if input.ends_with('/') { + input_path + } else { + fallback_parent.clone() + }; + + let entries = match std::fs::read_dir(scan_dir) { + Ok(read_dir) => Ok(read_dir), + Err(err) if err.kind() == ErrorKind::NotFound => std::fs::read_dir(fallback_parent), + Err(err) => Err(err), + }? + .collect::, _>>()?; + + let mut idx = 0; + let limit = 15; + + while idx < entries.len() && self.paths.len() < limit { + let entry = entries.get(idx).unwrap(); + + let path = entry.path(); + let path_str = if path.is_dir() { + format!("{}/", path.to_string_lossy()) + } else { + path.to_string_lossy().to_string() + }; + + if path_str.starts_with(&self.input) && path_str.len() != self.input.len() { + self.paths.push(path_str); + } + + idx = idx.saturating_add(1); + } + + self.lcp = self.longest_common_prefix(); + + Ok(()) + } + + fn longest_common_prefix(&self) -> String { + let mut ret: String = String::new(); + + let mut sorted = self.paths.clone(); + sorted.sort(); + if sorted.is_empty() { + return ret; + } + + let mut first_word = sorted.first().unwrap().chars(); + let mut last_word = sorted.last().unwrap().chars(); + + loop { + match (first_word.next(), last_word.next()) { + (Some(c1), Some(c2)) if c1 == c2 => { + ret.push(c1); + } + _ => return ret, + } + } + } +} + +impl Autocomplete for FilePathCompleter { + fn get_suggestions(&mut self, input: &str) -> Result, CustomUserError> { + self.update_input(input)?; + + Ok(self.paths.clone()) + } + + fn get_completion( + &mut self, + input: &str, + highlighted_suggestion: Option, + ) -> Result { + self.update_input(input)?; + + Ok(match highlighted_suggestion { + Some(suggestion) => Replacement::Some(suggestion), + None => match self.lcp.is_empty() { + true => Replacement::None, + false => Replacement::Some(self.lcp.clone()), + }, + }) + } +} diff --git a/src/main.rs b/src/main.rs index b868d2a..ca1b9d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,10 +16,11 @@ use clap::error::ErrorKind; use clap::{CommandFactory, Parser, Subcommand}; use clap_verbosity_flag::Verbosity; use git2::{Commit, Oid, Repository}; +use inquire::{min_length, Confirm, CustomType, Select}; use inquire::{validator::Validation, Text}; use serde_yaml; use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{self, PathBuf}; use std::{env, io::BufReader, path::Path}; use std::{ ffi::OsString, @@ -29,6 +30,8 @@ use std::{fmt::Debug, fs::File}; use std::{fs, io::prelude::*}; use sysinfo::{Disk, DiskExt, SystemExt}; +use crate::inquire_filepath_completer::FilePathCompleter; +use crate::storages::online_storage::OnlineStorage; use crate::storages::{ directory::Directory, get_storages, local_info, online_storage, physical_drive_partition::*, write_storages, Storage, StorageExt, StorageType, STORAGESFILE, @@ -62,6 +65,9 @@ enum Commands { /// Sync with git repo. Sync {}, + + /// Check config files. + Check {}, } #[derive(clap::Args)] @@ -84,17 +90,26 @@ enum StorageCommands { }, /// List all storages. List {}, - /// Add new device-specific name to existing storage. + /// Make `storage` available for the current device. /// For physical disk, the name is taken from system info automatically. - Bind { storage: String }, + Bind { + /// Name of the storage. + storage: String, + /// Device specific alias for the storage. + #[arg(short, long)] + alias: String, + /// Mount point on this device. + #[arg(short, long)] + path: path::PathBuf, + }, } mod devices; +mod inquire_filepath_completer; mod storages; struct BackupLog {} -#[feature(absolute_path)] fn main() -> Result<()> { let cli = Cli::parse(); env_logger::Builder::new() @@ -200,8 +215,66 @@ fn main() -> Result<()> { let device = get_device(&config_dir)?; let (key, storage) = match storage_type { StorageType::Physical => { - // select storage - let (key, storage) = select_physical_storage(device, &storages)?; + let use_sysinfo = { + let options = vec![ + "Fetch disk information automatically.", + "Type disk information manually.", + ]; + let ans = Select::new("Do you fetch disk information automatically? (it may take a few minutes)", options) + .prompt().context("Failed to get response. Please try again.")?; + match ans { + "Fetch disk information automatically." => true, + _ => false, + } + }; + let (key, storage) = if use_sysinfo { + // select storage + select_physical_storage(device, &storages)? + } else { + let mut name = String::new(); + loop { + name = Text::new("Name for the storage:") + .with_validator(min_length!(0, "At least 1 character")) + .prompt() + .context("Failed to get Name")?; + if storages.iter().all(|(k, _v)| k != &name) { + break; + } + println!("The name {} is already used.", name); + } + let kind = Text::new("Kind of storage (ex. SSD):") + .prompt() + .context("Failed to get kind.")?; + let capacity: u64 = CustomType::::new("Capacity (byte):") + .with_error_message("Please type number.") + .prompt() + .context("Failed to get capacity.")?; + let fs = Text::new("filesystem:") + .prompt() + .context("Failed to get fs.")?; + let is_removable = Confirm::new("Is removable") + .prompt() + .context("Failed to get is_removable")?; + let mount_path: path::PathBuf = PathBuf::from( + Text::new("mount path:") + .with_autocomplete(FilePathCompleter::default()) + .prompt()?, + ); + let local_info = + local_info::LocalInfo::new("".to_string(), mount_path); + ( + name.clone(), + PhysicalDrivePartition::new( + name, + kind, + capacity, + fs, + is_removable, + local_info, + &device, + ), + ) + }; println!("storage: {}: {:?}", key, storage); (key, Storage::PhysicalStorage(storage)) } @@ -233,7 +306,46 @@ fn main() -> Result<()> { )?; (key_name, Storage::SubDirectory(storage)) } - StorageType::Online => todo!(), + StorageType::Online => { + let path = path.unwrap_or_else(|| { + let mut cmd = Cli::command(); + cmd.error( + ErrorKind::MissingRequiredArgument, + " is required with sub-directory", + ) + .exit(); + }); + let mut name = String::new(); + loop { + name = Text::new("Name for the storage:") + .with_validator(min_length!(0, "At least 1 character")) + .prompt() + .context("Failed to get Name")?; + if storages.iter().all(|(k, _v)| k != &name) { + break; + } + println!("The name {} is already used.", name); + } + let provider = Text::new("Provider:") + .prompt() + .context("Failed to get provider")?; + let capacity: u64 = CustomType::::new("Capacity (byte):") + .with_error_message("Please type number.") + .prompt() + .context("Failed to get capacity.")?; + let alias = Text::new("Alias:") + .prompt() + .context("Failed to get provider")?; + let storage = OnlineStorage::new( + name.clone(), + provider, + capacity, + alias, + path, + &device, + ); + (name, Storage::Online(storage)) + } }; // add to storages @@ -261,30 +373,31 @@ fn main() -> Result<()> { for (k, storage) in &storages { println!("{}: {}", k, storage); println!(" {}", storage.mount_path(&device, &storages)?.display()); + // println!("{}: {}", storage.shorttypename(), storage.name()); // TODO } } StorageCommands::Bind { storage: storage_name, + alias: new_alias, + path: mount_point, } => { + let device = get_device(&config_dir)?; // get storages let mut storages: HashMap = get_storages(&config_dir)?; let commit_comment = { // find matching storage - let storage = storages + let storage = &mut storages .get_mut(&storage_name) .context(format!("No storage has name {}", storage_name))?; - // get disk from sysinfo - let mut sysinfo = sysinfo::System::new_all(); - sysinfo.refresh_disks(); - let disk = select_sysinfo_disk(&sysinfo)?; - let system_name = disk - .name() - .to_str() - .context("Failed to convert disk name to valid string")?; - // add to storages - storage.bind_device(disk, &config_dir)?; - trace!("storage: {}", storage); - format!("{} to {}", system_name, storage.name()) + let old_alias = storage + .local_info(&device) + .context(format!("Failed to get LocalInfo for {}", storage.name()))? + .alias() + .clone(); + // TODO: get mount path for directory automatically? + storage.bound_on_device(new_alias, mount_point, &device)?; + // trace!("storage: {}", &storage); + format!("{} to {}", old_alias, storage.name()) }; trace!("bound new system name to the storage"); trace!("storages: {:#?}", storages); @@ -312,6 +425,11 @@ fn main() -> Result<()> { Commands::Sync {} => { unimplemented!("Sync is not implemented") } + Commands::Check {} => { + println!("Config dir: {}", &config_dir.display()); + let _storages = storages::get_storages(&config_dir).context("Failed to parse storages file."); + todo!() + }, } full_status(&Repository::open(&config_dir)?)?; Ok(()) diff --git a/src/storages.rs b/src/storages.rs index 6cb7a4d..7e3ece6 100644 --- a/src/storages.rs +++ b/src/storages.rs @@ -28,16 +28,21 @@ pub enum Storage { } impl Storage { - /// Add or update alias of `disk` for current device. - pub fn bind_device( - &mut self, - disk: &sysinfo::Disk, - config_dir: &std::path::PathBuf, - ) -> anyhow::Result<()> { + /// Full type name like "PhysicalStorage". + pub fn typename(&self) -> &str { match self { - Self::PhysicalStorage(s) => s.bind_device(disk, config_dir), - Self::SubDirectory(_) => Err(anyhow!("SubDirectory doesn't have system alias.")), - Self::Online(_) => todo!(), + Self::PhysicalStorage(_) => "PhysicalStorage", + Self::SubDirectory(_) => "SubDirectory", + Self::Online(_) => "OnlineStorage", + } + } + + /// Short type name with one letter like "P". + pub fn shorttypename(&self) -> &str { + match self { + Self::PhysicalStorage(_) => "P", + Self::SubDirectory(_) => "S", + Self::Online(_) => "O", } } } @@ -51,11 +56,11 @@ impl StorageExt for Storage { } } - fn has_alias(&self, device: &devices::Device) -> bool { + fn local_info(&self, device: &devices::Device) -> Option<&local_info::LocalInfo> { match self { - Self::PhysicalStorage(s) => s.has_alias(&device), - Self::SubDirectory(s) => s.has_alias(&device), - Self::Online(s) => s.has_alias(&device), + Self::PhysicalStorage(s) => s.local_info(device), + Self::SubDirectory(s) => s.local_info(device), + Self::Online(s) => s.local_info(device), } } @@ -70,6 +75,19 @@ impl StorageExt for Storage { Self::Online(s) => s.mount_path(&device, &storages), } } + + fn bound_on_device( + &mut self, + alias: String, + mount_point: path::PathBuf, + device: &devices::Device, + ) -> Result<()> { + match self { + Storage::PhysicalStorage(s) => s.bound_on_device(alias, mount_point, device), + Storage::SubDirectory(s) => s.bound_on_device(alias, mount_point, device), + Storage::Online(s) => s.bound_on_device(alias, mount_point, device), + } + } } impl fmt::Display for Storage { @@ -85,13 +103,30 @@ impl fmt::Display for Storage { /// Trait to manipulate all `Storage`s (Enums). pub trait StorageExt { fn name(&self) -> &String; - fn has_alias(&self, device: &devices::Device) -> bool; + + /// Return `true` if `self` has local info on the `device`. + /// Used to check if the storage is bound on the `device`. + fn has_alias(&self, device: &devices::Device) -> bool { + self.local_info(device).is_some() + } + + /// Get local info of `device`. + fn local_info(&self, device: &devices::Device) -> Option<&local_info::LocalInfo>; + /// Get mount path of `self` on `device`. fn mount_path( &self, device: &devices::Device, storages: &HashMap, ) -> Result; + + /// Add local info of `device` to `self`. + fn bound_on_device( + &mut self, + alias: String, + mount_point: path::PathBuf, + device: &devices::Device, + ) -> Result<()>; } pub mod directory; diff --git a/src/storages/directory.rs b/src/storages/directory.rs index 5367ea9..c77a480 100644 --- a/src/storages/directory.rs +++ b/src/storages/directory.rs @@ -104,16 +104,6 @@ impl Directory { let parent_mount_path = parent.mount_path(&device, &storages)?; Ok(parent_mount_path.join(self.relative_path.clone())) } - - fn bind_device(&mut self, alias: String, device: &devices::Device, storages: &HashMap) -> Result<()> { - let mount_path = self.mount_path(&device, &storages)?; - let new_local_info = LocalInfo::new(alias, mount_path); - match self.local_info.insert(device.name(), new_local_info) { - Some(v) => println!("Value updated. old val is: {:?}", v), - None => println!("inserted new val"), - }; - Ok(()) - } } impl StorageExt for Directory { @@ -121,8 +111,8 @@ impl StorageExt for Directory { &self.name } - fn has_alias(&self, device: &devices::Device) -> bool { - self.local_info.get(&device.name()).is_some() + fn local_info(&self, device: &devices::Device) -> Option<&LocalInfo> { + self.local_info.get(&device.name()) } fn mount_path( @@ -132,6 +122,21 @@ impl StorageExt for Directory { ) -> Result { Ok(self.mount_path(device, storages)?) } + + /// This method doesn't use `mount_path`. + fn bound_on_device( + &mut self, + alias: String, + mount_point: path::PathBuf, + device: &devices::Device, + ) -> Result<()> { + let new_local_info = LocalInfo::new(alias, mount_point); + match self.local_info.insert(device.name(), new_local_info) { + Some(v) => println!("Value updated. old val is: {:?}", v), + None => println!("inserted new val"), + }; + Ok(()) + } } impl fmt::Display for Directory { diff --git a/src/storages/local_info.rs b/src/storages/local_info.rs index 1369ba8..4e36a33 100644 --- a/src/storages/local_info.rs +++ b/src/storages/local_info.rs @@ -18,6 +18,10 @@ impl LocalInfo { LocalInfo { alias, mount_path } } + pub fn alias(&self) -> String { + self.alias.clone() // ? + } + pub fn mount_path(&self) -> path::PathBuf { self.mount_path.clone() } diff --git a/src/storages/online_storage.rs b/src/storages/online_storage.rs index 06e9abe..77b16da 100644 --- a/src/storages/online_storage.rs +++ b/src/storages/online_storage.rs @@ -1,6 +1,6 @@ //! Online storage which is not a children of any physical drive. -use anyhow::Context; +use anyhow::{Context, Result}; use byte_unit::Byte; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -9,6 +9,7 @@ use std::path; use crate::devices; +use super::local_info; use super::local_info::LocalInfo; use super::StorageExt; @@ -16,13 +17,26 @@ use super::StorageExt; pub struct OnlineStorage { name: String, provider: String, - capacity: u8, + capacity: u64, local_info: HashMap, } impl OnlineStorage { - fn new(name: String, provider: String, capacity: u8, path: path::PathBuf, device: &devices::Device) -> OnlineStorage { - todo!() + pub fn new( + name: String, + provider: String, + capacity: u64, + alias: String, + path: path::PathBuf, + device: &devices::Device, + ) -> OnlineStorage { + let local_info = local_info::LocalInfo::new(alias, path); + OnlineStorage { + name, + provider, + capacity, + local_info: HashMap::from([(device.name(), local_info)]), + } } } @@ -31,8 +45,8 @@ impl StorageExt for OnlineStorage { &self.name } - fn has_alias(&self, device: &devices::Device) -> bool { - self.local_info.get(&device.name()).is_some() + fn local_info(&self, device: &devices::Device) -> Option<&LocalInfo> { + self.local_info.get(&device.name()) } fn mount_path( @@ -46,6 +60,22 @@ impl StorageExt for OnlineStorage { .context(format!("LocalInfo for storage: {} not found", &self.name()))? .mount_path()) } + + fn bound_on_device( + &mut self, + alias: String, + mount_point: path::PathBuf, + device: &devices::Device, + ) -> Result<()> { + match self + .local_info + .insert(device.name(), LocalInfo::new(alias, mount_point)) + { + Some(old) => info!("Value replaced. Old value: {:?}", old), + None => info!("New value inserted."), + }; + Ok(()) + } } impl fmt::Display for OnlineStorage { diff --git a/src/storages/physical_drive_partition.rs b/src/storages/physical_drive_partition.rs index 7c8d4fb..48eeb76 100644 --- a/src/storages/physical_drive_partition.rs +++ b/src/storages/physical_drive_partition.rs @@ -14,7 +14,7 @@ use std::{ }; use sysinfo::{Disk, DiskExt, SystemExt}; -use super::local_info::LocalInfo; +use super::local_info::{self, LocalInfo}; /// Partitoin of physical (on-premises) drive. #[derive(Serialize, Deserialize, Debug)] @@ -29,6 +29,25 @@ pub struct PhysicalDrivePartition { } impl PhysicalDrivePartition { + pub fn new( + name: String, + kind: String, + capacity: u64, + fs: String, + is_removable: bool, + local_info: LocalInfo, + device: &Device, + ) -> PhysicalDrivePartition { + PhysicalDrivePartition { + name, + kind, + capacity, + fs, + is_removable, + local_info: HashMap::from([(device.name(), local_info)]), + } + } + /// Try to get Physical drive info from sysinfo. pub fn try_from_sysinfo_disk( disk: &sysinfo::Disk, @@ -82,8 +101,8 @@ impl StorageExt for PhysicalDrivePartition { &self.name } - fn has_alias(&self, device: &devices::Device) -> bool { - self.local_info.get(&device.name()).is_some() + fn local_info(&self, device: &devices::Device) -> Option<&local_info::LocalInfo> { + self.local_info.get(&device.name()) } fn mount_path( @@ -97,6 +116,22 @@ impl StorageExt for PhysicalDrivePartition { .context(format!("LocalInfo for storage: {} not found", &self.name()))? .mount_path()) } + + fn bound_on_device( + &mut self, + alias: String, + mount_point: path::PathBuf, + device: &devices::Device, + ) -> Result<()> { + match self + .local_info + .insert(device.name(), LocalInfo::new(alias, mount_point)) + { + Some(old) => info!("Value replaced. Old value: {:?}", old), + None => info!("New value inserted."), + }; + Ok(()) + } } impl fmt::Display for PhysicalDrivePartition { @@ -122,8 +157,11 @@ pub fn select_physical_storage( ) -> Result<(String, PhysicalDrivePartition)> { trace!("select_physical_storage"); // get disk info fron sysinfo - let mut sys_disks = sysinfo::System::new_all(); - sys_disks.refresh_disks(); + let sys_disks = + sysinfo::System::new_with_specifics(sysinfo::RefreshKind::new().with_disks_list()); + trace!("refresh"); + // sys_disks.refresh_disks_list(); + // sys_disks.refresh_disks(); trace!("Available disks"); for disk in sys_disks.disks() { trace!("{:?}", disk)