From 37782c934c2b76f8319e8d83a9c0d284fbb04ed3 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Wed, 7 Aug 2024 11:39:58 +0200 Subject: [PATCH 01/20] (WIP) new: implement `status` subcommand - Error while getting mount path of Storages - Need to redesign the API of LocalInfo or something --- src/cmd_args.rs | 12 +++++++ src/cmd_status.rs | 81 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 6 ++++ src/storages.rs | 1 - 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/cmd_status.rs diff --git a/src/cmd_args.rs b/src/cmd_args.rs index 70a8033..4627f97 100644 --- a/src/cmd_args.rs +++ b/src/cmd_args.rs @@ -44,6 +44,18 @@ pub(crate) enum Commands { #[command(subcommand)] Backup(BackupSubCommands), + /// Print status for the given path. + Status { + /// Target path. Default is the current directory. + path: Option, + /// Show storage which the path belongs to. + #[arg(short)] + storage: bool, + /// Show backup config covering the path. + #[arg(short)] + backup: bool, + }, + /// Print config dir. Path {}, diff --git a/src/cmd_status.rs b/src/cmd_status.rs new file mode 100644 index 0000000..eebc2cc --- /dev/null +++ b/src/cmd_status.rs @@ -0,0 +1,81 @@ +use anyhow::{Context, Result}; +use console::Style; +use std::{ + env, + path::{self, Path, PathBuf}, +}; + +use crate::{ + backups::Backups, + devices::{self, Device}, + storages::{self, StorageExt, Storages}, + util, +}; + +// TODO: fine styling like `backup list`, or should I just use the same style? +pub(crate) fn cmd_status( + path: Option, + show_storage: bool, + show_backup: bool, + config_dir: &Path, +) -> Result<()> { + let path = path.unwrap_or(env::current_dir().context("Failed to get current directory.")?); + let device = devices::get_device(config_dir)?; + + if show_storage { + let storages = storages::Storages::read(config_dir)?; + let storage = util::min_parent_storage(&path, &storages, &device); + + match storage { + Some(storage) => { + println!("Storage: {}", storage.0.name()) + } + None => { + println!("Storage: None"); + } + } + } + if show_backup { + let devices = devices::get_devices(config_dir)?; + let storages = storages::Storages::read(config_dir)?; + let backups = Backups::read(config_dir, &device)?; + let covering_backup = devices + .iter() + .map(|device| (device, parent_backups(&path, &backups, &storages, device))); + + for (backup_device, covering_backups) in covering_backup { + println!("Device: {}", backup_device.name()); + for backup in covering_backups { + println!(" {}", console::style(backup.0).bold()); + } + } + } + todo!() +} + +fn parent_backups<'a>( + target_path: &'a PathBuf, + backups: &'a Backups, + storages: &'a Storages, + device: &'a Device, +) -> Vec<(&'a String, PathBuf)> { + backups + .list + .iter() + .filter_map(|(k, v)| { + let backup_path = match v.source().path(storages, device) { + Ok(path) => path, + Err(e) => { + error!("Error while getting backup source path: {}", e); + return None; + } + }; + let diff = pathdiff::diff_paths(target_path, backup_path)?; + if diff.components().any(|c| c == path::Component::ParentDir) { + None + } else { + Some((k, diff)) + } + }) + .collect() +} diff --git a/src/main.rs b/src/main.rs index 18e332d..23dadcd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ mod cmd_backup; mod cmd_check; mod cmd_completion; mod cmd_init; +mod cmd_status; mod cmd_storage; mod cmd_sync; mod devices; @@ -91,6 +92,11 @@ fn main() -> Result<()> { println!("{}", &config_dir.display()); } Commands::Sync { remote_name } => cmd_sync::cmd_sync(&config_dir, remote_name)?, + Commands::Status { + path, + storage, + backup, + } => cmd_status::cmd_status(path, storage, backup, &config_dir)?, Commands::Check {} => cmd_check::cmd_check(&config_dir)?, Commands::Backup(backup) => { trace!("backup subcommand with args: {:?}", backup); diff --git a/src/storages.rs b/src/storages.rs index 4031d61..b80536e 100644 --- a/src/storages.rs +++ b/src/storages.rs @@ -144,7 +144,6 @@ pub trait StorageExt { fn local_info(&self, device: &devices::Device) -> Option<&local_info::LocalInfo>; /// Get mount path of `self` on `device`. - /// `storages` is a `BTreeMap` with key of storage name and value of the storage. fn mount_path(&self, device: &devices::Device) -> Result; /// Add local info of `device` to `self`. From a409a439061e3ccd6687b8ef2456937445337f84 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Fri, 9 Aug 2024 21:45:44 +0200 Subject: [PATCH 02/20] (WIP) feat: stat for backup with name - replace path funcs return from Result to Option - add tests for `parent_backups` --- src/backups.rs | 8 +- src/cmd_backup.rs | 20 ++- src/cmd_status.rs | 217 ++++++++++++++++++++--- src/cmd_storage.rs | 4 +- src/storages.rs | 5 +- src/storages/directory.rs | 13 +- src/storages/online_storage.rs | 8 +- src/storages/physical_drive_partition.rs | 9 +- src/util.rs | 5 +- 9 files changed, 236 insertions(+), 53 deletions(-) diff --git a/src/backups.rs b/src/backups.rs index fbb4c1f..896fcd9 100644 --- a/src/backups.rs +++ b/src/backups.rs @@ -44,10 +44,12 @@ impl BackupTarget { } } - pub fn path(&self, storages: &Storages, device: &Device) -> Result { + /// Get full path of the [`BackupTarget`]. + pub fn path(&self, storages: &Storages, device: &Device) -> Option { let parent = storages.get(&self.storage).unwrap(); - let parent_path = parent.mount_path(device)?; - Ok(parent_path.join(self.path.clone())) + let parent_path = parent + .mount_path(device)?; + Some(parent_path.join(self.path.clone())) } } diff --git a/src/cmd_backup.rs b/src/cmd_backup.rs index 7ae9767..df6d13d 100644 --- a/src/cmd_backup.rs +++ b/src/cmd_backup.rs @@ -188,10 +188,16 @@ fn write_backups_list( ))?; name_width = name_width.max(backup.name().width()); dev_width = dev_width.max(dev.width()); - let src = backup.source().path(storages, device)?; + let src = backup + .source() + .path(storages, device) + .context("Couldn't get path for source")?; src_width = src_width.max(format!("{}", src.display()).width()); src_storage_width = src_storage_width.max(backup.source().storage.width()); - let dest = backup.destination().path(storages, device)?; + let dest = backup + .destination() + .path(storages, device) + .context("Couldn't get path for destination")?; dest_width = dest_width.max(format!("{}", dest.display()).width()); dest_storage_width = dest_storage_width.max(backup.destination().storage.width()); let cmd_name = backup.command().name(); @@ -203,8 +209,14 @@ fn write_backups_list( "Couldn't find the device specified in the backup config: {}", backup.name() ))?; - let src = backup.source().path(storages, device)?; - let dest = backup.destination().path(storages, device)?; + let src = backup + .source() + .path(storages, device) + .context("Couldn't get path for source")?; + let dest = backup + .destination() + .path(storages, device) + .context("Couldn't get path for destination")?; let cmd_name = backup.command().name(); let (last_backup_elapsed, style_on_time_elapsed) = match backup.last_backup() { Some(log) => { diff --git a/src/cmd_status.rs b/src/cmd_status.rs index eebc2cc..cd90860 100644 --- a/src/cmd_status.rs +++ b/src/cmd_status.rs @@ -6,9 +6,9 @@ use std::{ }; use crate::{ - backups::Backups, + backups::{Backup, Backups}, devices::{self, Device}, - storages::{self, StorageExt, Storages}, + storages::{self, Storage, StorageExt, Storages}, util, }; @@ -20,12 +20,14 @@ pub(crate) fn cmd_status( config_dir: &Path, ) -> Result<()> { let path = path.unwrap_or(env::current_dir().context("Failed to get current directory.")?); - let device = devices::get_device(config_dir)?; + let currrent_device = devices::get_device(config_dir)?; if show_storage { let storages = storages::Storages::read(config_dir)?; - let storage = util::min_parent_storage(&path, &storages, &device); + let storage = util::min_parent_storage(&path, &storages, &currrent_device); + trace!("storage {:?}", storage); + // TODO: recursively trace all storages for subdirectory? match storage { Some(storage) => { println!("Storage: {}", storage.0.name()) @@ -38,44 +40,217 @@ pub(crate) fn cmd_status( if show_backup { let devices = devices::get_devices(config_dir)?; let storages = storages::Storages::read(config_dir)?; - let backups = Backups::read(config_dir, &device)?; - let covering_backup = devices + let backups = Backups::read(config_dir, &currrent_device)?; + + let (target_storage, target_diff_from_storage) = + util::min_parent_storage(&path, &storages, &currrent_device) + .context("Target path is not covered in any storage")?; + + let covering_backup: Vec<_> = devices .iter() - .map(|device| (device, parent_backups(&path, &backups, &storages, device))); + .map(|device| { + ( + device, + parent_backups( + &target_diff_from_storage, + target_storage, + &backups, + &storages, + device, + ), + ) + }) + .collect(); + trace!("{:?}", covering_backup.first()); + + let name_len = &covering_backup + .iter() + .map(|(_, backups)| { + backups + .iter() + .map(|(backup, _path)| backup.name().len()) + .max() + .unwrap_or(0) + }) + .max() + .unwrap_or(5); for (backup_device, covering_backups) in covering_backup { println!("Device: {}", backup_device.name()); - for backup in covering_backups { - println!(" {}", console::style(backup.0).bold()); + for (backup, path_from_backup) in covering_backups { + println!( + " {:( - target_path: &'a PathBuf, + target_path_from_storage: &'a Path, + target_storage: &'a Storage, backups: &'a Backups, storages: &'a Storages, device: &'a Device, -) -> Vec<(&'a String, PathBuf)> { +) -> Vec<(&'a Backup, PathBuf)> { + trace!("Dev {:?}", device.name()); + let target_path = match target_storage.mount_path(device) { + Some(target_path) => target_path.join(target_path_from_storage), + None => return vec![], + }; + trace!("Path on the device {:?}", target_path); backups .list .iter() - .filter_map(|(k, v)| { - let backup_path = match v.source().path(storages, device) { - Ok(path) => path, - Err(e) => { - error!("Error while getting backup source path: {}", e); - return None; - } - }; - let diff = pathdiff::diff_paths(target_path, backup_path)?; + .filter_map(|(_k, backup)| { + let backup_path = backup.source().path(storages, device)?; + let diff = pathdiff::diff_paths(&target_path, backup_path)?; if diff.components().any(|c| c == path::Component::ParentDir) { None } else { - Some((k, diff)) + Some((backup, diff)) } }) .collect() } + +#[cfg(test)] +mod test { + use std::path::PathBuf; + + use crate::{ + backups::{self, ExternallyInvoked}, + devices, + storages::{self, online_storage::OnlineStorage, StorageExt}, + util, + }; + + use super::parent_backups; + + #[test] + fn test_parent_backups() { + let device1 = devices::Device::new("device_1".to_string()); + let mut storage1 = storages::Storage::Online(OnlineStorage::new( + "storage_1".to_string(), + "smb".to_string(), + 1_000_000, + "str1".to_string(), + PathBuf::from("/home/foo/"), + &device1, + )); + let storage2 = storages::Storage::Online(OnlineStorage::new( + "storage_2".to_string(), + "smb".to_string(), + 1_000_000_000, + "str2".to_string(), + PathBuf::from("/"), + &device1, + )); + let device2 = devices::Device::new("device_2".to_string()); + storage1 + .bound_on_device("alias".to_string(), PathBuf::from("/mnt/dev"), &device2) + .unwrap(); + let storage3 = storages::Storage::Online(OnlineStorage::new( + "storage_3".to_string(), + "smb".to_string(), + 2_000_000_000, + "str2".to_string(), + PathBuf::from("/"), + &device2, + )); + let storages = { + let mut storages = storages::Storages::new(); + storages.add(storage1).unwrap(); + storages.add(storage2).unwrap(); + storages.add(storage3).unwrap(); + storages + }; + + let backup1 = backups::Backup::new( + "backup_1".to_string(), + device1.name().to_string(), + backups::BackupTarget { + storage: "storage_1".to_string(), + path: PathBuf::from("bar"), + }, + backups::BackupTarget { + storage: "storage_1".to_string(), + path: PathBuf::from("hoge"), + }, + backups::BackupCommand::ExternallyInvoked(ExternallyInvoked::new( + "cmd".to_string(), + "".to_string(), + )), + ); + let backup2 = backups::Backup::new( + "backup_2".to_string(), + device2.name().to_string(), + backups::BackupTarget { + storage: "storage_1".to_string(), + path: PathBuf::from(""), + }, + backups::BackupTarget { + storage: "storage_3".to_string(), + path: PathBuf::from("foo"), + }, + backups::BackupCommand::ExternallyInvoked(ExternallyInvoked::new( + "cmd".to_string(), + "".to_string(), + )), + ); + + let backups = { + let mut backups = backups::Backups::new(); + backups.add(backup1).unwrap(); + backups.add(backup2).unwrap(); + backups + }; + + let target_path1 = PathBuf::from("/home/foo/bar/hoo"); + let (target_storage1, target_path_from_storage1) = + util::min_parent_storage(&target_path1, &storages, &device1) + .expect("Failed to get storage"); + let covering_backups_1 = parent_backups( + &target_path_from_storage1, + target_storage1, + &backups, + &storages, + &device1, + ); + assert_eq!(covering_backups_1.len(), 2); + + let target_path2 = PathBuf::from("/mnt/"); + let (target_storage2, target_path_from_storage2) = + util::min_parent_storage(&target_path2, &storages, &device2) + .expect("Failed to get storage"); + let covering_backups_2 = parent_backups( + &target_path_from_storage2, + target_storage2, + &backups, + &storages, + &device2, + ); + assert_eq!(covering_backups_2.len(), 0); + + let target_path3 = PathBuf::from("/mnt/dev/foo"); + let (target_storage3, target_path_from_storage3) = + util::min_parent_storage(&target_path3, &storages, &device2) + .expect("Failed to get storage"); + let covering_backups_3 = parent_backups( + &target_path_from_storage3, + target_storage3, + &backups, + &storages, + &device2, + ); + assert_eq!(covering_backups_3.len(), 1); + let mut covering_backup_names_3 = covering_backups_3.iter().map(|(backup, _)| backup.name()); + assert_eq!(covering_backup_names_3.next().unwrap(), "backup_2"); + assert!(covering_backup_names_3.next().is_none()); + } +} diff --git a/src/cmd_storage.rs b/src/cmd_storage.rs index 8c39646..2cb9414 100644 --- a/src/cmd_storage.rs +++ b/src/cmd_storage.rs @@ -215,8 +215,8 @@ fn write_storages_list( "" }; let path = storage.mount_path(device).map_or_else( - |e| { - info!("Not found: {}", e); + || { + info!("Mount path not found"); "".to_string() }, |v| v.display().to_string(), diff --git a/src/storages.rs b/src/storages.rs index b80536e..d4b9dd4 100644 --- a/src/storages.rs +++ b/src/storages.rs @@ -78,7 +78,7 @@ impl StorageExt for Storage { } } - fn mount_path(&self, device: &devices::Device) -> Result { + fn mount_path(&self, device: &devices::Device) -> Option { match self { Self::Physical(s) => s.mount_path(device), Self::SubDirectory(s) => s.mount_path(device), @@ -144,7 +144,8 @@ pub trait StorageExt { 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) -> Result; + /// Return [`None`] if the storage([`self`]) is not configured for the `device`. + fn mount_path(&self, device: &devices::Device) -> Option; /// Add local info of `device` to `self`. fn bound_on_device( diff --git a/src/storages/directory.rs b/src/storages/directory.rs index 4cd7803..f1e460c 100644 --- a/src/storages/directory.rs +++ b/src/storages/directory.rs @@ -19,7 +19,7 @@ pub struct Directory { /// Relative path to the parent storage. relative_path: path::PathBuf, pub notes: String, - /// Device and localinfo pairs. + /// [`devices::Device`] name and localinfo pairs. local_infos: BTreeMap, } @@ -80,7 +80,8 @@ impl Directory { let parent_mount_path = self .parent(storages) .context("Can't find parent storage")? - .mount_path(device)?; + .mount_path(device) + .context("Can't find mount path")?; Ok(parent_mount_path.join(self.relative_path.clone())) } } @@ -98,12 +99,10 @@ impl StorageExt for Directory { self.local_infos.get(&device.name()) } - fn mount_path(&self, device: &devices::Device) -> Result { - Ok(self - .local_infos + fn mount_path(&self, device: &devices::Device) -> Option { + self.local_infos .get(&device.name()) - .context(format!("LocalInfo for storage: {} not found", &self.name()))? - .mount_path()) + .map(|info| info.mount_path()) } /// This method doesn't use `mount_path`. diff --git a/src/storages/online_storage.rs b/src/storages/online_storage.rs index 4ce2459..6c4d62c 100644 --- a/src/storages/online_storage.rs +++ b/src/storages/online_storage.rs @@ -61,12 +61,10 @@ impl StorageExt for OnlineStorage { self.local_infos.get(&device.name()) } - fn mount_path(&self, device: &devices::Device) -> Result { - Ok(self - .local_infos + fn mount_path(&self, device: &devices::Device) -> Option { + self.local_infos .get(&device.name()) - .context(format!("LocalInfo for storage: {} not found", &self.name()))? - .mount_path()) + .map(|info| info.mount_path()) } fn bound_on_device( diff --git a/src/storages/physical_drive_partition.rs b/src/storages/physical_drive_partition.rs index 95dec3f..14a35a6 100644 --- a/src/storages/physical_drive_partition.rs +++ b/src/storages/physical_drive_partition.rs @@ -21,6 +21,7 @@ pub struct PhysicalDrivePartition { fs: String, is_removable: bool, // system_names: BTreeMap, + /// [`Device`] name and [`LocalInfo`] mapping. local_infos: BTreeMap, } @@ -112,12 +113,10 @@ impl StorageExt for PhysicalDrivePartition { self.local_infos.get(&device.name()) } - fn mount_path(&self, device: &devices::Device) -> Result { - Ok(self - .local_infos + fn mount_path(&self, device: &devices::Device) -> Option { + self.local_infos .get(&device.name()) - .context(format!("LocalInfo for storage: {} not found", &self.name()))? - .mount_path()) + .map(|info| info.mount_path()) } fn bound_on_device( diff --git a/src/util.rs b/src/util.rs index 820de33..8f27bc4 100644 --- a/src/util.rs +++ b/src/util.rs @@ -18,10 +18,7 @@ pub fn min_parent_storage<'a>( .list .iter() .filter_map(|(k, storage)| { - let storage_path = match storage.mount_path(device) { - Ok(path) => path, - Err(_) => return None, - }; + let storage_path = storage.mount_path(device)?; let diff = pathdiff::diff_paths(path, storage_path)?; if diff.components().any(|c| c == path::Component::ParentDir) { None From bc3939c9bc09bee4e5b0c943e6ecde3f652aa947 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Fri, 8 Nov 2024 06:09:04 +0900 Subject: [PATCH 03/20] (WIP): implementing multi device backup search --- src/backups.rs | 17 ++++++++-------- src/cmd_backup.rs | 15 ++------------ src/cmd_status.rs | 52 +++++++++++++++++++++++++++++++++++------------ src/util.rs | 12 +++++++++++ 4 files changed, 61 insertions(+), 35 deletions(-) diff --git a/src/backups.rs b/src/backups.rs index 896fcd9..5b39e61 100644 --- a/src/backups.rs +++ b/src/backups.rs @@ -27,7 +27,7 @@ pub fn backups_file(device: &Device) -> PathBuf { } /// Targets for backup source or destination. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackupTarget { /// `name()` of [`crate::storages::Storage`]. /// Use `String` for serialization/deserialization. @@ -47,14 +47,13 @@ impl BackupTarget { /// Get full path of the [`BackupTarget`]. pub fn path(&self, storages: &Storages, device: &Device) -> Option { let parent = storages.get(&self.storage).unwrap(); - let parent_path = parent - .mount_path(device)?; + let parent_path = parent.mount_path(device)?; Some(parent_path.join(self.path.clone())) } } /// Type of backup commands. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum BackupCommand { ExternallyInvoked(ExternallyInvoked), } @@ -81,7 +80,7 @@ impl BackupCommandExt for BackupCommand { /// Backup commands which is not invoked from xdbm itself. /// Call xdbm externally to record backup datetime and status. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExternallyInvoked { name: String, pub note: String, @@ -104,7 +103,7 @@ impl BackupCommandExt for ExternallyInvoked { } /// Backup execution log. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackupLog { pub datetime: DateTime, status: BackupResult, @@ -124,7 +123,7 @@ impl BackupLog { } /// Result of backup. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum BackupResult { Success, Failure, @@ -141,7 +140,7 @@ impl BackupResult { } /// Backup source, destination, command and logs. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Backup { /// must be unique name: String, @@ -202,7 +201,7 @@ impl Backup { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Backups { pub list: BTreeMap, } diff --git a/src/cmd_backup.rs b/src/cmd_backup.rs index df6d13d..9c58378 100644 --- a/src/cmd_backup.rs +++ b/src/cmd_backup.rs @@ -5,7 +5,7 @@ use std::{ }; use anyhow::{anyhow, Context, Ok, Result}; -use chrono::{Local, TimeDelta}; +use chrono::Local; use console::Style; use dunce::canonicalize; use git2::Repository; @@ -154,17 +154,6 @@ pub fn cmd_backup_list( Ok(()) } -fn duration_style(time: TimeDelta) -> Style { - match time { - x if x < TimeDelta::days(7) => Style::new().green(), - x if x < TimeDelta::days(14) => Style::new().yellow(), - x if x < TimeDelta::days(28) => Style::new().magenta(), - x if x < TimeDelta::days(28 * 3) => Style::new().red(), - x if x < TimeDelta::days(180) => Style::new().red().bold(), - _ => Style::new().on_red().black(), - } -} - /// TODO: status printing fn write_backups_list( mut writer: impl io::Write, @@ -222,7 +211,7 @@ fn write_backups_list( Some(log) => { let time = Local::now() - log.datetime; let s = util::format_summarized_duration(time); - let style = duration_style(time); + let style = util::duration_style(time); (style.apply_to(s), style) } None => { diff --git a/src/cmd_status.rs b/src/cmd_status.rs index cd90860..ba1ad87 100644 --- a/src/cmd_status.rs +++ b/src/cmd_status.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use console::Style; +use chrono::Local; use std::{ env, path::{self, Path, PathBuf}, @@ -40,7 +40,11 @@ pub(crate) fn cmd_status( if show_backup { let devices = devices::get_devices(config_dir)?; let storages = storages::Storages::read(config_dir)?; - let backups = Backups::read(config_dir, &currrent_device)?; + let backups = devices.iter().map(|device| { + Backups::read(config_dir, device) + .context("Backups were not found") + .unwrap() + }); let (target_storage, target_diff_from_storage) = util::min_parent_storage(&path, &storages, &currrent_device) @@ -48,13 +52,27 @@ pub(crate) fn cmd_status( let covering_backup: Vec<_> = devices .iter() - .map(|device| { + .zip(backups) + .map(|(device, backups)| { + debug!( + "dev {}, storage {:?}", + device.name(), + backups + .list + .iter() + .map(|(backup_name, backup)| format!( + "{} {}", + backup_name, + backup.source().storage + )) + .collect::>() + ); ( device, parent_backups( &target_diff_from_storage, target_storage, - &backups, + backups, &storages, device, ), @@ -78,9 +96,14 @@ pub(crate) fn cmd_status( for (backup_device, covering_backups) in covering_backup { println!("Device: {}", backup_device.name()); for (backup, path_from_backup) in covering_backups { + let last_backup = match backup.last_backup() { + Some(log) => util::format_summarized_duration(Local::now() - log.datetime), + None => "---".to_string(), + }; println!( - " {:( target_path_from_storage: &'a Path, target_storage: &'a Storage, - backups: &'a Backups, + backups: Backups, storages: &'a Storages, device: &'a Device, -) -> Vec<(&'a Backup, PathBuf)> { +) -> Vec<(Backup, PathBuf)> { trace!("Dev {:?}", device.name()); let target_path = match target_storage.mount_path(device) { Some(target_path) => target_path.join(target_path_from_storage), @@ -106,10 +129,12 @@ fn parent_backups<'a>( trace!("Path on the device {:?}", target_path); backups .list - .iter() + .into_iter() .filter_map(|(_k, backup)| { let backup_path = backup.source().path(storages, device)?; - let diff = pathdiff::diff_paths(&target_path, backup_path)?; + trace!("{:?}", backup_path.components()); + let diff = pathdiff::diff_paths(&target_path, backup_path.clone())?; + trace!("Backup: {:?}, Diff: {:?}", backup_path, diff); if diff.components().any(|c| c == path::Component::ParentDir) { None } else { @@ -218,7 +243,7 @@ mod test { let covering_backups_1 = parent_backups( &target_path_from_storage1, target_storage1, - &backups, + backups.clone(), &storages, &device1, ); @@ -231,7 +256,7 @@ mod test { let covering_backups_2 = parent_backups( &target_path_from_storage2, target_storage2, - &backups, + backups.clone(), &storages, &device2, ); @@ -244,12 +269,13 @@ mod test { let covering_backups_3 = parent_backups( &target_path_from_storage3, target_storage3, - &backups, + backups, &storages, &device2, ); assert_eq!(covering_backups_3.len(), 1); - let mut covering_backup_names_3 = covering_backups_3.iter().map(|(backup, _)| backup.name()); + let mut covering_backup_names_3 = + covering_backups_3.iter().map(|(backup, _)| backup.name()); assert_eq!(covering_backup_names_3.next().unwrap(), "backup_2"); assert!(covering_backup_names_3.next().is_none()); } diff --git a/src/util.rs b/src/util.rs index 8f27bc4..5730dc7 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,7 @@ use std::path::{self, PathBuf}; use anyhow::{Context, Result}; +use chrono::TimeDelta; use console::Style; use crate::{ @@ -59,6 +60,17 @@ pub fn format_summarized_duration(dt: chrono::Duration) -> String { } } +pub fn duration_style(time: TimeDelta) -> Style { + match time { + x if x < TimeDelta::days(7) => Style::new().green(), + x if x < TimeDelta::days(14) => Style::new().yellow(), + x if x < TimeDelta::days(28) => Style::new().magenta(), + x if x < TimeDelta::days(28 * 3) => Style::new().red(), + x if x < TimeDelta::days(180) => Style::new().red().bold(), + _ => Style::new().on_red().black(), + } +} + #[cfg(test)] mod test { use anyhow::Result; From 772689ab6af36974df642e4a7e326c358e33cdd5 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 1 Dec 2024 19:14:57 +0900 Subject: [PATCH 04/20] change: change type of relative path shared on multiple platforms to Vector Parsers for path on Windows and Unix are different on separator character treatment. Replacing to Vector avoids this differenct for cross-platform compatibility. --- src/backups.rs | 15 ++++++++++----- src/cmd_backup.rs | 8 ++++---- src/cmd_status.rs | 10 +++++----- src/storages/directory.rs | 37 ++++++++++++++++++++++--------------- 4 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/backups.rs b/src/backups.rs index 5b39e61..2816a6a 100644 --- a/src/backups.rs +++ b/src/backups.rs @@ -33,22 +33,27 @@ pub struct BackupTarget { /// Use `String` for serialization/deserialization. pub storage: String, /// Relative path to the `storage`. - pub path: PathBuf, + pub path: Vec, } impl BackupTarget { - pub fn new(storage_name: String, relative_path: PathBuf) -> Self { - BackupTarget { + pub fn new(storage_name: String, relative_path: PathBuf) -> Result { + let relative_path = relative_path + .components() + .map(|c| c.as_os_str().to_str().map(|s| s.to_owned())) + .collect::>() + .context("Path contains non-utf8 character")?; + Ok(BackupTarget { storage: storage_name, path: relative_path, - } + }) } /// Get full path of the [`BackupTarget`]. pub fn path(&self, storages: &Storages, device: &Device) -> Option { let parent = storages.get(&self.storage).unwrap(); let parent_path = parent.mount_path(device)?; - Some(parent_path.join(self.path.clone())) + Some(parent_path.join(self.path.clone().iter().collect::())) } } diff --git a/src/cmd_backup.rs b/src/cmd_backup.rs index 9c58378..cab7a9e 100644 --- a/src/cmd_backup.rs +++ b/src/cmd_backup.rs @@ -89,8 +89,8 @@ fn new_backup( Ok(Backup::new( name, device.name(), - src_target, - dest_target, + src_target?, + dest_target?, command, )) } @@ -361,9 +361,9 @@ mod test { &storages, )?; assert!(backup.source().storage == "online"); - assert_eq!(backup.source().path, PathBuf::from("docs")); + assert_eq!(backup.source().path, vec!["docs"]); assert!(backup.destination().storage == "online"); - assert!(backup.destination().path == PathBuf::from("tmp")); + assert!(backup.destination().path == vec!["tmp"]); Ok(()) } } diff --git a/src/cmd_status.rs b/src/cmd_status.rs index ba1ad87..c93e641 100644 --- a/src/cmd_status.rs +++ b/src/cmd_status.rs @@ -146,7 +146,7 @@ fn parent_backups<'a>( #[cfg(test)] mod test { - use std::path::PathBuf; + use std::{path::PathBuf, vec}; use crate::{ backups::{self, ExternallyInvoked}, @@ -201,11 +201,11 @@ mod test { device1.name().to_string(), backups::BackupTarget { storage: "storage_1".to_string(), - path: PathBuf::from("bar"), + path: vec!["bar".to_string()], }, backups::BackupTarget { storage: "storage_1".to_string(), - path: PathBuf::from("hoge"), + path: vec!["hoge".to_string()], }, backups::BackupCommand::ExternallyInvoked(ExternallyInvoked::new( "cmd".to_string(), @@ -217,11 +217,11 @@ mod test { device2.name().to_string(), backups::BackupTarget { storage: "storage_1".to_string(), - path: PathBuf::from(""), + path: vec!["".to_string()], }, backups::BackupTarget { storage: "storage_3".to_string(), - path: PathBuf::from("foo"), + path: vec!["foo".to_string()], }, backups::BackupCommand::ExternallyInvoked(ExternallyInvoked::new( "cmd".to_string(), diff --git a/src/storages/directory.rs b/src/storages/directory.rs index f1e460c..1777ec3 100644 --- a/src/storages/directory.rs +++ b/src/storages/directory.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; use std::{collections::BTreeMap, fmt, path}; use crate::devices; @@ -17,7 +18,7 @@ pub struct Directory { /// ID of parent storage. parent: String, /// Relative path to the parent storage. - relative_path: path::PathBuf, + relative_path: Vec, pub notes: String, /// [`devices::Device`] name and localinfo pairs. local_infos: BTreeMap, @@ -34,14 +35,19 @@ impl Directory { relative_path: path::PathBuf, notes: String, local_infos: BTreeMap, - ) -> Directory { - Directory { + ) -> Result { + let relative_path = relative_path + .components() + .map(|c| c.as_os_str().to_str().map(|s| s.to_owned())) + .collect::>>() + .context("Path contains non-utf8 character")?; + Ok(Directory { name, parent, relative_path, notes, local_infos, - } + }) } pub fn try_from_device_path( @@ -56,23 +62,23 @@ impl Directory { .context("Failed to compare diff of paths")?; trace!("Selected parent: {}", parent.name()); let local_info = LocalInfo::new(alias, path); - Ok(Directory::new( + Directory::new( name, parent.name().to_string(), diff_path, notes, BTreeMap::from([(device.name(), local_info)]), - )) + ) } pub fn update_note(self, notes: String) -> Directory { - Directory::new( - self.name, - self.parent, - self.relative_path, + Directory { + name: self.name, + parent: self.parent, + relative_path: self.relative_path, notes, - self.local_infos, - ) + local_infos: self.local_infos, + } } /// Resolve mount path of directory with current device. @@ -82,7 +88,7 @@ impl Directory { .context("Can't find parent storage")? .mount_path(device) .context("Can't find mount path")?; - Ok(parent_mount_path.join(self.relative_path.clone())) + Ok(parent_mount_path.join(self.relative_path.clone().iter().collect::())) } } @@ -133,7 +139,7 @@ impl fmt::Display for Directory { "S {name:<10} < {parent:<10}{relative_path:<10} : {notes}", name = self.name(), parent = self.parent, - relative_path = self.relative_path.display(), + relative_path = self.relative_path.iter().collect::().display(), notes = self.notes, ) } @@ -177,7 +183,8 @@ mod test { "subdir".into(), "some note".to_string(), local_infos, - ); + ) + .unwrap(); let mut storages = Storages::new(); storages.add(storages::Storage::Physical(physical)).unwrap(); storages.add(Storage::SubDirectory(directory)).unwrap(); From 8b0dbb2314d2ab3cd9081163899ea4e52a193e4d Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 1 Dec 2024 19:19:28 +0900 Subject: [PATCH 05/20] fix: add lifetime annotation (clippy) --- src/backups.rs | 2 +- src/storages/directory.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backups.rs b/src/backups.rs index 2816a6a..8f70509 100644 --- a/src/backups.rs +++ b/src/backups.rs @@ -180,7 +180,7 @@ impl Backup { &self.name } - pub fn device<'a>(&'a self, devices: &'a [Device]) -> Option<&Device> { + pub fn device<'a>(&'a self, devices: &'a [Device]) -> Option<&'a Device> { devices.iter().find(|dev| dev.name() == self.device) } diff --git a/src/storages/directory.rs b/src/storages/directory.rs index 1777ec3..1642518 100644 --- a/src/storages/directory.rs +++ b/src/storages/directory.rs @@ -127,7 +127,7 @@ impl StorageExt for Directory { } // Get parent `&Storage` of directory. - fn parent<'a>(&'a self, storages: &'a Storages) -> Option<&Storage> { + fn parent<'a>(&'a self, storages: &'a Storages) -> Option<&'a Storage> { storages.get(&self.parent) } } From dbc0d78f99de53077ebf3c3577b779e35a39fc50 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 1 Dec 2024 19:36:33 +0900 Subject: [PATCH 06/20] update: CHANGELOG on path type change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 685004c..dd526a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changed - Colored output for `storage list` and `backup list` ([#15](https://github.com/qwjyh/xdbm/pull/15)) +- **BREAKING** Relative path is changed from `PathBuf` to `Vector` for portability. This means that existing config files need to be changed. ## [0.2.1] - 2024-06-19 From ced354bf583425a0f7fe6d38b5b283a04e2da2d7 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 1 Dec 2024 20:37:23 +0900 Subject: [PATCH 07/20] fix: sub directory decision logic on windows Diff of "C:\foo" from "D:\" is "C:\foo" and doesn't contain "..". In old logic, "C:\foo" is treated as subpath of "D:\" but this is not intuitive. --- src/cmd_status.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cmd_status.rs b/src/cmd_status.rs index c93e641..c80ff89 100644 --- a/src/cmd_status.rs +++ b/src/cmd_status.rs @@ -135,7 +135,11 @@ fn parent_backups<'a>( trace!("{:?}", backup_path.components()); let diff = pathdiff::diff_paths(&target_path, backup_path.clone())?; trace!("Backup: {:?}, Diff: {:?}", backup_path, diff); - if diff.components().any(|c| c == path::Component::ParentDir) { + // note: Should `RootDir` is included in this list? + if diff + .components() + .any(|c| matches!(c, path::Component::ParentDir | path::Component::Prefix(_))) + { None } else { Some((backup, diff)) From 6a0abd03d5843fc6432d28523eeb49bfe1ca2447 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 1 Dec 2024 20:55:49 +0900 Subject: [PATCH 08/20] fix: apply subpath logic patch to `util::min_parent_storage` --- src/util.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/util.rs b/src/util.rs index 5730dc7..ea2c947 100644 --- a/src/util.rs +++ b/src/util.rs @@ -21,7 +21,10 @@ pub fn min_parent_storage<'a>( .filter_map(|(k, storage)| { let storage_path = storage.mount_path(device)?; let diff = pathdiff::diff_paths(path, storage_path)?; - if diff.components().any(|c| c == path::Component::ParentDir) { + if diff + .components() + .any(|c| matches!(c, path::Component::ParentDir | path::Component::Prefix(_))) + { None } else { Some((k, diff)) From d743e607accea646a41c56f9a7e3cbd7ca9ede57 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 1 Dec 2024 23:14:50 +0900 Subject: [PATCH 09/20] update: remove todo!() at the end of cmd_status --- src/cmd_status.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cmd_status.rs b/src/cmd_status.rs index c80ff89..ebc61d2 100644 --- a/src/cmd_status.rs +++ b/src/cmd_status.rs @@ -109,7 +109,8 @@ pub(crate) fn cmd_status( } } } - todo!() + + Ok(()) } /// Get [`Backup`]s for `device` which covers `target_path`. From 315a75424aded1688c97ea9a716927c2b50f8428 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 1 Dec 2024 23:31:58 +0900 Subject: [PATCH 10/20] add(test): command backup after done doesn't include --- --- tests/cli.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index 4dd4fe6..3c399cd 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -448,6 +448,21 @@ mod integrated_test { .assert() .success(); + // backup list after backup done + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_2.path()) + .arg("backup") + .arg("list") + .assert() + .success() + .stdout( + predicate::str::contains("foodoc") + .and(predicate::str::contains("nas")) + .and(predicate::str::contains("gdrive_docs")) + .and(predicate::str::contains("---").not()), + ); + Ok(()) } } From bce9f3710c001964eca9a720464cf6776507bf0b Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 1 Dec 2024 23:32:36 +0900 Subject: [PATCH 11/20] add(test): command status test --- tests/cli.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index 3c399cd..bad49e8 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -463,6 +463,49 @@ mod integrated_test { .and(predicate::str::contains("---").not()), ); + // status + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_2.path()) + .arg("status") + .assert() + .success(); + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_2.path()) + .arg("status") + .arg("-s") + .arg(backup_src.clone().join("foo")) + .assert() + .success() + .stdout(predicate::str::contains("nas").and(predicate::str::contains("foodoc").not())); + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_2.path()) + .arg("status") + .arg("-sb") + .arg(backup_src.clone().join("foo")) + .assert() + .success() + .stdout( + predicate::str::contains("nas") + .and(predicate::str::contains("second")) + .and(predicate::str::contains("foodoc")), + ); + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_2.path()) + .arg("status") + .arg("-sb") + .arg(backup_src.clone().parent().unwrap()) + .assert() + .success() + .stdout( + predicate::str::contains("nas") + .and(predicate::str::contains("second")) + .and(predicate::str::contains("foodoc").not()), + ); + Ok(()) } } From 697c9c0712ce7958f08aeaa4636ed14e5fc3845c Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 1 Dec 2024 23:33:24 +0900 Subject: [PATCH 12/20] add(test): test for multiple backups and status & add comment comment for current config --- tests/cli.rs | 274 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index bad49e8..b082182 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -216,6 +216,8 @@ mod integrated_test { #[test] fn two_devices() -> Result<()> { // 1st device + // + // devices: first let config_dir_1 = assert_fs::TempDir::new()?; setup_gitconfig()?; let mut cmd1 = Command::cargo_bin("xdbm")?; @@ -248,6 +250,8 @@ mod integrated_test { ))?; // 2nd device + // + // devices: first, second let config_dir_2 = assert_fs::TempDir::new()?; let mut cmd2 = Command::cargo_bin("xdbm")?; cmd2.arg("-c") @@ -271,6 +275,7 @@ mod integrated_test { assert!(config_dir_2.join("backups").join("first.yml").exists()); assert!(config_dir_2.join("backups").join("second.yml").exists()); + // sync std::process::Command::new("git") .arg("push") .current_dir(&config_dir_2) @@ -287,6 +292,11 @@ mod integrated_test { .success(); // Add storage + // + // devices: first, second + // storages: + // - gdrive @ sample_storage (online) + // - first: sample_storage let sample_storage = assert_fs::TempDir::new()?; let mut cmd_add_storage_1 = Command::cargo_bin("xdbm")?; cmd_add_storage_1 @@ -308,6 +318,13 @@ mod integrated_test { .success() .stdout(predicate::str::contains("")); // Add storage (directory) + // + // devices: first, second + // storages: + // - gdrive (online) + // - first: sample_storage + // - gdrive_docs (subdir of sample_storage/foo/bar) + // - first let sample_directory = &sample_storage.join("foo").join("bar"); DirBuilder::new().recursive(true).create(sample_directory)?; Command::cargo_bin("xdbm")? @@ -339,6 +356,14 @@ mod integrated_test { .success(); // bind + // + // devices: first, second + // storages: + // - gdrive (online) + // - first: sample_storage + // - gdrive_docs (subdir of sample_storage/foo/bar) + // - first + // - second: sample_directory Command::cargo_bin("xdbm")? .arg("-c") .arg(config_dir_2.path()) @@ -354,6 +379,16 @@ mod integrated_test { .stdout(predicate::str::contains("")); // storage 3 + // + // devices: first, second + // storages: + // - gdrive (online) + // - first: sample_storage + // - gdrive_docs (subdir of sample_storage/foo/bar) + // - first + // - second: sample_directory + // - nas (online) + // - second: sample_storage_2 let sample_storage_2 = assert_fs::TempDir::new()?; Command::cargo_bin("xdbm")? .arg("-c") @@ -384,6 +419,19 @@ mod integrated_test { .stdout(predicate::str::contains("gdrive_docs").and(predicate::str::contains("nas"))); // backup add + // + // devices: first, second + // storages: + // - gdrive (online) + // - first: sample_storage + // - gdrive_docs (subdir of sample_storage/foo/bar) + // - first + // - second: sample_directory + // - nas (online) + // - second: sample_storage_2 + // backups: + // - foodoc: second + // - sample_storage_2/foo/bar -> sample_directory/docs let backup_src = &sample_storage_2.join("foo").join("bar"); DirBuilder::new().recursive(true).create(backup_src)?; let backup_dest = &sample_directory.join("docs"); @@ -438,6 +486,19 @@ mod integrated_test { ); // backup done + // + // devices: first, second + // storages: + // - gdrive (online) + // - first: sample_storage + // - gdrive_docs (subdir of sample_storage/foo/bar) + // - first + // - second: sample_directory + // - nas (online) + // - second: sample_storage_2 + // backups: + // - foodoc: second + // - sample_storage_2/foo/bar -> sample_directory/docs (done 1) Command::cargo_bin("xdbm")? .arg("-c") .arg(config_dir_2.path()) @@ -506,6 +567,219 @@ mod integrated_test { .and(predicate::str::contains("foodoc").not()), ); + std::process::Command::new("git") + .arg("push") + .current_dir(&config_dir_2) + .assert() + .success(); + std::process::Command::new("git") + .arg("pull") + .current_dir(&config_dir_1) + .assert() + .success(); + + // bind + // + // devices: first, second + // storages: + // - gdrive (online) + // - first: sample_storage + // - gdrive_docs (subdir of sample_storage/foo/bar) + // - first + // - second: sample_directory + // - nas (online) + // - first: sample_storage_2_first_path + // - second: sample_storage_2 + // backups: + // - foodoc: second + // - sample_storage_2/foo/bar -> sample_directory/docs (done 1) + let sample_storage_2_first_path = assert_fs::TempDir::new()?; + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_1.path()) + .arg("storage") + .arg("bind") + .arg("--alias") + .arg("sample2") + .arg("--path") + .arg(sample_storage_2_first_path.path()) + .arg("nas") + .assert() + .success() + .stdout(predicate::str::contains("")); + + // backup add + // + // devices: first, second + // storages: + // - gdrive (online) + // - first: sample_storage + // - gdrive_docs (subdir of sample_storage/foo/bar) + // - first + // - second: sample_directory + // - nas (online) + // - first: sample_storage_2_first_path + // - second: sample_storage_2 + // backups: + // - foodoc: second + // - sample_storage_2/foo/bar -> sample_directory/docs (done 1) + // - abcdbackup: first + // - sample_storage_2_first_path/abcd/efgh -> sample_storage/Downloads/abcd/efgh + let backup_src = &sample_storage_2_first_path.join("abcd").join("efgh"); + DirBuilder::new().recursive(true).create(backup_src)?; + let backup_dest = &sample_storage.join("Downloads").join("abcd").join("efgh"); + DirBuilder::new().recursive(true).create(backup_dest)?; + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_1.path()) + .arg("backup") + .arg("add") + .arg("--src") + .arg(backup_src) + .arg("--dest") + .arg(backup_dest) + .arg("abcdbackup") + .arg("external") + .arg("rsync") + .arg("note: nonsense") + .assert() + .success(); + + // backup add + // + // devices: first, second + // storages: + // - gdrive (online) + // - first: sample_storage + // - gdrive_docs (subdir of sample_storage/foo/bar) + // - first + // - second: sample_directory + // - nas (online) + // - first: sample_storage_2_first_path + // - second: sample_storage_2 + // backups: + // - foodoc: second + // - sample_storage_2/foo/bar -> sample_directory/docs (done 1) + // - abcdbackup: first + // - sample_storage_2_first_path/abcd/efgh -> sample_storage/Downloads/abcd/efgh + // - abcdsubbackup: first + // - sample_storage_2_first_path/abcd/efgh/sub -> sample_storage/Downloads/abcd/efgh/sub + let backup_src = &sample_storage_2_first_path + .join("abcd") + .join("efgh") + .join("sub"); + DirBuilder::new().recursive(true).create(backup_src)?; + let backup_dest = &sample_storage + .join("Downloads") + .join("abcd") + .join("efgh") + .join("sub"); + DirBuilder::new().recursive(true).create(backup_dest)?; + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_1.path()) + .arg("backup") + .arg("add") + .arg("--src") + .arg(backup_src) + .arg("--dest") + .arg(backup_dest) + .arg("abcdsubbackup") + .arg("external") + .arg("rsync") + .arg("note: only subdirectory") + .assert() + .success(); + + std::process::Command::new("git") + .arg("push") + .current_dir(&config_dir_1) + .assert() + .success(); + std::process::Command::new("git") + .arg("pull") + .current_dir(&config_dir_2) + .assert() + .success(); + + // backup add + // + // devices: first, second + // storages: + // - gdrive (online) + // - first: sample_storage + // - gdrive_docs (subdir of sample_storage/foo/bar) + // - first + // - second: sample_directory + // - nas (online) + // - first: sample_storage_2_first_path + // - second: sample_storage_2 + // backups: + // - foodoc: second + // - sample_storage_2/foo/bar -> sample_directory/docs (done 1) + // - abcdbackup: first + // - sample_storage_2_first_path/abcd/efgh -> sample_storage/Downloads/abcd/efgh + // - abcdsubbackup: first + // - sample_storage_2_first_path/abcd/efgh/sub -> sample_storage/Downloads/abcd/efgh/sub + // - abcdbackup2: second + // - sample_storage_2/abcd/efgh -> sample_directory/Downloads/abcd/efgh + let backup_src = &sample_storage_2.join("abcd").join("efgh"); + DirBuilder::new().recursive(true).create(backup_src)?; + let backup_dest = &sample_directory + .join("Downloads") + .join("abcd") + .join("efgh"); + DirBuilder::new().recursive(true).create(backup_dest)?; + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_2.path()) + .arg("backup") + .arg("add") + .arg("--src") + .arg(backup_src) + .arg("--dest") + .arg(backup_dest) + .arg("abcdbackup2") + .arg("external") + .arg("rsync") + .arg("note: only subdirectory") + .assert() + .success(); + + // status + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_2.path()) + .arg("status") + .arg("-sb") + .arg(backup_src) + .assert() + .success() + .stdout( + predicate::str::contains("nas") + .and(predicate::str::contains("first")) + .and(predicate::str::contains("abcdbackup")) + .and(predicate::str::contains("abcdsubbackup").not()) + .and(predicate::str::contains("second")) + .and(predicate::str::contains("abcdbackup2")), + ); + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_2.path()) + .arg("status") + .arg("-sb") + .arg(backup_src.join("sub")) + .assert() + .success() + .stdout( + predicate::str::contains("nas") + .and(predicate::str::contains("first")) + .and(predicate::str::contains("abcdbackup")) + .and(predicate::str::contains("abcdsubbackup")) + .and(predicate::str::contains("second")) + .and(predicate::str::contains("abcdbackup2")), + ); + Ok(()) } } From c4aa76d1251301dd815ed298de9cb59ff2e820ef Mon Sep 17 00:00:00 2001 From: qwjyh Date: Mon, 2 Dec 2024 02:15:49 +0900 Subject: [PATCH 13/20] update(test): add prefix '_' to unused variables --- tests/cli.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cli.rs b/tests/cli.rs index b082182..3407c86 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -145,7 +145,7 @@ mod integrated_test { // bare-repo let bare_repo_dir = assert_fs::TempDir::new()?; - let bare_repo = Repository::init_bare(&bare_repo_dir)?; + let _bare_repo = Repository::init_bare(&bare_repo_dir)?; // push to bare repository let repo_1 = Repository::open(&config_dir_1)?; let upstream_name = "remote"; @@ -229,7 +229,7 @@ mod integrated_test { // bare-repo let bare_repo_dir = assert_fs::TempDir::new()?; - let bare_repo = Repository::init_bare(&bare_repo_dir)?; + let _bare_repo = Repository::init_bare(&bare_repo_dir)?; // push to bare repository let repo_1 = Repository::open(&config_dir_1)?; let upstream_name = "remote"; From af1b56eee21a151ebdf5e2f51352db48bb3a6b47 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Mon, 2 Dec 2024 02:52:53 +0900 Subject: [PATCH 14/20] add: style to cmd_status --- src/cmd_status.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/cmd_status.rs b/src/cmd_status.rs index ebc61d2..7c7f7f2 100644 --- a/src/cmd_status.rs +++ b/src/cmd_status.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use chrono::Local; +use console::Style; use std::{ env, path::{self, Path, PathBuf}, @@ -96,14 +97,20 @@ pub(crate) fn cmd_status( for (backup_device, covering_backups) in covering_backup { println!("Device: {}", backup_device.name()); for (backup, path_from_backup) in covering_backups { - let last_backup = match backup.last_backup() { - Some(log) => util::format_summarized_duration(Local::now() - log.datetime), - None => "---".to_string(), + let (last_backup, style) = match backup.last_backup() { + Some(log) => { + let timediff = Local::now() - log.datetime; + ( + util::format_summarized_duration(timediff), + util::duration_style(timediff), + ) + } + None => ("---".to_string(), Style::new().red()), }; println!( " {: Date: Mon, 2 Dec 2024 02:53:30 +0900 Subject: [PATCH 15/20] update: don't display devices with no backups in cmd_status --- src/cmd_status.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cmd_status.rs b/src/cmd_status.rs index 7c7f7f2..5215ac7 100644 --- a/src/cmd_status.rs +++ b/src/cmd_status.rs @@ -95,6 +95,10 @@ pub(crate) fn cmd_status( .unwrap_or(5); for (backup_device, covering_backups) in covering_backup { + if covering_backups.is_empty() { + continue; + } + println!("Device: {}", backup_device.name()); for (backup, path_from_backup) in covering_backups { let (last_backup, style) = match backup.last_backup() { From 24f13566854805b0bd3105642554b9ba34a48a37 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Mon, 2 Dec 2024 02:59:34 +0900 Subject: [PATCH 16/20] fix(test): for commit 7e043a652ca7a9c48582e8a7ed784715d3eaa1aa --- tests/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cli.rs b/tests/cli.rs index 3407c86..fe9a837 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -563,7 +563,7 @@ mod integrated_test { .success() .stdout( predicate::str::contains("nas") - .and(predicate::str::contains("second")) + .and(predicate::str::contains("second").not()) .and(predicate::str::contains("foodoc").not()), ); From 996ca7e4b3af0c8acd2933fca1d4a43c6d2ccf1f Mon Sep 17 00:00:00 2001 From: qwjyh Date: Mon, 2 Dec 2024 03:00:56 +0900 Subject: [PATCH 17/20] run fmt --- tests/cli.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/cli.rs b/tests/cli.rs index fe9a837..929a74e 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -725,10 +725,7 @@ mod integrated_test { // - sample_storage_2/abcd/efgh -> sample_directory/Downloads/abcd/efgh let backup_src = &sample_storage_2.join("abcd").join("efgh"); DirBuilder::new().recursive(true).create(backup_src)?; - let backup_dest = &sample_directory - .join("Downloads") - .join("abcd") - .join("efgh"); + let backup_dest = &sample_directory.join("Downloads").join("abcd").join("efgh"); DirBuilder::new().recursive(true).create(backup_dest)?; Command::cargo_bin("xdbm")? .arg("-c") From 3d8aa7bca0ecf1bb745d4733cea7c56e7bad2dfb Mon Sep 17 00:00:00 2001 From: qwjyh Date: Mon, 2 Dec 2024 03:05:43 +0900 Subject: [PATCH 18/20] update: CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd526a2..16a827f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - Colored output for `storage list` and `backup list` ([#15](https://github.com/qwjyh/xdbm/pull/15)) - **BREAKING** Relative path is changed from `PathBuf` to `Vector` for portability. This means that existing config files need to be changed. +- Add `status` subcommand to see storage and backup on given path or current working directory ([#17](https://github.com/qwjyh/xdbm/pull/17)). ## [0.2.1] - 2024-06-19 From 07ef49ca7b203b995823fb3fb0f91b5bc052495e Mon Sep 17 00:00:00 2001 From: qwjyh Date: Mon, 2 Dec 2024 03:07:25 +0900 Subject: [PATCH 19/20] add: long argument option to cmd_status --- src/cmd_args.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmd_args.rs b/src/cmd_args.rs index 4627f97..c0b46b7 100644 --- a/src/cmd_args.rs +++ b/src/cmd_args.rs @@ -49,10 +49,10 @@ pub(crate) enum Commands { /// Target path. Default is the current directory. path: Option, /// Show storage which the path belongs to. - #[arg(short)] + #[arg(short, long)] storage: bool, /// Show backup config covering the path. - #[arg(short)] + #[arg(short, long)] backup: bool, }, From 4e8387e3b05477db78288b0dd27a10ab000f82c5 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Mon, 2 Dec 2024 03:07:48 +0900 Subject: [PATCH 20/20] fix typo --- src/cmd_status.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cmd_status.rs b/src/cmd_status.rs index 5215ac7..560c02f 100644 --- a/src/cmd_status.rs +++ b/src/cmd_status.rs @@ -21,11 +21,11 @@ pub(crate) fn cmd_status( config_dir: &Path, ) -> Result<()> { let path = path.unwrap_or(env::current_dir().context("Failed to get current directory.")?); - let currrent_device = devices::get_device(config_dir)?; + let current_device = devices::get_device(config_dir)?; if show_storage { let storages = storages::Storages::read(config_dir)?; - let storage = util::min_parent_storage(&path, &storages, &currrent_device); + let storage = util::min_parent_storage(&path, &storages, ¤t_device); trace!("storage {:?}", storage); // TODO: recursively trace all storages for subdirectory? @@ -48,7 +48,7 @@ pub(crate) fn cmd_status( }); let (target_storage, target_diff_from_storage) = - util::min_parent_storage(&path, &storages, &currrent_device) + util::min_parent_storage(&path, &storages, ¤t_device) .context("Target path is not covered in any storage")?; let covering_backup: Vec<_> = devices