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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 685004c..16a827f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### 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 diff --git a/src/backups.rs b/src/backups.rs index 896fcd9..8f70509 100644 --- a/src/backups.rs +++ b/src/backups.rs @@ -27,34 +27,38 @@ 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. 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())) + let parent_path = parent.mount_path(device)?; + Some(parent_path.join(self.path.clone().iter().collect::())) } } /// Type of backup commands. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum BackupCommand { ExternallyInvoked(ExternallyInvoked), } @@ -81,7 +85,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 +108,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 +128,7 @@ impl BackupLog { } /// Result of backup. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum BackupResult { Success, Failure, @@ -141,7 +145,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, @@ -176,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) } @@ -202,7 +206,7 @@ impl Backup { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Backups { pub list: BTreeMap, } 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, }, diff --git a/src/cmd_backup.rs b/src/cmd_backup.rs index df6d13d..cab7a9e 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; @@ -89,8 +89,8 @@ fn new_backup( Ok(Backup::new( name, device.name(), - src_target, - dest_target, + src_target?, + dest_target?, command, )) } @@ -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 => { @@ -372,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 cd90860..560c02f 100644 --- a/src/cmd_status.rs +++ b/src/cmd_status.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use chrono::Local; use console::Style; use std::{ env, @@ -20,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? @@ -40,21 +41,39 @@ 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) + util::min_parent_storage(&path, &storages, ¤t_device) .context("Target path is not covered in any storage")?; 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, ), @@ -76,17 +95,33 @@ 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() { + Some(log) => { + let timediff = Local::now() - log.datetime; + ( + util::format_summarized_duration(timediff), + util::duration_style(timediff), + ) + } + None => ("---".to_string(), Style::new().red()), + }; 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,11 +141,17 @@ 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)?; - if diff.components().any(|c| c == path::Component::ParentDir) { + trace!("{:?}", backup_path.components()); + let diff = pathdiff::diff_paths(&target_path, backup_path.clone())?; + trace!("Backup: {:?}, Diff: {:?}", backup_path, diff); + // 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)) @@ -121,7 +162,7 @@ fn parent_backups<'a>( #[cfg(test)] mod test { - use std::path::PathBuf; + use std::{path::PathBuf, vec}; use crate::{ backups::{self, ExternallyInvoked}, @@ -176,11 +217,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(), @@ -192,11 +233,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(), @@ -218,7 +259,7 @@ mod test { let covering_backups_1 = parent_backups( &target_path_from_storage1, target_storage1, - &backups, + backups.clone(), &storages, &device1, ); @@ -231,7 +272,7 @@ mod test { let covering_backups_2 = parent_backups( &target_path_from_storage2, target_storage2, - &backups, + backups.clone(), &storages, &device2, ); @@ -244,12 +285,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/storages/directory.rs b/src/storages/directory.rs index f1e460c..1642518 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::())) } } @@ -121,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) } } @@ -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(); diff --git a/src/util.rs b/src/util.rs index 8f27bc4..ea2c947 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::{ @@ -20,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)) @@ -59,6 +63,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; diff --git a/tests/cli.rs b/tests/cli.rs index 4dd4fe6..929a74e 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"; @@ -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")?; @@ -227,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"; @@ -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()) @@ -448,6 +509,274 @@ 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()), + ); + + // 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").not()) + .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(()) } }