diff --git a/src/.storages.rs.rustfmt b/src/.storages.rs.rustfmt new file mode 100644 index 0000000..c0525f7 --- /dev/null +++ b/src/.storages.rs.rustfmt @@ -0,0 +1,106 @@ +//! Manipulates storages. + +use crate::storages:: +use crate::storages::directory::Directory; +use crate::storages::physical_drive_partition::PhysicalDrivePartition; +use anyhow::{anyhow, Context, Result}; +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; +use std::{ffi, fmt, fs, io, path::Path, collections::HashMap}; + +/// YAML file to store known storages.. +pub const STORAGESFILE: &str = "storages.yml"; + +#[derive(ValueEnum, Clone, Copy, Debug)] +pub enum StorageType { + Physical, + SubDirectory, + // Online, +} + +/// All storage types. +#[derive(Serialize, Deserialize, Debug)] +pub enum Storage { + PhysicalStorage(PhysicalDrivePartition), + SubDirectory(Directory), + // /// Online storage provided by others. + // OnlineStorage { + // name: String, + // provider: String, + // capacity: u8, + // }, +} + +impl Storage { + /// Add or update alias of `disk` for current device. + pub fn add_alias( + &mut self, + disk: &sysinfo::Disk, + config_dir: &std::path::PathBuf, + ) -> anyhow::Result<()> { + match self { + Self::PhysicalStorage(s) => s.add_alias(disk, config_dir), + Self::SubDirectory(_) => Err(anyhow!("SubDirectory doesn't have system alias.")), + } + } +} + +impl StorageExt for Storage { + fn name(&self) -> &String { + match self { + Self::PhysicalStorage(s) => s.name(), + Self::SubDirectory(s) => s.name(), + } + } +} + +impl fmt::Display for Storage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PhysicalStorage(s) => s.fmt(f), + Self::SubDirectory(s) => s.fmt(f), + } + } +} + +/// Trait to manipulate all `Storage`s (Enums). +pub trait StorageExt { + fn name(&self) -> &String; +} + +pub mod directory; +pub mod physical_drive_partition; + +/// Get `Vec` from devices.yml([DEVICESFILE]). +/// If [DEVICESFILE] isn't found, return empty vec. +pub fn get_storages(config_dir: &Path) -> Result> { + if let Some(storages_file) = fs::read_dir(&config_dir)? + .filter(|f| { + f.as_ref().map_or_else( + |_e| false, + |f| { + let storagesfile: ffi::OsString = STORAGESFILE.into(); + f.path().file_name() == Some(&storagesfile) + }, + ) + }) + .next() + { + trace!("{} found: {:?}", STORAGESFILE, storages_file); + let f = fs::File::open(config_dir.join(STORAGESFILE))?; + let reader = io::BufReader::new(f); + let yaml: HashMap = + serde_yaml::from_reader(reader).context("Failed to read devices.yml")?; + Ok(yaml) + } else { + trace!("No {} found", STORAGESFILE); + Ok(HashMap::new()) + } +} + +/// Write `storages` to yaml file in `config_dir`. +pub fn write_storages(config_dir: &Path, storages: HashMap) -> Result<()> { + let f = fs::File::create(config_dir.join(STORAGESFILE))?; + let writer = io::BufWriter::new(f); + serde_yaml::to_writer(writer, &storages).map_err(|e| anyhow!(e)) +} diff --git a/src/main.rs b/src/main.rs index 3b3940b..d5d99c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -200,16 +200,18 @@ fn main() -> Result<()> { let mut storages: HashMap = get_storages(&config_dir)?; trace!("found storages: {:?}", storages); + let device = get_device(&config_dir)?; let (key, storage) = match storage_type { StorageType::Physical => { // select storage - let device = get_device(&config_dir)?; let (key, storage) = select_physical_storage(device, &storages)?; println!("storage: {}: {:?}", key, storage); (key, Storage::PhysicalStorage(storage)) } StorageType::SubDirectory => { - let mut storages: HashMap = get_storages(&config_dir)?; + if storages.is_empty() { + return Err(anyhow!("No storages found. Please add at least 1 physical storage first.")); + } let path = path.unwrap_or_else(|| { let mut cmd = Cli::command(); cmd.error( @@ -222,9 +224,17 @@ fn main() -> Result<()> { // Nightly feature std::path::absolute let path = path.canonicalize()?; trace!("canonicalized: path: {:?}", path); - - // let (key, storage) = storages::directory::Directory::new(name, parent, relative_path, notes) - todo!() + + let key_name = ask_unique_name(&storages, "sub-directory".to_string())?; + let notes = Text::new("Notes for this sub-directory:").prompt()?; + let storage = storages::directory::Directory::try_from_device_path( + key_name.clone(), + path, + notes, + &device, + &storages, + )?; + (key_name, Storage::SubDirectory(storage)) } }; @@ -249,8 +259,10 @@ fn main() -> Result<()> { // Get storages let storages: HashMap = get_storages(&config_dir)?; trace!("found storages: {:?}", storages); - for (k, storage) in storages { + let device = get_device(&config_dir)?; + for (k, storage) in &storages { println!("{}: {}", k, storage); + println!(" {}", storage.mount_path(&device, &storages)?.display()); } } StorageCommands::Bind { @@ -307,7 +319,7 @@ fn main() -> Result<()> { Ok(()) } -/// Get devname of the device. +/// Get devname of the device from file `devname`. fn get_devname(config_dir: &Path) -> Result { let f = File::open(config_dir.join("devname")).context("Failed to open devname file")?; let bufreader = BufReader::new(f); @@ -385,6 +397,18 @@ fn write_devices(config_dir: &Path, devices: Vec) -> Result<()> { serde_yaml::to_writer(writer, &devices).map_err(|e| anyhow!(e)) } +fn ask_unique_name(storages: &HashMap, target: String) -> Result { + let mut disk_name = String::new(); + loop { + disk_name = Text::new(format!("Name for {}:", target).as_str()).prompt()?; + if storages.iter().all(|(k, v)| k != &disk_name) { + break; + } + println!("The name {} is already used.", disk_name); + } + Ok(disk_name) +} + fn find_last_commit(repo: &Repository) -> Result, git2::Error> { if repo.is_empty()? { Ok(None) diff --git a/src/storages.rs b/src/storages.rs index 5453f22..67faf22 100644 --- a/src/storages.rs +++ b/src/storages.rs @@ -1,12 +1,12 @@ //! Manipulates storages. use crate::storages::physical_drive_partition::PhysicalDrivePartition; -use crate::{devices, storages::directory::Directory}; +use crate::storages::directory::Directory; +use crate::devices; use anyhow::{anyhow, Context, Result}; use clap::ValueEnum; use serde::{Deserialize, Serialize}; -use std::path::{self}; -use std::{collections::HashMap, ffi, fmt, fs, io, path::Path}; +use std::{collections::HashMap, ffi, fmt, fs, io, path}; /// YAML file to store known storages.. pub const STORAGESFILE: &str = "storages.yml"; @@ -53,16 +53,26 @@ impl StorageExt for Storage { } } - // fn mount_path( - // &self, - // &device: &devices::Device, - // &storages: &HashMap, - // ) -> Result<&path::PathBuf> { - // match self { - // Self::PhysicalStorage(s) => s.mount_path(&device, &storages), - // Self::SubDirectory(s) => s.mount_path(&device, &storages), - // } - // } + fn has_alias( + &self, + device: &devices::Device, + ) -> bool { + match self { + Self::PhysicalStorage(s) => s.has_alias(&device), + Self::SubDirectory(s) => s.has_alias(&device), + } + } + + fn mount_path( + &self, + device: &devices::Device, + storages: &HashMap, + ) -> Result { + match self { + Self::PhysicalStorage(s) => s.mount_path(&device, &storages), + Self::SubDirectory(s) => s.mount_path(&device, &storages), + } + } } impl fmt::Display for Storage { @@ -77,11 +87,12 @@ impl fmt::Display for Storage { /// Trait to manipulate all `Storage`s (Enums). pub trait StorageExt { fn name(&self) -> &String; - // fn mount_path( - // &self, - // &device: &devices::Device, - // &storages: &HashMap, - // ) -> Result<&path::PathBuf>; + fn has_alias(&self, device: &devices::Device) -> bool; + fn mount_path( + &self, + device: &devices::Device, + storages: &HashMap, + ) -> Result; } pub mod directory; @@ -90,7 +101,7 @@ pub mod physical_drive_partition; /// Get `Vec` from devices.yml([DEVICESFILE]). /// If [DEVICESFILE] isn't found, return empty vec. -pub fn get_storages(config_dir: &Path) -> Result> { +pub fn get_storages(config_dir: &path::Path) -> Result> { if let Some(storages_file) = fs::read_dir(&config_dir)? .filter(|f| { f.as_ref().map_or_else( @@ -116,7 +127,7 @@ pub fn get_storages(config_dir: &Path) -> Result> { } /// Write `storages` to yaml file in `config_dir`. -pub fn write_storages(config_dir: &Path, storages: HashMap) -> Result<()> { +pub fn write_storages(config_dir: &path::Path, storages: HashMap) -> Result<()> { let f = fs::File::create(config_dir.join(STORAGESFILE))?; let writer = io::BufWriter::new(f); serde_yaml::to_writer(writer, &storages).map_err(|e| anyhow!(e)) diff --git a/src/storages/directory.rs b/src/storages/directory.rs index 9cc67a2..290c231 100644 --- a/src/storages/directory.rs +++ b/src/storages/directory.rs @@ -5,13 +5,14 @@ use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, fmt::{self, write}, + hash::Hash, path, rc::Rc, }; use crate::devices; -use super::{Storage, StorageExt}; +use super::{local_info::LocalInfo, Storage, StorageExt}; /// Subdirectory of other [Storage]s. #[derive(Serialize, Deserialize, Debug)] @@ -20,6 +21,7 @@ pub struct Directory { parent: String, relative_path: path::PathBuf, notes: String, + local_info: HashMap, } impl Directory { @@ -27,40 +29,83 @@ impl Directory { /// - `parent`: where the directory locates. /// - `relative_path`: path from root of the parent storage. /// - `notes`: supplimental notes. - pub fn new( + fn new( name: String, parent: String, // todo implement serialize relative_path: path::PathBuf, notes: String, + local_info: HashMap, ) -> Directory { Directory { name, parent, relative_path, notes, + local_info, } } + pub fn try_from_device_path( + name: String, + path: path::PathBuf, + notes: String, + device: &devices::Device, + storages: &HashMap, + ) -> Result { + let (parent_name, diff_path) = storages + .iter() + .filter(|(_k, v)| v.has_alias(&device)) + .filter_map(|(k, v)| { + let diff = pathdiff::diff_paths(&path, v.mount_path(&device, &storages).unwrap())?; + trace!("Pathdiff: {:?}", diff); + Some((k, diff)) + }) + .min_by_key(|(_k, v)| { + let diff_paths: Vec> = v.components().collect(); + diff_paths.len() + }) + .context(format!("Failed to compare diff of paths"))?; + trace!("Selected parent: {}", parent_name); + let local_info = LocalInfo::new("".to_string(), path); + Ok(Directory::new( + name, + parent_name.clone(), + diff_path, + notes, + HashMap::from([(device.name(), local_info)]), + )) + } + + pub fn update_note(self, notes: String) -> Directory { + Directory::new( + self.name, + self.parent, + self.relative_path, + notes, + self.local_info, + ) + } + /// Get parent `&Storage` of directory. fn parent<'a>(&'a self, storages: &'a HashMap) -> Result<&Storage> { let parent = &self.parent; - storages.get(&self.parent.clone()).context(format!( + storages.get(parent).context(format!( "No parent {} exists for directory {}", parent, self.name() )) } - // /// Resolve mount path of directory with current device. - // fn mount_path( - // &self, - // &device: &devices::Device, - // &storages: &HashMap, - // ) -> Result { - // let parent = self.parent(&storages)?; - // let parent_mount_path = parent.mount_path(&device, &storages)?; - // Ok(parent_mount_path.join(self.relative_path.clone())) - // } + /// Resolve mount path of directory with current device. + fn mount_path( + &self, + device: &devices::Device, + storages: &HashMap, + ) -> Result { + let parent = self.parent(&storages)?; + let parent_mount_path = parent.mount_path(&device, &storages)?; + Ok(parent_mount_path.join(self.relative_path.clone())) + } } impl StorageExt for Directory { @@ -68,9 +113,17 @@ impl StorageExt for Directory { &self.name } - // fn mount_path(&self, &device: &devices::Device, &storages: &HashMap) -> Result<&path::PathBuf> { - // Ok(&self.mount_path(&device, &storages)?) - // } + fn has_alias(&self, device: &devices::Device) -> bool { + self.local_info.get(&device.name()).is_some() + } + + fn mount_path( + &self, + device: &devices::Device, + storages: &HashMap, + ) -> Result { + Ok(self.mount_path(device, storages)?) + } } impl fmt::Display for Directory { diff --git a/src/storages/local_info.rs b/src/storages/local_info.rs index 845d5ee..024647c 100644 --- a/src/storages/local_info.rs +++ b/src/storages/local_info.rs @@ -1,6 +1,10 @@ use serde::{Deserialize, Serialize}; use std::path; +/// Store local (device-specific) information +/// +/// - `alias`: name in device +/// - `mount_path`: mount path on the device #[derive(Serialize, Deserialize, Debug)] pub struct LocalInfo { alias: String, @@ -12,7 +16,7 @@ impl LocalInfo { LocalInfo { alias, mount_path } } - pub fn mount_path(&self) -> &path::PathBuf { - &self.mount_path + pub fn mount_path(&self) -> path::PathBuf { + self.mount_path.clone() } } diff --git a/src/storages/physical_drive_partition.rs b/src/storages/physical_drive_partition.rs index 6846ac6..744f68e 100644 --- a/src/storages/physical_drive_partition.rs +++ b/src/storages/physical_drive_partition.rs @@ -48,7 +48,7 @@ impl PhysicalDrivePartition { let fs = disk.file_system(); trace!("fs: {:?}", fs); let fs = std::str::from_utf8(fs)?; - let local_info = LocalInfo::new(alias, disk.mount_point().to_path_buf()); + let local_info = LocalInfo::new(alias, disk.mount_point().to_path_buf().canonicalize()?); Ok(PhysicalDrivePartition { name: name, kind: format!("{:?}", disk.kind()), @@ -87,13 +87,17 @@ impl StorageExt for PhysicalDrivePartition { &self.name } - // fn mount_path(&self, &device: &devices::Device, &storages: &HashMap) -> Result<&path::PathBuf> { - // Ok(&self - // .local_info - // .get(&device.name()) - // .context(format!("LocalInfo for storage: {} not found", &self.name()))? - // .mount_path()) - // } + fn has_alias(&self, device: &devices::Device) -> bool { + self.local_info.get(&device.name()).is_some() + } + + fn mount_path(&self, device: &devices::Device, _: &HashMap) -> Result { + Ok(self + .local_info + .get(&device.name()) + .context(format!("LocalInfo for storage: {} not found", &self.name()))? + .mount_path()) + } } impl fmt::Display for PhysicalDrivePartition {