mirror of
https://github.com/qwjyh/xdbm
synced 2024-12-05 04:51:04 +09:00
Compare commits
20 commits
a409a43906
...
4e8387e3b0
Author | SHA1 | Date | |
---|---|---|---|
4e8387e3b0 | |||
07ef49ca7b | |||
3d8aa7bca0 | |||
996ca7e4b3 | |||
24f1356685 | |||
7e043a652c | |||
af1b56eee2 | |||
c4aa76d125 | |||
697c9c0712 | |||
bce9f3710c | |||
315a75424a | |||
d743e607ac | |||
7592ec0ad0 | |||
|
8fc8029435 | ||
6a0abd03d5 | |||
ced354bf58 | |||
dbc0d78f99 | |||
8b0dbb2314 | |||
772689ab6a | |||
bc3939c9bc |
9 changed files with 469 additions and 77 deletions
8
.github/workflows/rust.yml
vendored
8
.github/workflows/rust.yml
vendored
|
@ -10,13 +10,17 @@ env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-and-lint:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup
|
||||||
|
run: rustup component add clippy
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --verbose
|
run: cargo build --verbose
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --verbose
|
run: cargo test --verbose
|
||||||
|
- name: Lint
|
||||||
|
run: cargo clippy --all-targets --all-features
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Colored output for `storage list` and `backup list` ([#15](https://github.com/qwjyh/xdbm/pull/15))
|
- 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<String>` 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
|
## [0.2.1] - 2024-06-19
|
||||||
|
|
||||||
|
|
|
@ -27,34 +27,38 @@ pub fn backups_file(device: &Device) -> PathBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Targets for backup source or destination.
|
/// Targets for backup source or destination.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct BackupTarget {
|
pub struct BackupTarget {
|
||||||
/// `name()` of [`crate::storages::Storage`].
|
/// `name()` of [`crate::storages::Storage`].
|
||||||
/// Use `String` for serialization/deserialization.
|
/// Use `String` for serialization/deserialization.
|
||||||
pub storage: String,
|
pub storage: String,
|
||||||
/// Relative path to the `storage`.
|
/// Relative path to the `storage`.
|
||||||
pub path: PathBuf,
|
pub path: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackupTarget {
|
impl BackupTarget {
|
||||||
pub fn new(storage_name: String, relative_path: PathBuf) -> Self {
|
pub fn new(storage_name: String, relative_path: PathBuf) -> Result<Self> {
|
||||||
BackupTarget {
|
let relative_path = relative_path
|
||||||
|
.components()
|
||||||
|
.map(|c| c.as_os_str().to_str().map(|s| s.to_owned()))
|
||||||
|
.collect::<Option<_>>()
|
||||||
|
.context("Path contains non-utf8 character")?;
|
||||||
|
Ok(BackupTarget {
|
||||||
storage: storage_name,
|
storage: storage_name,
|
||||||
path: relative_path,
|
path: relative_path,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get full path of the [`BackupTarget`].
|
/// Get full path of the [`BackupTarget`].
|
||||||
pub fn path(&self, storages: &Storages, device: &Device) -> Option<PathBuf> {
|
pub fn path(&self, storages: &Storages, device: &Device) -> Option<PathBuf> {
|
||||||
let parent = storages.get(&self.storage).unwrap();
|
let parent = storages.get(&self.storage).unwrap();
|
||||||
let parent_path = parent
|
let parent_path = parent.mount_path(device)?;
|
||||||
.mount_path(device)?;
|
Some(parent_path.join(self.path.clone().iter().collect::<PathBuf>()))
|
||||||
Some(parent_path.join(self.path.clone()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type of backup commands.
|
/// Type of backup commands.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum BackupCommand {
|
pub enum BackupCommand {
|
||||||
ExternallyInvoked(ExternallyInvoked),
|
ExternallyInvoked(ExternallyInvoked),
|
||||||
}
|
}
|
||||||
|
@ -81,7 +85,7 @@ impl BackupCommandExt for BackupCommand {
|
||||||
|
|
||||||
/// Backup commands which is not invoked from xdbm itself.
|
/// Backup commands which is not invoked from xdbm itself.
|
||||||
/// Call xdbm externally to record backup datetime and status.
|
/// Call xdbm externally to record backup datetime and status.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ExternallyInvoked {
|
pub struct ExternallyInvoked {
|
||||||
name: String,
|
name: String,
|
||||||
pub note: String,
|
pub note: String,
|
||||||
|
@ -104,7 +108,7 @@ impl BackupCommandExt for ExternallyInvoked {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backup execution log.
|
/// Backup execution log.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct BackupLog {
|
pub struct BackupLog {
|
||||||
pub datetime: DateTime<Local>,
|
pub datetime: DateTime<Local>,
|
||||||
status: BackupResult,
|
status: BackupResult,
|
||||||
|
@ -124,7 +128,7 @@ impl BackupLog {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of backup.
|
/// Result of backup.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum BackupResult {
|
pub enum BackupResult {
|
||||||
Success,
|
Success,
|
||||||
Failure,
|
Failure,
|
||||||
|
@ -141,7 +145,7 @@ impl BackupResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backup source, destination, command and logs.
|
/// Backup source, destination, command and logs.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Backup {
|
pub struct Backup {
|
||||||
/// must be unique
|
/// must be unique
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -176,7 +180,7 @@ impl Backup {
|
||||||
&self.name
|
&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)
|
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 struct Backups {
|
||||||
pub list: BTreeMap<String, Backup>,
|
pub list: BTreeMap<String, Backup>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,10 +49,10 @@ pub(crate) enum Commands {
|
||||||
/// Target path. Default is the current directory.
|
/// Target path. Default is the current directory.
|
||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
/// Show storage which the path belongs to.
|
/// Show storage which the path belongs to.
|
||||||
#[arg(short)]
|
#[arg(short, long)]
|
||||||
storage: bool,
|
storage: bool,
|
||||||
/// Show backup config covering the path.
|
/// Show backup config covering the path.
|
||||||
#[arg(short)]
|
#[arg(short, long)]
|
||||||
backup: bool,
|
backup: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Ok, Result};
|
use anyhow::{anyhow, Context, Ok, Result};
|
||||||
use chrono::{Local, TimeDelta};
|
use chrono::Local;
|
||||||
use console::Style;
|
use console::Style;
|
||||||
use dunce::canonicalize;
|
use dunce::canonicalize;
|
||||||
use git2::Repository;
|
use git2::Repository;
|
||||||
|
@ -89,8 +89,8 @@ fn new_backup(
|
||||||
Ok(Backup::new(
|
Ok(Backup::new(
|
||||||
name,
|
name,
|
||||||
device.name(),
|
device.name(),
|
||||||
src_target,
|
src_target?,
|
||||||
dest_target,
|
dest_target?,
|
||||||
command,
|
command,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -154,17 +154,6 @@ pub fn cmd_backup_list(
|
||||||
Ok(())
|
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
|
/// TODO: status printing
|
||||||
fn write_backups_list(
|
fn write_backups_list(
|
||||||
mut writer: impl io::Write,
|
mut writer: impl io::Write,
|
||||||
|
@ -222,7 +211,7 @@ fn write_backups_list(
|
||||||
Some(log) => {
|
Some(log) => {
|
||||||
let time = Local::now() - log.datetime;
|
let time = Local::now() - log.datetime;
|
||||||
let s = util::format_summarized_duration(time);
|
let s = util::format_summarized_duration(time);
|
||||||
let style = duration_style(time);
|
let style = util::duration_style(time);
|
||||||
(style.apply_to(s), style)
|
(style.apply_to(s), style)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -372,9 +361,9 @@ mod test {
|
||||||
&storages,
|
&storages,
|
||||||
)?;
|
)?;
|
||||||
assert!(backup.source().storage == "online");
|
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().storage == "online");
|
||||||
assert!(backup.destination().path == PathBuf::from("tmp"));
|
assert!(backup.destination().path == vec!["tmp"]);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::Local;
|
||||||
use console::Style;
|
use console::Style;
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
|
@ -20,11 +21,11 @@ pub(crate) fn cmd_status(
|
||||||
config_dir: &Path,
|
config_dir: &Path,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let path = path.unwrap_or(env::current_dir().context("Failed to get current directory.")?);
|
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 {
|
if show_storage {
|
||||||
let storages = storages::Storages::read(config_dir)?;
|
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);
|
trace!("storage {:?}", storage);
|
||||||
|
|
||||||
// TODO: recursively trace all storages for subdirectory?
|
// TODO: recursively trace all storages for subdirectory?
|
||||||
|
@ -40,21 +41,39 @@ pub(crate) fn cmd_status(
|
||||||
if show_backup {
|
if show_backup {
|
||||||
let devices = devices::get_devices(config_dir)?;
|
let devices = devices::get_devices(config_dir)?;
|
||||||
let storages = storages::Storages::read(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) =
|
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")?;
|
.context("Target path is not covered in any storage")?;
|
||||||
|
|
||||||
let covering_backup: Vec<_> = devices
|
let covering_backup: Vec<_> = devices
|
||||||
.iter()
|
.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::<Vec<_>>()
|
||||||
|
);
|
||||||
(
|
(
|
||||||
device,
|
device,
|
||||||
parent_backups(
|
parent_backups(
|
||||||
&target_diff_from_storage,
|
&target_diff_from_storage,
|
||||||
target_storage,
|
target_storage,
|
||||||
&backups,
|
backups,
|
||||||
&storages,
|
&storages,
|
||||||
device,
|
device,
|
||||||
),
|
),
|
||||||
|
@ -76,17 +95,33 @@ pub(crate) fn cmd_status(
|
||||||
.unwrap_or(5);
|
.unwrap_or(5);
|
||||||
|
|
||||||
for (backup_device, covering_backups) in covering_backup {
|
for (backup_device, covering_backups) in covering_backup {
|
||||||
|
if covering_backups.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
println!("Device: {}", backup_device.name());
|
println!("Device: {}", backup_device.name());
|
||||||
for (backup, path_from_backup) in covering_backups {
|
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!(
|
println!(
|
||||||
" {:<name_len$} {}",
|
" {:<name_len$} {} {}",
|
||||||
console::style(backup.name()).bold(),
|
console::style(backup.name()).bold(),
|
||||||
|
style.apply_to(last_backup),
|
||||||
path_from_backup.display(),
|
path_from_backup.display(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
todo!()
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`Backup`]s for `device` which covers `target_path`.
|
/// Get [`Backup`]s for `device` which covers `target_path`.
|
||||||
|
@ -94,10 +129,10 @@ pub(crate) fn cmd_status(
|
||||||
fn parent_backups<'a>(
|
fn parent_backups<'a>(
|
||||||
target_path_from_storage: &'a Path,
|
target_path_from_storage: &'a Path,
|
||||||
target_storage: &'a Storage,
|
target_storage: &'a Storage,
|
||||||
backups: &'a Backups,
|
backups: Backups,
|
||||||
storages: &'a Storages,
|
storages: &'a Storages,
|
||||||
device: &'a Device,
|
device: &'a Device,
|
||||||
) -> Vec<(&'a Backup, PathBuf)> {
|
) -> Vec<(Backup, PathBuf)> {
|
||||||
trace!("Dev {:?}", device.name());
|
trace!("Dev {:?}", device.name());
|
||||||
let target_path = match target_storage.mount_path(device) {
|
let target_path = match target_storage.mount_path(device) {
|
||||||
Some(target_path) => target_path.join(target_path_from_storage),
|
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);
|
trace!("Path on the device {:?}", target_path);
|
||||||
backups
|
backups
|
||||||
.list
|
.list
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter_map(|(_k, backup)| {
|
.filter_map(|(_k, backup)| {
|
||||||
let backup_path = backup.source().path(storages, device)?;
|
let backup_path = backup.source().path(storages, device)?;
|
||||||
let diff = pathdiff::diff_paths(&target_path, backup_path)?;
|
trace!("{:?}", backup_path.components());
|
||||||
if diff.components().any(|c| c == path::Component::ParentDir) {
|
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
|
None
|
||||||
} else {
|
} else {
|
||||||
Some((backup, diff))
|
Some((backup, diff))
|
||||||
|
@ -121,7 +162,7 @@ fn parent_backups<'a>(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, vec};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backups::{self, ExternallyInvoked},
|
backups::{self, ExternallyInvoked},
|
||||||
|
@ -176,11 +217,11 @@ mod test {
|
||||||
device1.name().to_string(),
|
device1.name().to_string(),
|
||||||
backups::BackupTarget {
|
backups::BackupTarget {
|
||||||
storage: "storage_1".to_string(),
|
storage: "storage_1".to_string(),
|
||||||
path: PathBuf::from("bar"),
|
path: vec!["bar".to_string()],
|
||||||
},
|
},
|
||||||
backups::BackupTarget {
|
backups::BackupTarget {
|
||||||
storage: "storage_1".to_string(),
|
storage: "storage_1".to_string(),
|
||||||
path: PathBuf::from("hoge"),
|
path: vec!["hoge".to_string()],
|
||||||
},
|
},
|
||||||
backups::BackupCommand::ExternallyInvoked(ExternallyInvoked::new(
|
backups::BackupCommand::ExternallyInvoked(ExternallyInvoked::new(
|
||||||
"cmd".to_string(),
|
"cmd".to_string(),
|
||||||
|
@ -192,11 +233,11 @@ mod test {
|
||||||
device2.name().to_string(),
|
device2.name().to_string(),
|
||||||
backups::BackupTarget {
|
backups::BackupTarget {
|
||||||
storage: "storage_1".to_string(),
|
storage: "storage_1".to_string(),
|
||||||
path: PathBuf::from(""),
|
path: vec!["".to_string()],
|
||||||
},
|
},
|
||||||
backups::BackupTarget {
|
backups::BackupTarget {
|
||||||
storage: "storage_3".to_string(),
|
storage: "storage_3".to_string(),
|
||||||
path: PathBuf::from("foo"),
|
path: vec!["foo".to_string()],
|
||||||
},
|
},
|
||||||
backups::BackupCommand::ExternallyInvoked(ExternallyInvoked::new(
|
backups::BackupCommand::ExternallyInvoked(ExternallyInvoked::new(
|
||||||
"cmd".to_string(),
|
"cmd".to_string(),
|
||||||
|
@ -218,7 +259,7 @@ mod test {
|
||||||
let covering_backups_1 = parent_backups(
|
let covering_backups_1 = parent_backups(
|
||||||
&target_path_from_storage1,
|
&target_path_from_storage1,
|
||||||
target_storage1,
|
target_storage1,
|
||||||
&backups,
|
backups.clone(),
|
||||||
&storages,
|
&storages,
|
||||||
&device1,
|
&device1,
|
||||||
);
|
);
|
||||||
|
@ -231,7 +272,7 @@ mod test {
|
||||||
let covering_backups_2 = parent_backups(
|
let covering_backups_2 = parent_backups(
|
||||||
&target_path_from_storage2,
|
&target_path_from_storage2,
|
||||||
target_storage2,
|
target_storage2,
|
||||||
&backups,
|
backups.clone(),
|
||||||
&storages,
|
&storages,
|
||||||
&device2,
|
&device2,
|
||||||
);
|
);
|
||||||
|
@ -244,12 +285,13 @@ mod test {
|
||||||
let covering_backups_3 = parent_backups(
|
let covering_backups_3 = parent_backups(
|
||||||
&target_path_from_storage3,
|
&target_path_from_storage3,
|
||||||
target_storage3,
|
target_storage3,
|
||||||
&backups,
|
backups,
|
||||||
&storages,
|
&storages,
|
||||||
&device2,
|
&device2,
|
||||||
);
|
);
|
||||||
assert_eq!(covering_backups_3.len(), 1);
|
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_eq!(covering_backup_names_3.next().unwrap(), "backup_2");
|
||||||
assert!(covering_backup_names_3.next().is_none());
|
assert!(covering_backup_names_3.next().is_none());
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::{collections::BTreeMap, fmt, path};
|
use std::{collections::BTreeMap, fmt, path};
|
||||||
|
|
||||||
use crate::devices;
|
use crate::devices;
|
||||||
|
@ -17,7 +18,7 @@ pub struct Directory {
|
||||||
/// ID of parent storage.
|
/// ID of parent storage.
|
||||||
parent: String,
|
parent: String,
|
||||||
/// Relative path to the parent storage.
|
/// Relative path to the parent storage.
|
||||||
relative_path: path::PathBuf,
|
relative_path: Vec<String>,
|
||||||
pub notes: String,
|
pub notes: String,
|
||||||
/// [`devices::Device`] name and localinfo pairs.
|
/// [`devices::Device`] name and localinfo pairs.
|
||||||
local_infos: BTreeMap<String, LocalInfo>,
|
local_infos: BTreeMap<String, LocalInfo>,
|
||||||
|
@ -34,14 +35,19 @@ impl Directory {
|
||||||
relative_path: path::PathBuf,
|
relative_path: path::PathBuf,
|
||||||
notes: String,
|
notes: String,
|
||||||
local_infos: BTreeMap<String, LocalInfo>,
|
local_infos: BTreeMap<String, LocalInfo>,
|
||||||
) -> Directory {
|
) -> Result<Directory> {
|
||||||
Directory {
|
let relative_path = relative_path
|
||||||
|
.components()
|
||||||
|
.map(|c| c.as_os_str().to_str().map(|s| s.to_owned()))
|
||||||
|
.collect::<Option<Vec<_>>>()
|
||||||
|
.context("Path contains non-utf8 character")?;
|
||||||
|
Ok(Directory {
|
||||||
name,
|
name,
|
||||||
parent,
|
parent,
|
||||||
relative_path,
|
relative_path,
|
||||||
notes,
|
notes,
|
||||||
local_infos,
|
local_infos,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_from_device_path(
|
pub fn try_from_device_path(
|
||||||
|
@ -56,23 +62,23 @@ impl Directory {
|
||||||
.context("Failed to compare diff of paths")?;
|
.context("Failed to compare diff of paths")?;
|
||||||
trace!("Selected parent: {}", parent.name());
|
trace!("Selected parent: {}", parent.name());
|
||||||
let local_info = LocalInfo::new(alias, path);
|
let local_info = LocalInfo::new(alias, path);
|
||||||
Ok(Directory::new(
|
Directory::new(
|
||||||
name,
|
name,
|
||||||
parent.name().to_string(),
|
parent.name().to_string(),
|
||||||
diff_path,
|
diff_path,
|
||||||
notes,
|
notes,
|
||||||
BTreeMap::from([(device.name(), local_info)]),
|
BTreeMap::from([(device.name(), local_info)]),
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_note(self, notes: String) -> Directory {
|
pub fn update_note(self, notes: String) -> Directory {
|
||||||
Directory::new(
|
Directory {
|
||||||
self.name,
|
name: self.name,
|
||||||
self.parent,
|
parent: self.parent,
|
||||||
self.relative_path,
|
relative_path: self.relative_path,
|
||||||
notes,
|
notes,
|
||||||
self.local_infos,
|
local_infos: self.local_infos,
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve mount path of directory with current device.
|
/// Resolve mount path of directory with current device.
|
||||||
|
@ -82,7 +88,7 @@ impl Directory {
|
||||||
.context("Can't find parent storage")?
|
.context("Can't find parent storage")?
|
||||||
.mount_path(device)
|
.mount_path(device)
|
||||||
.context("Can't find mount path")?;
|
.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::<PathBuf>()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +127,7 @@ impl StorageExt for Directory {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get parent `&Storage` of 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)
|
storages.get(&self.parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +139,7 @@ impl fmt::Display for Directory {
|
||||||
"S {name:<10} < {parent:<10}{relative_path:<10} : {notes}",
|
"S {name:<10} < {parent:<10}{relative_path:<10} : {notes}",
|
||||||
name = self.name(),
|
name = self.name(),
|
||||||
parent = self.parent,
|
parent = self.parent,
|
||||||
relative_path = self.relative_path.display(),
|
relative_path = self.relative_path.iter().collect::<PathBuf>().display(),
|
||||||
notes = self.notes,
|
notes = self.notes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -177,7 +183,8 @@ mod test {
|
||||||
"subdir".into(),
|
"subdir".into(),
|
||||||
"some note".to_string(),
|
"some note".to_string(),
|
||||||
local_infos,
|
local_infos,
|
||||||
);
|
)
|
||||||
|
.unwrap();
|
||||||
let mut storages = Storages::new();
|
let mut storages = Storages::new();
|
||||||
storages.add(storages::Storage::Physical(physical)).unwrap();
|
storages.add(storages::Storage::Physical(physical)).unwrap();
|
||||||
storages.add(Storage::SubDirectory(directory)).unwrap();
|
storages.add(Storage::SubDirectory(directory)).unwrap();
|
||||||
|
|
17
src/util.rs
17
src/util.rs
|
@ -1,6 +1,7 @@
|
||||||
use std::path::{self, PathBuf};
|
use std::path::{self, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::TimeDelta;
|
||||||
use console::Style;
|
use console::Style;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -20,7 +21,10 @@ pub fn min_parent_storage<'a>(
|
||||||
.filter_map(|(k, storage)| {
|
.filter_map(|(k, storage)| {
|
||||||
let storage_path = storage.mount_path(device)?;
|
let storage_path = storage.mount_path(device)?;
|
||||||
let diff = pathdiff::diff_paths(path, storage_path)?;
|
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
|
None
|
||||||
} else {
|
} else {
|
||||||
Some((k, diff))
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
333
tests/cli.rs
333
tests/cli.rs
|
@ -145,7 +145,7 @@ mod integrated_test {
|
||||||
|
|
||||||
// bare-repo
|
// bare-repo
|
||||||
let bare_repo_dir = assert_fs::TempDir::new()?;
|
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
|
// push to bare repository
|
||||||
let repo_1 = Repository::open(&config_dir_1)?;
|
let repo_1 = Repository::open(&config_dir_1)?;
|
||||||
let upstream_name = "remote";
|
let upstream_name = "remote";
|
||||||
|
@ -216,6 +216,8 @@ mod integrated_test {
|
||||||
#[test]
|
#[test]
|
||||||
fn two_devices() -> Result<()> {
|
fn two_devices() -> Result<()> {
|
||||||
// 1st device
|
// 1st device
|
||||||
|
//
|
||||||
|
// devices: first
|
||||||
let config_dir_1 = assert_fs::TempDir::new()?;
|
let config_dir_1 = assert_fs::TempDir::new()?;
|
||||||
setup_gitconfig()?;
|
setup_gitconfig()?;
|
||||||
let mut cmd1 = Command::cargo_bin("xdbm")?;
|
let mut cmd1 = Command::cargo_bin("xdbm")?;
|
||||||
|
@ -227,7 +229,7 @@ mod integrated_test {
|
||||||
|
|
||||||
// bare-repo
|
// bare-repo
|
||||||
let bare_repo_dir = assert_fs::TempDir::new()?;
|
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
|
// push to bare repository
|
||||||
let repo_1 = Repository::open(&config_dir_1)?;
|
let repo_1 = Repository::open(&config_dir_1)?;
|
||||||
let upstream_name = "remote";
|
let upstream_name = "remote";
|
||||||
|
@ -248,6 +250,8 @@ mod integrated_test {
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
// 2nd device
|
// 2nd device
|
||||||
|
//
|
||||||
|
// devices: first, second
|
||||||
let config_dir_2 = assert_fs::TempDir::new()?;
|
let config_dir_2 = assert_fs::TempDir::new()?;
|
||||||
let mut cmd2 = Command::cargo_bin("xdbm")?;
|
let mut cmd2 = Command::cargo_bin("xdbm")?;
|
||||||
cmd2.arg("-c")
|
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("first.yml").exists());
|
||||||
assert!(config_dir_2.join("backups").join("second.yml").exists());
|
assert!(config_dir_2.join("backups").join("second.yml").exists());
|
||||||
|
|
||||||
|
// sync
|
||||||
std::process::Command::new("git")
|
std::process::Command::new("git")
|
||||||
.arg("push")
|
.arg("push")
|
||||||
.current_dir(&config_dir_2)
|
.current_dir(&config_dir_2)
|
||||||
|
@ -287,6 +292,11 @@ mod integrated_test {
|
||||||
.success();
|
.success();
|
||||||
|
|
||||||
// Add storage
|
// Add storage
|
||||||
|
//
|
||||||
|
// devices: first, second
|
||||||
|
// storages:
|
||||||
|
// - gdrive @ sample_storage (online)
|
||||||
|
// - first: sample_storage
|
||||||
let sample_storage = assert_fs::TempDir::new()?;
|
let sample_storage = assert_fs::TempDir::new()?;
|
||||||
let mut cmd_add_storage_1 = Command::cargo_bin("xdbm")?;
|
let mut cmd_add_storage_1 = Command::cargo_bin("xdbm")?;
|
||||||
cmd_add_storage_1
|
cmd_add_storage_1
|
||||||
|
@ -308,6 +318,13 @@ mod integrated_test {
|
||||||
.success()
|
.success()
|
||||||
.stdout(predicate::str::contains(""));
|
.stdout(predicate::str::contains(""));
|
||||||
// Add storage (directory)
|
// 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");
|
let sample_directory = &sample_storage.join("foo").join("bar");
|
||||||
DirBuilder::new().recursive(true).create(sample_directory)?;
|
DirBuilder::new().recursive(true).create(sample_directory)?;
|
||||||
Command::cargo_bin("xdbm")?
|
Command::cargo_bin("xdbm")?
|
||||||
|
@ -339,6 +356,14 @@ mod integrated_test {
|
||||||
.success();
|
.success();
|
||||||
|
|
||||||
// bind
|
// 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")?
|
Command::cargo_bin("xdbm")?
|
||||||
.arg("-c")
|
.arg("-c")
|
||||||
.arg(config_dir_2.path())
|
.arg(config_dir_2.path())
|
||||||
|
@ -354,6 +379,16 @@ mod integrated_test {
|
||||||
.stdout(predicate::str::contains(""));
|
.stdout(predicate::str::contains(""));
|
||||||
|
|
||||||
// storage 3
|
// 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()?;
|
let sample_storage_2 = assert_fs::TempDir::new()?;
|
||||||
Command::cargo_bin("xdbm")?
|
Command::cargo_bin("xdbm")?
|
||||||
.arg("-c")
|
.arg("-c")
|
||||||
|
@ -384,6 +419,19 @@ mod integrated_test {
|
||||||
.stdout(predicate::str::contains("gdrive_docs").and(predicate::str::contains("nas")));
|
.stdout(predicate::str::contains("gdrive_docs").and(predicate::str::contains("nas")));
|
||||||
|
|
||||||
// backup add
|
// 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");
|
let backup_src = &sample_storage_2.join("foo").join("bar");
|
||||||
DirBuilder::new().recursive(true).create(backup_src)?;
|
DirBuilder::new().recursive(true).create(backup_src)?;
|
||||||
let backup_dest = &sample_directory.join("docs");
|
let backup_dest = &sample_directory.join("docs");
|
||||||
|
@ -438,6 +486,19 @@ mod integrated_test {
|
||||||
);
|
);
|
||||||
|
|
||||||
// backup done
|
// 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")?
|
Command::cargo_bin("xdbm")?
|
||||||
.arg("-c")
|
.arg("-c")
|
||||||
.arg(config_dir_2.path())
|
.arg(config_dir_2.path())
|
||||||
|
@ -448,6 +509,274 @@ mod integrated_test {
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue