Compare commits

..

22 commits

Author SHA1 Message Date
qwjyh
aed0049f98
Merge pull request #17 from qwjyh/cmd-status
[feat] `status` subcommand & change relative path type
2024-12-02 03:41:33 +09:00
4e8387e3b0 fix typo 2024-12-02 03:07:48 +09:00
07ef49ca7b add: long argument option to cmd_status 2024-12-02 03:07:25 +09:00
3d8aa7bca0 update: CHANGELOG 2024-12-02 03:05:43 +09:00
996ca7e4b3 run fmt 2024-12-02 03:00:56 +09:00
24f1356685 fix(test): for commit 7e043a652c 2024-12-02 02:59:34 +09:00
7e043a652c update: don't display devices with no backups in cmd_status 2024-12-02 02:53:30 +09:00
af1b56eee2 add: style to cmd_status 2024-12-02 02:52:53 +09:00
c4aa76d125 update(test): add prefix '_' to unused variables 2024-12-02 02:15:49 +09:00
697c9c0712 add(test): test for multiple backups and status & add comment
comment for current config
2024-12-01 23:33:24 +09:00
bce9f3710c add(test): command status test 2024-12-01 23:32:36 +09:00
315a75424a add(test): command backup after done doesn't include --- 2024-12-01 23:31:58 +09:00
d743e607ac update: remove todo!() at the end of cmd_status 2024-12-01 23:14:50 +09:00
7592ec0ad0 Merge branch 'main' into cmd-status 2024-12-01 21:17:22 +09:00
6a0abd03d5 fix: apply subpath logic patch to util::min_parent_storage 2024-12-01 20:55:49 +09:00
ced354bf58 fix: sub directory decision logic on windows
Diff of "C:\foo" from "D:\" is "C:\foo" and doesn't contain "..". In old
logic, "C:\foo" is treated as subpath of "D:\" but this is not
intuitive.
2024-12-01 20:41:37 +09:00
dbc0d78f99 update: CHANGELOG on path type change 2024-12-01 19:36:33 +09:00
8b0dbb2314 fix: add lifetime annotation (clippy) 2024-12-01 19:19:28 +09:00
772689ab6a change: change type of relative path shared on multiple platforms to Vector<String>
Parsers for path on Windows and Unix are different on separator character treatment.
Replacing to Vector<String> avoids this differenct for cross-platform compatibility.
2024-12-01 19:14:57 +09:00
bc3939c9bc (WIP): implementing multi device backup search 2024-11-08 06:09:04 +09:00
a409a43906 (WIP) feat: stat for backup with name
- replace path funcs return from Result to Option
- add tests for `parent_backups`
2024-08-09 21:45:44 +02:00
37782c934c (WIP) new: implement status subcommand
- Error while getting mount path of Storages
- Need to redesign the API of LocalInfo or something
2024-08-07 11:39:58 +02:00
13 changed files with 749 additions and 80 deletions

View file

@ -4,6 +4,8 @@
### Changed
- Colored output for `storage list` and `backup list` ([#15](https://github.com/qwjyh/xdbm/pull/15))
- **BREAKING** Relative path is changed from `PathBuf` to `Vector<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

View file

@ -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>,
}

View file

@ -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 {},

View file

@ -5,7 +5,7 @@ use std::{
};
use anyhow::{anyhow, Context, Ok, Result};
use chrono::{Local, TimeDelta};
use chrono::Local;
use console::Style;
use dunce::canonicalize;
use git2::Repository;
@ -89,8 +89,8 @@ fn new_backup(
Ok(Backup::new(
name,
device.name(),
src_target,
dest_target,
src_target?,
dest_target?,
command,
))
}
@ -154,17 +154,6 @@ pub fn cmd_backup_list(
Ok(())
}
fn duration_style(time: TimeDelta) -> Style {
match time {
x if x < TimeDelta::days(7) => Style::new().green(),
x if x < TimeDelta::days(14) => Style::new().yellow(),
x if x < TimeDelta::days(28) => Style::new().magenta(),
x if x < TimeDelta::days(28 * 3) => Style::new().red(),
x if x < TimeDelta::days(180) => Style::new().red().bold(),
_ => Style::new().on_red().black(),
}
}
/// TODO: status printing
fn write_backups_list(
mut writer: impl io::Write,
@ -188,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();
@ -203,14 +198,20 @@ fn write_backups_list(
"Couldn't find the device specified in the backup config: {}",
backup.name()
))?;
let src = backup.source().path(storages, device)?;
let dest = backup.destination().path(storages, device)?;
let src = backup
.source()
.path(storages, device)
.context("Couldn't get path for source")?;
let dest = backup
.destination()
.path(storages, device)
.context("Couldn't get path for destination")?;
let cmd_name = backup.command().name();
let (last_backup_elapsed, style_on_time_elapsed) = match backup.last_backup() {
Some(log) => {
let time = Local::now() - log.datetime;
let s = util::format_summarized_duration(time);
let style = duration_style(time);
let style = util::duration_style(time);
(style.apply_to(s), style)
}
None => {
@ -360,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
View 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, &current_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, &current_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());
}
}

View file

@ -215,8 +215,8 @@ fn write_storages_list(
""
};
let path = storage.mount_path(device).map_or_else(
|e| {
info!("Not found: {}", e);
|| {
info!("Mount path not found");
"".to_string()
},
|v| v.display().to_string(),

View file

@ -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);

View file

@ -78,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),
@ -144,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(

View file

@ -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,7 +183,8 @@ mod test {
"subdir".into(),
"some note".to_string(),
local_infos,
);
)
.unwrap();
let mut storages = Storages::new();
storages.add(storages::Storage::Physical(physical)).unwrap();
storages.add(Storage::SubDirectory(directory)).unwrap();

View file

@ -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(

View file

@ -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(

View file

@ -1,6 +1,7 @@
use std::path::{self, PathBuf};
use anyhow::{Context, Result};
use chrono::TimeDelta;
use console::Style;
use crate::{
@ -18,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))
@ -62,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;

View file

@ -145,7 +145,7 @@ mod integrated_test {
// bare-repo
let bare_repo_dir = assert_fs::TempDir::new()?;
let bare_repo = Repository::init_bare(&bare_repo_dir)?;
let _bare_repo = Repository::init_bare(&bare_repo_dir)?;
// push to bare repository
let repo_1 = Repository::open(&config_dir_1)?;
let upstream_name = "remote";
@ -216,6 +216,8 @@ mod integrated_test {
#[test]
fn two_devices() -> Result<()> {
// 1st device
//
// devices: first
let config_dir_1 = assert_fs::TempDir::new()?;
setup_gitconfig()?;
let mut cmd1 = Command::cargo_bin("xdbm")?;
@ -227,7 +229,7 @@ mod integrated_test {
// bare-repo
let bare_repo_dir = assert_fs::TempDir::new()?;
let bare_repo = Repository::init_bare(&bare_repo_dir)?;
let _bare_repo = Repository::init_bare(&bare_repo_dir)?;
// push to bare repository
let repo_1 = Repository::open(&config_dir_1)?;
let upstream_name = "remote";
@ -248,6 +250,8 @@ mod integrated_test {
))?;
// 2nd device
//
// devices: first, second
let config_dir_2 = assert_fs::TempDir::new()?;
let mut cmd2 = Command::cargo_bin("xdbm")?;
cmd2.arg("-c")
@ -271,6 +275,7 @@ mod integrated_test {
assert!(config_dir_2.join("backups").join("first.yml").exists());
assert!(config_dir_2.join("backups").join("second.yml").exists());
// sync
std::process::Command::new("git")
.arg("push")
.current_dir(&config_dir_2)
@ -287,6 +292,11 @@ mod integrated_test {
.success();
// Add storage
//
// devices: first, second
// storages:
// - gdrive @ sample_storage (online)
// - first: sample_storage
let sample_storage = assert_fs::TempDir::new()?;
let mut cmd_add_storage_1 = Command::cargo_bin("xdbm")?;
cmd_add_storage_1
@ -308,6 +318,13 @@ mod integrated_test {
.success()
.stdout(predicate::str::contains(""));
// Add storage (directory)
//
// devices: first, second
// storages:
// - gdrive (online)
// - first: sample_storage
// - gdrive_docs (subdir of sample_storage/foo/bar)
// - first
let sample_directory = &sample_storage.join("foo").join("bar");
DirBuilder::new().recursive(true).create(sample_directory)?;
Command::cargo_bin("xdbm")?
@ -339,6 +356,14 @@ mod integrated_test {
.success();
// bind
//
// devices: first, second
// storages:
// - gdrive (online)
// - first: sample_storage
// - gdrive_docs (subdir of sample_storage/foo/bar)
// - first
// - second: sample_directory
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
@ -354,6 +379,16 @@ mod integrated_test {
.stdout(predicate::str::contains(""));
// storage 3
//
// devices: first, second
// storages:
// - gdrive (online)
// - first: sample_storage
// - gdrive_docs (subdir of sample_storage/foo/bar)
// - first
// - second: sample_directory
// - nas (online)
// - second: sample_storage_2
let sample_storage_2 = assert_fs::TempDir::new()?;
Command::cargo_bin("xdbm")?
.arg("-c")
@ -384,6 +419,19 @@ mod integrated_test {
.stdout(predicate::str::contains("gdrive_docs").and(predicate::str::contains("nas")));
// backup add
//
// devices: first, second
// storages:
// - gdrive (online)
// - first: sample_storage
// - gdrive_docs (subdir of sample_storage/foo/bar)
// - first
// - second: sample_directory
// - nas (online)
// - second: sample_storage_2
// backups:
// - foodoc: second
// - sample_storage_2/foo/bar -> sample_directory/docs
let backup_src = &sample_storage_2.join("foo").join("bar");
DirBuilder::new().recursive(true).create(backup_src)?;
let backup_dest = &sample_directory.join("docs");
@ -438,6 +486,19 @@ mod integrated_test {
);
// backup done
//
// devices: first, second
// storages:
// - gdrive (online)
// - first: sample_storage
// - gdrive_docs (subdir of sample_storage/foo/bar)
// - first
// - second: sample_directory
// - nas (online)
// - second: sample_storage_2
// backups:
// - foodoc: second
// - sample_storage_2/foo/bar -> sample_directory/docs (done 1)
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
@ -448,6 +509,274 @@ mod integrated_test {
.assert()
.success();
// backup list after backup done
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
.arg("backup")
.arg("list")
.assert()
.success()
.stdout(
predicate::str::contains("foodoc")
.and(predicate::str::contains("nas"))
.and(predicate::str::contains("gdrive_docs"))
.and(predicate::str::contains("---").not()),
);
// status
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
.arg("status")
.assert()
.success();
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
.arg("status")
.arg("-s")
.arg(backup_src.clone().join("foo"))
.assert()
.success()
.stdout(predicate::str::contains("nas").and(predicate::str::contains("foodoc").not()));
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
.arg("status")
.arg("-sb")
.arg(backup_src.clone().join("foo"))
.assert()
.success()
.stdout(
predicate::str::contains("nas")
.and(predicate::str::contains("second"))
.and(predicate::str::contains("foodoc")),
);
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
.arg("status")
.arg("-sb")
.arg(backup_src.clone().parent().unwrap())
.assert()
.success()
.stdout(
predicate::str::contains("nas")
.and(predicate::str::contains("second").not())
.and(predicate::str::contains("foodoc").not()),
);
std::process::Command::new("git")
.arg("push")
.current_dir(&config_dir_2)
.assert()
.success();
std::process::Command::new("git")
.arg("pull")
.current_dir(&config_dir_1)
.assert()
.success();
// bind
//
// devices: first, second
// storages:
// - gdrive (online)
// - first: sample_storage
// - gdrive_docs (subdir of sample_storage/foo/bar)
// - first
// - second: sample_directory
// - nas (online)
// - first: sample_storage_2_first_path
// - second: sample_storage_2
// backups:
// - foodoc: second
// - sample_storage_2/foo/bar -> sample_directory/docs (done 1)
let sample_storage_2_first_path = assert_fs::TempDir::new()?;
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_1.path())
.arg("storage")
.arg("bind")
.arg("--alias")
.arg("sample2")
.arg("--path")
.arg(sample_storage_2_first_path.path())
.arg("nas")
.assert()
.success()
.stdout(predicate::str::contains(""));
// backup add
//
// devices: first, second
// storages:
// - gdrive (online)
// - first: sample_storage
// - gdrive_docs (subdir of sample_storage/foo/bar)
// - first
// - second: sample_directory
// - nas (online)
// - first: sample_storage_2_first_path
// - second: sample_storage_2
// backups:
// - foodoc: second
// - sample_storage_2/foo/bar -> sample_directory/docs (done 1)
// - abcdbackup: first
// - sample_storage_2_first_path/abcd/efgh -> sample_storage/Downloads/abcd/efgh
let backup_src = &sample_storage_2_first_path.join("abcd").join("efgh");
DirBuilder::new().recursive(true).create(backup_src)?;
let backup_dest = &sample_storage.join("Downloads").join("abcd").join("efgh");
DirBuilder::new().recursive(true).create(backup_dest)?;
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_1.path())
.arg("backup")
.arg("add")
.arg("--src")
.arg(backup_src)
.arg("--dest")
.arg(backup_dest)
.arg("abcdbackup")
.arg("external")
.arg("rsync")
.arg("note: nonsense")
.assert()
.success();
// backup add
//
// devices: first, second
// storages:
// - gdrive (online)
// - first: sample_storage
// - gdrive_docs (subdir of sample_storage/foo/bar)
// - first
// - second: sample_directory
// - nas (online)
// - first: sample_storage_2_first_path
// - second: sample_storage_2
// backups:
// - foodoc: second
// - sample_storage_2/foo/bar -> sample_directory/docs (done 1)
// - abcdbackup: first
// - sample_storage_2_first_path/abcd/efgh -> sample_storage/Downloads/abcd/efgh
// - abcdsubbackup: first
// - sample_storage_2_first_path/abcd/efgh/sub -> sample_storage/Downloads/abcd/efgh/sub
let backup_src = &sample_storage_2_first_path
.join("abcd")
.join("efgh")
.join("sub");
DirBuilder::new().recursive(true).create(backup_src)?;
let backup_dest = &sample_storage
.join("Downloads")
.join("abcd")
.join("efgh")
.join("sub");
DirBuilder::new().recursive(true).create(backup_dest)?;
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_1.path())
.arg("backup")
.arg("add")
.arg("--src")
.arg(backup_src)
.arg("--dest")
.arg(backup_dest)
.arg("abcdsubbackup")
.arg("external")
.arg("rsync")
.arg("note: only subdirectory")
.assert()
.success();
std::process::Command::new("git")
.arg("push")
.current_dir(&config_dir_1)
.assert()
.success();
std::process::Command::new("git")
.arg("pull")
.current_dir(&config_dir_2)
.assert()
.success();
// backup add
//
// devices: first, second
// storages:
// - gdrive (online)
// - first: sample_storage
// - gdrive_docs (subdir of sample_storage/foo/bar)
// - first
// - second: sample_directory
// - nas (online)
// - first: sample_storage_2_first_path
// - second: sample_storage_2
// backups:
// - foodoc: second
// - sample_storage_2/foo/bar -> sample_directory/docs (done 1)
// - abcdbackup: first
// - sample_storage_2_first_path/abcd/efgh -> sample_storage/Downloads/abcd/efgh
// - abcdsubbackup: first
// - sample_storage_2_first_path/abcd/efgh/sub -> sample_storage/Downloads/abcd/efgh/sub
// - abcdbackup2: second
// - sample_storage_2/abcd/efgh -> sample_directory/Downloads/abcd/efgh
let backup_src = &sample_storage_2.join("abcd").join("efgh");
DirBuilder::new().recursive(true).create(backup_src)?;
let backup_dest = &sample_directory.join("Downloads").join("abcd").join("efgh");
DirBuilder::new().recursive(true).create(backup_dest)?;
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
.arg("backup")
.arg("add")
.arg("--src")
.arg(backup_src)
.arg("--dest")
.arg(backup_dest)
.arg("abcdbackup2")
.arg("external")
.arg("rsync")
.arg("note: only subdirectory")
.assert()
.success();
// status
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
.arg("status")
.arg("-sb")
.arg(backup_src)
.assert()
.success()
.stdout(
predicate::str::contains("nas")
.and(predicate::str::contains("first"))
.and(predicate::str::contains("abcdbackup"))
.and(predicate::str::contains("abcdsubbackup").not())
.and(predicate::str::contains("second"))
.and(predicate::str::contains("abcdbackup2")),
);
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
.arg("status")
.arg("-sb")
.arg(backup_src.join("sub"))
.assert()
.success()
.stdout(
predicate::str::contains("nas")
.and(predicate::str::contains("first"))
.and(predicate::str::contains("abcdbackup"))
.and(predicate::str::contains("abcdsubbackup"))
.and(predicate::str::contains("second"))
.and(predicate::str::contains("abcdbackup2")),
);
Ok(())
}
}