From bc3939c9bc09bee4e5b0c943e6ecde3f652aa947 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Fri, 8 Nov 2024 06:09:04 +0900 Subject: [PATCH 01/19] (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 02/19] 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 03/19] 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 04/19] 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 05/19] 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 06/19] 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 8fc8029435eac304098b4164344f5279de783836 Mon Sep 17 00:00:00 2001 From: qwjyh <62229267+qwjyh@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:13:42 +0900 Subject: [PATCH 07/19] Update CI and add lint (#18) * fix: update actions/checkout from v3 to v4 * update(CI): add clippy --- .github/workflows/rust.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 31000a2..7cee19b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -10,13 +10,17 @@ env: CARGO_TERM_COLOR: always jobs: - build: + build-and-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Setup + run: rustup component add clippy - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose + - name: Lint + run: cargo clippy --all-targets --all-features From d743e607accea646a41c56f9a7e3cbd7ca9ede57 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 1 Dec 2024 23:14:50 +0900 Subject: [PATCH 08/19] 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 09/19] 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 10/19] 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 11/19] 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 12/19] 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 13/19] 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 14/19] 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 15/19] 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 16/19] 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 17/19] 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 18/19] 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 19/19] 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