mirror of
https://github.com/qwjyh/xdbm
synced 2024-12-04 20:41:04 +09:00
Compare commits
31 commits
1f7b992010
...
51e90b28cd
Author | SHA1 | Date | |
---|---|---|---|
51e90b28cd | |||
|
aed0049f98 | ||
4e8387e3b0 | |||
07ef49ca7b | |||
3d8aa7bca0 | |||
996ca7e4b3 | |||
24f1356685 | |||
7e043a652c | |||
af1b56eee2 | |||
c4aa76d125 | |||
697c9c0712 | |||
bce9f3710c | |||
315a75424a | |||
d743e607ac | |||
7592ec0ad0 | |||
|
8fc8029435 | ||
6a0abd03d5 | |||
ced354bf58 | |||
dbc0d78f99 | |||
8b0dbb2314 | |||
772689ab6a | |||
bc3939c9bc | |||
a409a43906 | |||
37782c934c | |||
0abf9c0693 | |||
6e1619aa18 | |||
b9bb207f35 | |||
3b7e2387bd | |||
|
ea0acf177c | ||
a7c81d5976 | |||
|
bdd2d32901 |
17 changed files with 1519 additions and 409 deletions
8
.github/workflows/rust.yml
vendored
8
.github/workflows/rust.yml
vendored
|
@ -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
|
||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -2,6 +2,13 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### 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<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
|
||||
|
||||
### Changed
|
||||
- Dependencies are updated.
|
||||
- Format of storage size printing has been changed due to the update of byte-unit.
|
||||
|
@ -30,6 +37,7 @@
|
|||
- `backup done` subcommand
|
||||
- `completion` subcommand
|
||||
|
||||
[Unreleased]: https://github.com/qwjyh/xdbm/compare/v0.2.0...HEAD
|
||||
[Unreleased]: https://github.com/qwjyh/xdbm/compare/v0.2.1...HEAD
|
||||
[0.2.1]: https://github.com/qwjyh/xdbm/compare/v0.2.0...v0.2.1
|
||||
[0.2.0]: https://github.com/qwjyh/xdbm/releases/tag/v0.2.0
|
||||
[0.1.0]: https://github.com/qwjyh/xdbm/releases/tag/v0.1.0
|
||||
|
|
907
Cargo.lock
generated
907
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
25
Cargo.toml
25
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "xdbm"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
authors = ["qwjyh <urataw421@gmail.com>"]
|
||||
edition = "2021"
|
||||
description = "Cross device backup manager, which manages backups on several storages mounted on multiple devices."
|
||||
|
@ -13,25 +13,26 @@ keywords = ["cli", "backup"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.4.0", features = ["cargo", "derive"] }
|
||||
sysinfo = { version = "0.30", features = ["serde"] }
|
||||
clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||
sysinfo = { version = "0.32", features = ["serde"] }
|
||||
log = "0.4"
|
||||
clap-verbosity-flag = "2.2"
|
||||
clap-verbosity-flag = "3.0"
|
||||
clap_complete = "4.5"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
env_logger = "0.11.3"
|
||||
env_logger = "0.11.5"
|
||||
inquire = "0.7.5"
|
||||
git2 = "0.19"
|
||||
dirs = "5.0"
|
||||
dunce = "1.0.4"
|
||||
dunce = "1.0.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_yaml = "0.9"
|
||||
byte-unit = "5.1.4"
|
||||
byte-unit = "5.1"
|
||||
anyhow = "1.0"
|
||||
pathdiff = "0.2.1"
|
||||
unicode-width = "0.1.13"
|
||||
pathdiff = "0.2.3"
|
||||
unicode-width = "0.2.0"
|
||||
console = "0.15"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.14"
|
||||
assert_fs = "1.1.1"
|
||||
predicates = "3.1.0"
|
||||
assert_cmd = "2.0.16"
|
||||
assert_fs = "1.1.2"
|
||||
predicates = "3.1.2"
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
_Cross device backup manager_,
|
||||
which manages backups on several storages mounted on multiple devices with a single repository.
|
||||
|
||||
## Install
|
||||
- `git` is required for sync
|
||||
|
||||
## Usage
|
||||
1. `xdbm init` to setup new device(i.e. PC).
|
||||
2. `xdbm storage add` to add storages, or `xdbm storage bind` to make existing storages available on new device.
|
||||
|
@ -24,7 +27,7 @@ which manages backups on several storages mounted on multiple devices with a sin
|
|||
- [ ] write test for storage subcommand
|
||||
- [x] storage add online
|
||||
- [x] storage add directory
|
||||
- [ ] storage list
|
||||
- [x] storage list
|
||||
- [x] update storage bind command
|
||||
- [ ] add storage remove command
|
||||
- [ ] add sync subcommand
|
||||
|
@ -38,7 +41,7 @@ which manages backups on several storages mounted on multiple devices with a sin
|
|||
- [x] backup list
|
||||
- [x] status printing
|
||||
- [x] backup done
|
||||
- [ ] fancy display
|
||||
- [x] fancy display
|
||||
- [ ] json output
|
||||
- [ ] no commit option
|
||||
|
||||
|
|
|
@ -27,32 +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<String>,
|
||||
}
|
||||
|
||||
impl BackupTarget {
|
||||
pub fn new(storage_name: String, relative_path: PathBuf) -> Self {
|
||||
BackupTarget {
|
||||
pub fn new(storage_name: String, relative_path: PathBuf) -> Result<Self> {
|
||||
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,
|
||||
path: relative_path,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path(&self, storages: &Storages, device: &Device) -> Result<PathBuf> {
|
||||
/// Get full path of the [`BackupTarget`].
|
||||
pub fn path(&self, storages: &Storages, device: &Device) -> Option<PathBuf> {
|
||||
let parent = storages.get(&self.storage).unwrap();
|
||||
let parent_path = parent.mount_path(device)?;
|
||||
Ok(parent_path.join(self.path.clone()))
|
||||
Some(parent_path.join(self.path.clone().iter().collect::<PathBuf>()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of backup commands.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum BackupCommand {
|
||||
ExternallyInvoked(ExternallyInvoked),
|
||||
}
|
||||
|
@ -79,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,
|
||||
|
@ -102,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<Local>,
|
||||
status: BackupResult,
|
||||
|
@ -122,7 +128,7 @@ impl BackupLog {
|
|||
}
|
||||
|
||||
/// Result of backup.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum BackupResult {
|
||||
Success,
|
||||
Failure,
|
||||
|
@ -139,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,
|
||||
|
@ -174,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)
|
||||
}
|
||||
|
||||
|
@ -200,7 +206,7 @@ impl Backup {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Backups {
|
||||
pub list: BTreeMap<String, Backup>,
|
||||
}
|
||||
|
|
|
@ -44,6 +44,18 @@ pub(crate) enum Commands {
|
|||
#[command(subcommand)]
|
||||
Backup(BackupSubCommands),
|
||||
|
||||
/// Print status for the given path.
|
||||
Status {
|
||||
/// Target path. Default is the current directory.
|
||||
path: Option<PathBuf>,
|
||||
/// Show storage which the path belongs to.
|
||||
#[arg(short, long)]
|
||||
storage: bool,
|
||||
/// Show backup config covering the path.
|
||||
#[arg(short, long)]
|
||||
backup: bool,
|
||||
},
|
||||
|
||||
/// Print config dir.
|
||||
Path {},
|
||||
|
||||
|
@ -57,9 +69,7 @@ pub(crate) enum Commands {
|
|||
Check {},
|
||||
|
||||
/// Generate completion script.
|
||||
Completion {
|
||||
shell: clap_complete::Shell,
|
||||
}
|
||||
Completion { shell: clap_complete::Shell },
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
|
|
|
@ -6,6 +6,7 @@ use std::{
|
|||
|
||||
use anyhow::{anyhow, Context, Ok, Result};
|
||||
use chrono::Local;
|
||||
use console::Style;
|
||||
use dunce::canonicalize;
|
||||
use git2::Repository;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
@ -88,8 +89,8 @@ fn new_backup(
|
|||
Ok(Backup::new(
|
||||
name,
|
||||
device.name(),
|
||||
src_target,
|
||||
dest_target,
|
||||
src_target?,
|
||||
dest_target?,
|
||||
command,
|
||||
))
|
||||
}
|
||||
|
@ -176,10 +177,16 @@ fn write_backups_list(
|
|||
))?;
|
||||
name_width = name_width.max(backup.name().width());
|
||||
dev_width = dev_width.max(dev.width());
|
||||
let src = backup.source().path(storages, device)?;
|
||||
let src = backup
|
||||
.source()
|
||||
.path(storages, device)
|
||||
.context("Couldn't get path for source")?;
|
||||
src_width = src_width.max(format!("{}", src.display()).width());
|
||||
src_storage_width = src_storage_width.max(backup.source().storage.width());
|
||||
let dest = backup.destination().path(storages, device)?;
|
||||
let dest = backup
|
||||
.destination()
|
||||
.path(storages, device)
|
||||
.context("Couldn't get path for destination")?;
|
||||
dest_width = dest_width.max(format!("{}", dest.display()).width());
|
||||
dest_storage_width = dest_storage_width.max(backup.destination().storage.width());
|
||||
let cmd_name = backup.command().name();
|
||||
|
@ -188,39 +195,77 @@ fn write_backups_list(
|
|||
// main printing
|
||||
for ((dev, _name), backup) in &backups {
|
||||
let device = backup.device(devices).context(format!(
|
||||
"Couldn't find device specified in backup config {}",
|
||||
"Couldn't find the device specified in the backup config: {}",
|
||||
backup.name()
|
||||
))?;
|
||||
let src = backup.source().path(storages, device)?;
|
||||
let dest = backup.destination().path(storages, device)?;
|
||||
let src = backup
|
||||
.source()
|
||||
.path(storages, device)
|
||||
.context("Couldn't get path for source")?;
|
||||
let dest = backup
|
||||
.destination()
|
||||
.path(storages, device)
|
||||
.context("Couldn't get path for destination")?;
|
||||
let cmd_name = backup.command().name();
|
||||
let last_backup_elapsed = match backup.last_backup() {
|
||||
let (last_backup_elapsed, style_on_time_elapsed) = match backup.last_backup() {
|
||||
Some(log) => {
|
||||
let time = Local::now() - log.datetime;
|
||||
util::format_summarized_duration(time)
|
||||
let s = util::format_summarized_duration(time);
|
||||
let style = util::duration_style(time);
|
||||
(style.apply_to(s), style)
|
||||
}
|
||||
None => {
|
||||
let style = Style::new().red();
|
||||
(style.apply_to("---".to_string()), style)
|
||||
}
|
||||
None => "---".to_string(),
|
||||
};
|
||||
writeln!(
|
||||
writer,
|
||||
"{name:<name_width$} [{dev:<dev_width$}] {src:<src_storage_width$} → {dest:<dest_storage_width$} {last_backup_elapsed}",
|
||||
name = backup.name(),
|
||||
src = backup.source().storage,
|
||||
dest = backup.destination().storage,
|
||||
)?;
|
||||
if longprint {
|
||||
let cmd_note = backup.command().note();
|
||||
writeln!(writer, " src : {src:<src_width$}", src = src.display())?;
|
||||
if !longprint {
|
||||
writeln!(
|
||||
writer,
|
||||
" dest: {dest:<dest_width$}",
|
||||
"{name:<name_width$} [{dev:<dev_width$}] {src:<src_storage_width$} → {dest:<dest_storage_width$} {last_backup_elapsed}",
|
||||
name = style_on_time_elapsed.apply_to(backup.name()),
|
||||
dev = console::style(dev).blue(),
|
||||
src = backup.source().storage,
|
||||
dest = backup.destination().storage,
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
writer,
|
||||
"[{dev:<dev_width$}] {name:<name_width$} {last_backup_elapsed}",
|
||||
dev = console::style(dev).blue(),
|
||||
name = style_on_time_elapsed.bold().apply_to(backup.name()),
|
||||
)?;
|
||||
let last_backup_date = match backup.last_backup() {
|
||||
Some(date) => date.datetime.format("%Y-%m-%d %T").to_string(),
|
||||
None => "never".to_string(),
|
||||
};
|
||||
let cmd_note = backup.command().note();
|
||||
writeln!(
|
||||
writer,
|
||||
"{s_src} {src}",
|
||||
s_src = console::style("src :").italic().bright().black(),
|
||||
src = src.display()
|
||||
)?;
|
||||
writeln!(
|
||||
writer,
|
||||
"{s_dest} {dest}",
|
||||
s_dest = console::style("dest:").italic().bright().black(),
|
||||
dest = dest.display()
|
||||
)?;
|
||||
writeln!(
|
||||
writer,
|
||||
" {cmd_name:<cmd_name_width$}({note})",
|
||||
note = cmd_note,
|
||||
"{s_last} {last}",
|
||||
s_last = console::style("last:").italic().bright().black(),
|
||||
last = last_backup_date,
|
||||
)?;
|
||||
writeln!(
|
||||
writer,
|
||||
"{s_cmd} {cmd_name}({note})",
|
||||
s_cmd = console::style("cmd :").italic().bright().black(),
|
||||
cmd_name = console::style(cmd_name).underlined(),
|
||||
note = console::style(cmd_note).italic(),
|
||||
)?;
|
||||
writeln!(writer)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -316,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(())
|
||||
}
|
||||
}
|
||||
|
|
298
src/cmd_status.rs
Normal file
298
src/cmd_status.rs
Normal file
|
@ -0,0 +1,298 @@
|
|||
use anyhow::{Context, Result};
|
||||
use chrono::Local;
|
||||
use console::Style;
|
||||
use std::{
|
||||
env,
|
||||
path::{self, Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backups::{Backup, Backups},
|
||||
devices::{self, Device},
|
||||
storages::{self, Storage, StorageExt, Storages},
|
||||
util,
|
||||
};
|
||||
|
||||
// TODO: fine styling like `backup list`, or should I just use the same style?
|
||||
pub(crate) fn cmd_status(
|
||||
path: Option<PathBuf>,
|
||||
show_storage: bool,
|
||||
show_backup: bool,
|
||||
config_dir: &Path,
|
||||
) -> Result<()> {
|
||||
let path = path.unwrap_or(env::current_dir().context("Failed to get current directory.")?);
|
||||
let 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, ¤t_device);
|
||||
trace!("storage {:?}", storage);
|
||||
|
||||
// TODO: recursively trace all storages for subdirectory?
|
||||
match storage {
|
||||
Some(storage) => {
|
||||
println!("Storage: {}", storage.0.name())
|
||||
}
|
||||
None => {
|
||||
println!("Storage: None");
|
||||
}
|
||||
}
|
||||
}
|
||||
if show_backup {
|
||||
let devices = devices::get_devices(config_dir)?;
|
||||
let storages = storages::Storages::read(config_dir)?;
|
||||
let backups = 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, ¤t_device)
|
||||
.context("Target path is not covered in any storage")?;
|
||||
|
||||
let covering_backup: Vec<_> = devices
|
||||
.iter()
|
||||
.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,
|
||||
parent_backups(
|
||||
&target_diff_from_storage,
|
||||
target_storage,
|
||||
backups,
|
||||
&storages,
|
||||
device,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
trace!("{:?}", covering_backup.first());
|
||||
|
||||
let name_len = &covering_backup
|
||||
.iter()
|
||||
.map(|(_, backups)| {
|
||||
backups
|
||||
.iter()
|
||||
.map(|(backup, _path)| backup.name().len())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(5);
|
||||
|
||||
for (backup_device, covering_backups) in covering_backup {
|
||||
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!(
|
||||
" {:<name_len$} {} {}",
|
||||
console::style(backup.name()).bold(),
|
||||
style.apply_to(last_backup),
|
||||
path_from_backup.display(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get [`Backup`]s for `device` which covers `target_path`.
|
||||
/// Returns [`Vec`] of tuple of [`Backup`] and relative path from the backup root.
|
||||
fn parent_backups<'a>(
|
||||
target_path_from_storage: &'a Path,
|
||||
target_storage: &'a Storage,
|
||||
backups: Backups,
|
||||
storages: &'a Storages,
|
||||
device: &'a Device,
|
||||
) -> 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),
|
||||
None => return vec![],
|
||||
};
|
||||
trace!("Path on the device {:?}", target_path);
|
||||
backups
|
||||
.list
|
||||
.into_iter()
|
||||
.filter_map(|(_k, backup)| {
|
||||
let backup_path = backup.source().path(storages, device)?;
|
||||
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))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{path::PathBuf, vec};
|
||||
|
||||
use crate::{
|
||||
backups::{self, ExternallyInvoked},
|
||||
devices,
|
||||
storages::{self, online_storage::OnlineStorage, StorageExt},
|
||||
util,
|
||||
};
|
||||
|
||||
use super::parent_backups;
|
||||
|
||||
#[test]
|
||||
fn test_parent_backups() {
|
||||
let device1 = devices::Device::new("device_1".to_string());
|
||||
let mut storage1 = storages::Storage::Online(OnlineStorage::new(
|
||||
"storage_1".to_string(),
|
||||
"smb".to_string(),
|
||||
1_000_000,
|
||||
"str1".to_string(),
|
||||
PathBuf::from("/home/foo/"),
|
||||
&device1,
|
||||
));
|
||||
let storage2 = storages::Storage::Online(OnlineStorage::new(
|
||||
"storage_2".to_string(),
|
||||
"smb".to_string(),
|
||||
1_000_000_000,
|
||||
"str2".to_string(),
|
||||
PathBuf::from("/"),
|
||||
&device1,
|
||||
));
|
||||
let device2 = devices::Device::new("device_2".to_string());
|
||||
storage1
|
||||
.bound_on_device("alias".to_string(), PathBuf::from("/mnt/dev"), &device2)
|
||||
.unwrap();
|
||||
let storage3 = storages::Storage::Online(OnlineStorage::new(
|
||||
"storage_3".to_string(),
|
||||
"smb".to_string(),
|
||||
2_000_000_000,
|
||||
"str2".to_string(),
|
||||
PathBuf::from("/"),
|
||||
&device2,
|
||||
));
|
||||
let storages = {
|
||||
let mut storages = storages::Storages::new();
|
||||
storages.add(storage1).unwrap();
|
||||
storages.add(storage2).unwrap();
|
||||
storages.add(storage3).unwrap();
|
||||
storages
|
||||
};
|
||||
|
||||
let backup1 = backups::Backup::new(
|
||||
"backup_1".to_string(),
|
||||
device1.name().to_string(),
|
||||
backups::BackupTarget {
|
||||
storage: "storage_1".to_string(),
|
||||
path: vec!["bar".to_string()],
|
||||
},
|
||||
backups::BackupTarget {
|
||||
storage: "storage_1".to_string(),
|
||||
path: vec!["hoge".to_string()],
|
||||
},
|
||||
backups::BackupCommand::ExternallyInvoked(ExternallyInvoked::new(
|
||||
"cmd".to_string(),
|
||||
"".to_string(),
|
||||
)),
|
||||
);
|
||||
let backup2 = backups::Backup::new(
|
||||
"backup_2".to_string(),
|
||||
device2.name().to_string(),
|
||||
backups::BackupTarget {
|
||||
storage: "storage_1".to_string(),
|
||||
path: vec!["".to_string()],
|
||||
},
|
||||
backups::BackupTarget {
|
||||
storage: "storage_3".to_string(),
|
||||
path: vec!["foo".to_string()],
|
||||
},
|
||||
backups::BackupCommand::ExternallyInvoked(ExternallyInvoked::new(
|
||||
"cmd".to_string(),
|
||||
"".to_string(),
|
||||
)),
|
||||
);
|
||||
|
||||
let backups = {
|
||||
let mut backups = backups::Backups::new();
|
||||
backups.add(backup1).unwrap();
|
||||
backups.add(backup2).unwrap();
|
||||
backups
|
||||
};
|
||||
|
||||
let target_path1 = PathBuf::from("/home/foo/bar/hoo");
|
||||
let (target_storage1, target_path_from_storage1) =
|
||||
util::min_parent_storage(&target_path1, &storages, &device1)
|
||||
.expect("Failed to get storage");
|
||||
let covering_backups_1 = parent_backups(
|
||||
&target_path_from_storage1,
|
||||
target_storage1,
|
||||
backups.clone(),
|
||||
&storages,
|
||||
&device1,
|
||||
);
|
||||
assert_eq!(covering_backups_1.len(), 2);
|
||||
|
||||
let target_path2 = PathBuf::from("/mnt/");
|
||||
let (target_storage2, target_path_from_storage2) =
|
||||
util::min_parent_storage(&target_path2, &storages, &device2)
|
||||
.expect("Failed to get storage");
|
||||
let covering_backups_2 = parent_backups(
|
||||
&target_path_from_storage2,
|
||||
target_storage2,
|
||||
backups.clone(),
|
||||
&storages,
|
||||
&device2,
|
||||
);
|
||||
assert_eq!(covering_backups_2.len(), 0);
|
||||
|
||||
let target_path3 = PathBuf::from("/mnt/dev/foo");
|
||||
let (target_storage3, target_path_from_storage3) =
|
||||
util::min_parent_storage(&target_path3, &storages, &device2)
|
||||
.expect("Failed to get storage");
|
||||
let covering_backups_3 = parent_backups(
|
||||
&target_path_from_storage3,
|
||||
target_storage3,
|
||||
backups,
|
||||
&storages,
|
||||
&device2,
|
||||
);
|
||||
assert_eq!(covering_backups_3.len(), 1);
|
||||
let mut covering_backup_names_3 =
|
||||
covering_backups_3.iter().map(|(backup, _)| backup.name());
|
||||
assert_eq!(covering_backup_names_3.next().unwrap(), "backup_2");
|
||||
assert!(covering_backup_names_3.next().is_none());
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ use std::{
|
|||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use byte_unit::{Byte, UnitType};
|
||||
use console::style;
|
||||
use dunce::canonicalize;
|
||||
use git2::Repository;
|
||||
use inquire::{Confirm, CustomType, Text};
|
||||
|
@ -211,11 +212,11 @@ fn write_storages_list(
|
|||
"-"
|
||||
}
|
||||
} else {
|
||||
" "
|
||||
""
|
||||
};
|
||||
let path = storage.mount_path(device).map_or_else(
|
||||
|e| {
|
||||
info!("Not found: {}", e);
|
||||
|| {
|
||||
info!("Mount path not found");
|
||||
"".to_string()
|
||||
},
|
||||
|v| v.display().to_string(),
|
||||
|
@ -227,23 +228,24 @@ fn write_storages_list(
|
|||
} else {
|
||||
""
|
||||
};
|
||||
let typestyle = storage.typestyle();
|
||||
writeln!(
|
||||
writer,
|
||||
"{stype}{isremovable}: {name:<name_width$} {size:>10} {parent:<name_width$} {path}",
|
||||
stype = storage.shorttypename(),
|
||||
"{stype}{isremovable:<1}: {name:<name_width$} {size:>10} {parent:<name_width$} {path}",
|
||||
stype = typestyle.apply_to(storage.shorttypename()),
|
||||
isremovable = isremovable,
|
||||
name = storage.name(),
|
||||
name = typestyle.apply_to(storage.name()),
|
||||
size = size_str,
|
||||
parent = parent_name,
|
||||
parent = console::style(parent_name).bright().black(),
|
||||
path = path,
|
||||
)?;
|
||||
if long_display {
|
||||
let note = match storage {
|
||||
Storage::Physical(s) => s.kind(),
|
||||
Storage::SubDirectory(s) => &s.notes,
|
||||
Storage::Online(s) => &s.provider,
|
||||
Storage::Physical(s) => format!("kind: {}", s.kind()),
|
||||
Storage::SubDirectory(s) => s.notes.clone(),
|
||||
Storage::Online(s) => s.provider.clone(),
|
||||
};
|
||||
writeln!(writer, " {}", note)?;
|
||||
writeln!(writer, " {}", style(note).italic())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -31,6 +31,7 @@ mod cmd_backup;
|
|||
mod cmd_check;
|
||||
mod cmd_completion;
|
||||
mod cmd_init;
|
||||
mod cmd_status;
|
||||
mod cmd_storage;
|
||||
mod cmd_sync;
|
||||
mod devices;
|
||||
|
@ -91,6 +92,11 @@ fn main() -> Result<()> {
|
|||
println!("{}", &config_dir.display());
|
||||
}
|
||||
Commands::Sync { remote_name } => cmd_sync::cmd_sync(&config_dir, remote_name)?,
|
||||
Commands::Status {
|
||||
path,
|
||||
storage,
|
||||
backup,
|
||||
} => cmd_status::cmd_status(path, storage, backup, &config_dir)?,
|
||||
Commands::Check {} => cmd_check::cmd_check(&config_dir)?,
|
||||
Commands::Backup(backup) => {
|
||||
trace!("backup subcommand with args: {:?}", backup);
|
||||
|
|
|
@ -7,9 +7,10 @@ use crate::storages::{
|
|||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::ValueEnum;
|
||||
use console::{style, Style, StyledObject};
|
||||
use core::panic;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::BTreeMap, fmt, fs, io, path, u64};
|
||||
use std::{collections::BTreeMap, fmt, fs, io, path};
|
||||
|
||||
/// YAML file to store known storages..
|
||||
pub const STORAGESFILE: &str = "storages.yml";
|
||||
|
@ -50,6 +51,14 @@ impl Storage {
|
|||
Self::Online(_) => "O",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn typestyle(&self) -> Style {
|
||||
match self {
|
||||
Storage::Physical(_) => Style::new().cyan(),
|
||||
Storage::SubDirectory(_) => Style::new().yellow(),
|
||||
Storage::Online(_) => Style::new().green(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StorageExt for Storage {
|
||||
|
@ -69,7 +78,7 @@ impl StorageExt for Storage {
|
|||
}
|
||||
}
|
||||
|
||||
fn mount_path(&self, device: &devices::Device) -> Result<path::PathBuf> {
|
||||
fn mount_path(&self, device: &devices::Device) -> Option<path::PathBuf> {
|
||||
match self {
|
||||
Self::Physical(s) => s.mount_path(device),
|
||||
Self::SubDirectory(s) => s.mount_path(device),
|
||||
|
@ -135,8 +144,8 @@ pub trait StorageExt {
|
|||
fn local_info(&self, device: &devices::Device) -> Option<&local_info::LocalInfo>;
|
||||
|
||||
/// Get mount path of `self` on `device`.
|
||||
/// `storages` is a `BTreeMap` with key of storage name and value of the storage.
|
||||
fn mount_path(&self, device: &devices::Device) -> Result<path::PathBuf>;
|
||||
/// Return [`None`] if the storage([`self`]) is not configured for the `device`.
|
||||
fn mount_path(&self, device: &devices::Device) -> Option<path::PathBuf>;
|
||||
|
||||
/// Add local info of `device` to `self`.
|
||||
fn bound_on_device(
|
||||
|
|
|
@ -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,9 +18,9 @@ pub struct Directory {
|
|||
/// ID of parent storage.
|
||||
parent: String,
|
||||
/// Relative path to the parent storage.
|
||||
relative_path: path::PathBuf,
|
||||
relative_path: Vec<String>,
|
||||
pub notes: String,
|
||||
/// Device and localinfo pairs.
|
||||
/// [`devices::Device`] name and localinfo pairs.
|
||||
local_infos: BTreeMap<String, LocalInfo>,
|
||||
}
|
||||
|
||||
|
@ -34,14 +35,19 @@ impl Directory {
|
|||
relative_path: path::PathBuf,
|
||||
notes: String,
|
||||
local_infos: BTreeMap<String, LocalInfo>,
|
||||
) -> Directory {
|
||||
Directory {
|
||||
) -> Result<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,
|
||||
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.
|
||||
|
@ -80,8 +86,9 @@ impl Directory {
|
|||
let parent_mount_path = self
|
||||
.parent(storages)
|
||||
.context("Can't find parent storage")?
|
||||
.mount_path(device)?;
|
||||
Ok(parent_mount_path.join(self.relative_path.clone()))
|
||||
.mount_path(device)
|
||||
.context("Can't find mount path")?;
|
||||
Ok(parent_mount_path.join(self.relative_path.clone().iter().collect::<PathBuf>()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,12 +105,10 @@ impl StorageExt for Directory {
|
|||
self.local_infos.get(&device.name())
|
||||
}
|
||||
|
||||
fn mount_path(&self, device: &devices::Device) -> Result<path::PathBuf> {
|
||||
Ok(self
|
||||
.local_infos
|
||||
fn mount_path(&self, device: &devices::Device) -> Option<std::path::PathBuf> {
|
||||
self.local_infos
|
||||
.get(&device.name())
|
||||
.context(format!("LocalInfo for storage: {} not found", &self.name()))?
|
||||
.mount_path())
|
||||
.map(|info| info.mount_path())
|
||||
}
|
||||
|
||||
/// This method doesn't use `mount_path`.
|
||||
|
@ -122,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)
|
||||
}
|
||||
}
|
||||
|
@ -134,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::<PathBuf>().display(),
|
||||
notes = self.notes,
|
||||
)
|
||||
}
|
||||
|
@ -178,11 +183,10 @@ 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(storages::Storage::Physical(physical)).unwrap();
|
||||
storages.add(Storage::SubDirectory(directory)).unwrap();
|
||||
// assert_eq!(directory.name(), "test_name");
|
||||
assert_eq!(
|
||||
|
|
|
@ -61,12 +61,10 @@ impl StorageExt for OnlineStorage {
|
|||
self.local_infos.get(&device.name())
|
||||
}
|
||||
|
||||
fn mount_path(&self, device: &devices::Device) -> Result<std::path::PathBuf> {
|
||||
Ok(self
|
||||
.local_infos
|
||||
fn mount_path(&self, device: &devices::Device) -> Option<std::path::PathBuf> {
|
||||
self.local_infos
|
||||
.get(&device.name())
|
||||
.context(format!("LocalInfo for storage: {} not found", &self.name()))?
|
||||
.mount_path())
|
||||
.map(|info| info.mount_path())
|
||||
}
|
||||
|
||||
fn bound_on_device(
|
||||
|
|
|
@ -21,6 +21,7 @@ pub struct PhysicalDrivePartition {
|
|||
fs: String,
|
||||
is_removable: bool,
|
||||
// system_names: BTreeMap<String, String>,
|
||||
/// [`Device`] name and [`LocalInfo`] mapping.
|
||||
local_infos: BTreeMap<String, LocalInfo>,
|
||||
}
|
||||
|
||||
|
@ -112,12 +113,10 @@ impl StorageExt for PhysicalDrivePartition {
|
|||
self.local_infos.get(&device.name())
|
||||
}
|
||||
|
||||
fn mount_path(&self, device: &devices::Device) -> Result<path::PathBuf> {
|
||||
Ok(self
|
||||
.local_infos
|
||||
fn mount_path(&self, device: &devices::Device) -> Option<path::PathBuf> {
|
||||
self.local_infos
|
||||
.get(&device.name())
|
||||
.context(format!("LocalInfo for storage: {} not found", &self.name()))?
|
||||
.mount_path())
|
||||
.map(|info| info.mount_path())
|
||||
}
|
||||
|
||||
fn bound_on_device(
|
||||
|
|
23
src/util.rs
23
src/util.rs
|
@ -1,6 +1,8 @@
|
|||
use std::path::{self, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::TimeDelta;
|
||||
use console::Style;
|
||||
|
||||
use crate::{
|
||||
devices::Device,
|
||||
|
@ -17,12 +19,12 @@ pub fn min_parent_storage<'a>(
|
|||
.list
|
||||
.iter()
|
||||
.filter_map(|(k, storage)| {
|
||||
let storage_path = match storage.mount_path(device) {
|
||||
Ok(path) => path,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let storage_path = storage.mount_path(device)?;
|
||||
let diff = pathdiff::diff_paths(path, storage_path)?;
|
||||
if diff.components().any(|c| c == path::Component::ParentDir) {
|
||||
if diff
|
||||
.components()
|
||||
.any(|c| matches!(c, path::Component::ParentDir | path::Component::Prefix(_)))
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((k, diff))
|
||||
|
@ -61,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;
|
||||
|
|
385
tests/cli.rs
385
tests/cli.rs
|
@ -9,7 +9,7 @@ mod integrated_test {
|
|||
use dirs::home_dir;
|
||||
use git2::Repository;
|
||||
use log::trace;
|
||||
use predicates::prelude::predicate;
|
||||
use predicates::{boolean::PredicateBooleanExt, prelude::predicate};
|
||||
|
||||
/// Setup global gitconfig if it doesn't exist.
|
||||
///
|
||||
|
@ -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";
|
||||
|
@ -157,7 +157,14 @@ mod integrated_test {
|
|||
// set up upstream branch
|
||||
let (mut repo_1_branch, _branch_type) = repo_1.branches(None)?.next().unwrap()?;
|
||||
println!("head {}", repo_1.head().unwrap().name().unwrap());
|
||||
repo_1_branch.set_upstream(Some(format!("{}/{}", upstream_name, repo_1_branch.name().unwrap().unwrap()).as_str()))?;
|
||||
repo_1_branch.set_upstream(Some(
|
||||
format!(
|
||||
"{}/{}",
|
||||
upstream_name,
|
||||
repo_1_branch.name().unwrap().unwrap()
|
||||
)
|
||||
.as_str(),
|
||||
))?;
|
||||
|
||||
// 2nd device
|
||||
let config_dir_2 = assert_fs::TempDir::new()?;
|
||||
|
@ -209,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")?;
|
||||
|
@ -220,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";
|
||||
|
@ -231,9 +240,18 @@ mod integrated_test {
|
|||
println!("{:?}", bare_repo_dir.read_dir()?);
|
||||
// set up upstream branch
|
||||
let (mut repo_1_branch, _branch_type) = repo_1.branches(None)?.next().unwrap()?;
|
||||
repo_1_branch.set_upstream(Some(format!("{}/{}", upstream_name, repo_1_branch.name().unwrap().unwrap()).as_str()))?;
|
||||
repo_1_branch.set_upstream(Some(
|
||||
format!(
|
||||
"{}/{}",
|
||||
upstream_name,
|
||||
repo_1_branch.name().unwrap().unwrap()
|
||||
)
|
||||
.as_str(),
|
||||
))?;
|
||||
|
||||
// 2nd device
|
||||
//
|
||||
// devices: first, second
|
||||
let config_dir_2 = assert_fs::TempDir::new()?;
|
||||
let mut cmd2 = Command::cargo_bin("xdbm")?;
|
||||
cmd2.arg("-c")
|
||||
|
@ -257,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)
|
||||
|
@ -273,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
|
||||
|
@ -294,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")?
|
||||
|
@ -325,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())
|
||||
|
@ -340,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")
|
||||
|
@ -358,6 +407,7 @@ mod integrated_test {
|
|||
.assert()
|
||||
.success();
|
||||
|
||||
// storage list
|
||||
Command::cargo_bin("xdbm")?
|
||||
.arg("-c")
|
||||
.arg(config_dir_2.path())
|
||||
|
@ -365,8 +415,23 @@ mod integrated_test {
|
|||
.arg("list")
|
||||
.arg("-l")
|
||||
.assert()
|
||||
.success();
|
||||
.success()
|
||||
.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");
|
||||
|
@ -387,6 +452,7 @@ mod integrated_test {
|
|||
.assert()
|
||||
.success();
|
||||
|
||||
// backup add but with existing name
|
||||
Command::cargo_bin("xdbm")?
|
||||
.arg("-c")
|
||||
.arg(config_dir_2.path())
|
||||
|
@ -404,6 +470,313 @@ mod integrated_test {
|
|||
.failure()
|
||||
.stderr(predicate::str::contains("already"));
|
||||
|
||||
// backup list
|
||||
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("---")),
|
||||
);
|
||||
|
||||
// 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())
|
||||
.arg("backup")
|
||||
.arg("done")
|
||||
.arg("foodoc")
|
||||
.arg("0")
|
||||
.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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue