Compare commits

..

5 commits
v0.2.1 ... main

Author SHA1 Message Date
0abf9c0693 update readme 2024-07-15 12:12:38 +09:00
6e1619aa18 fmt 2024-06-28 00:33:45 +09:00
b9bb207f35 add integrated test 2024-06-28 00:32:49 +09:00
3b7e2387bd update CHANGELOG & README for #15 2024-06-27 20:41:54 +09:00
qwjyh
ea0acf177c
pretty printing (#15)
* pretty printing for `backup list` and `backup list --long`

* switch coloring crate from colored to console

- console can handle Style separately

* Style for storage list
2024-06-27 18:06:27 +09:00
11 changed files with 172 additions and 40 deletions

View file

@ -1,5 +1,10 @@
# Changelog # Changelog
## [Unreleased]
### Changed
- Colored output for `storage list` and `backup list` ([#15](https://github.com/qwjyh/xdbm/pull/15))
## [0.2.1] - 2024-06-19 ## [0.2.1] - 2024-06-19
### Changed ### Changed
@ -30,6 +35,7 @@
- `backup done` subcommand - `backup done` subcommand
- `completion` subcommand - `completion` subcommand
[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.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.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 [0.1.0]: https://github.com/qwjyh/xdbm/releases/tag/v0.1.0

26
Cargo.lock generated
View file

@ -347,6 +347,19 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]]
name = "console"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"unicode-width",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.6" version = "0.8.6"
@ -454,6 +467,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "env_filter" name = "env_filter"
version = "0.1.0" version = "0.1.0"
@ -724,6 +743,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.155" version = "0.2.155"
@ -1835,6 +1860,7 @@ dependencies = [
"clap", "clap",
"clap-verbosity-flag", "clap-verbosity-flag",
"clap_complete", "clap_complete",
"console",
"dirs", "dirs",
"dunce", "dunce",
"env_logger", "env_logger",

View file

@ -30,6 +30,7 @@ byte-unit = "5.1.4"
anyhow = "1.0" anyhow = "1.0"
pathdiff = "0.2.1" pathdiff = "0.2.1"
unicode-width = "0.1.13" unicode-width = "0.1.13"
console = "0.15"
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.14" assert_cmd = "2.0.14"

View file

@ -2,6 +2,9 @@
_Cross device backup manager_, _Cross device backup manager_,
which manages backups on several storages mounted on multiple devices with a single repository. which manages backups on several storages mounted on multiple devices with a single repository.
## Install
- `git` is required for sync
## Usage ## Usage
1. `xdbm init` to setup new device(i.e. PC). 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. 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 - [ ] write test for storage subcommand
- [x] storage add online - [x] storage add online
- [x] storage add directory - [x] storage add directory
- [ ] storage list - [x] storage list
- [x] update storage bind command - [x] update storage bind command
- [ ] add storage remove command - [ ] add storage remove command
- [ ] add sync subcommand - [ ] add sync subcommand
@ -38,7 +41,7 @@ which manages backups on several storages mounted on multiple devices with a sin
- [x] backup list - [x] backup list
- [x] status printing - [x] status printing
- [x] backup done - [x] backup done
- [ ] fancy display - [x] fancy display
- [ ] json output - [ ] json output
- [ ] no commit option - [ ] no commit option

View file

@ -57,9 +57,7 @@ pub(crate) enum Commands {
Check {}, Check {},
/// Generate completion script. /// Generate completion script.
Completion { Completion { shell: clap_complete::Shell },
shell: clap_complete::Shell,
}
} }
#[derive(Args, Debug)] #[derive(Args, Debug)]

View file

@ -5,7 +5,8 @@ use std::{
}; };
use anyhow::{anyhow, Context, Ok, Result}; use anyhow::{anyhow, Context, Ok, Result};
use chrono::Local; use chrono::{Local, TimeDelta};
use console::Style;
use dunce::canonicalize; use dunce::canonicalize;
use git2::Repository; use git2::Repository;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@ -153,6 +154,17 @@ 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,
@ -188,39 +200,71 @@ fn write_backups_list(
// main printing // main printing
for ((dev, _name), backup) in &backups { for ((dev, _name), backup) in &backups {
let device = backup.device(devices).context(format!( 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() backup.name()
))?; ))?;
let src = backup.source().path(storages, device)?; let src = backup.source().path(storages, device)?;
let dest = backup.destination().path(storages, device)?; let dest = backup.destination().path(storages, device)?;
let cmd_name = backup.command().name(); 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) => { Some(log) => {
let time = Local::now() - log.datetime; let time = Local::now() - log.datetime;
util::format_summarized_duration(time) let s = util::format_summarized_duration(time);
let style = duration_style(time);
(style.apply_to(s), style)
}
None => {
let style = Style::new().red();
(style.apply_to("---".to_string()), style)
} }
None => "---".to_string(),
}; };
writeln!( if !longprint {
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())?;
writeln!( writeln!(
writer, 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() dest = dest.display()
)?; )?;
writeln!( writeln!(
writer, writer,
" {cmd_name:<cmd_name_width$}({note})", "{s_last} {last}",
note = cmd_note, 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(()) Ok(())

View file

@ -7,6 +7,7 @@ use std::{
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use byte_unit::{Byte, UnitType}; use byte_unit::{Byte, UnitType};
use console::style;
use dunce::canonicalize; use dunce::canonicalize;
use git2::Repository; use git2::Repository;
use inquire::{Confirm, CustomType, Text}; use inquire::{Confirm, CustomType, Text};
@ -211,7 +212,7 @@ fn write_storages_list(
"-" "-"
} }
} else { } else {
" " ""
}; };
let path = storage.mount_path(device).map_or_else( let path = storage.mount_path(device).map_or_else(
|e| { |e| {
@ -227,23 +228,24 @@ fn write_storages_list(
} else { } else {
"" ""
}; };
let typestyle = storage.typestyle();
writeln!( writeln!(
writer, writer,
"{stype}{isremovable}: {name:<name_width$} {size:>10} {parent:<name_width$} {path}", "{stype}{isremovable:<1}: {name:<name_width$} {size:>10} {parent:<name_width$} {path}",
stype = storage.shorttypename(), stype = typestyle.apply_to(storage.shorttypename()),
isremovable = isremovable, isremovable = isremovable,
name = storage.name(), name = typestyle.apply_to(storage.name()),
size = size_str, size = size_str,
parent = parent_name, parent = console::style(parent_name).bright().black(),
path = path, path = path,
)?; )?;
if long_display { if long_display {
let note = match storage { let note = match storage {
Storage::Physical(s) => s.kind(), Storage::Physical(s) => format!("kind: {}", s.kind()),
Storage::SubDirectory(s) => &s.notes, Storage::SubDirectory(s) => s.notes.clone(),
Storage::Online(s) => &s.provider, Storage::Online(s) => s.provider.clone(),
}; };
writeln!(writer, " {}", note)?; writeln!(writer, " {}", style(note).italic())?;
} }
} }
Ok(()) Ok(())

View file

@ -7,9 +7,10 @@ use crate::storages::{
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use clap::ValueEnum; use clap::ValueEnum;
use console::{style, Style, StyledObject};
use core::panic; use core::panic;
use serde::{Deserialize, Serialize}; 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.. /// YAML file to store known storages..
pub const STORAGESFILE: &str = "storages.yml"; pub const STORAGESFILE: &str = "storages.yml";
@ -50,6 +51,14 @@ impl Storage {
Self::Online(_) => "O", 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 { impl StorageExt for Storage {

View file

@ -180,9 +180,7 @@ mod test {
local_infos, local_infos,
); );
let mut storages = Storages::new(); let mut storages = Storages::new();
storages storages.add(storages::Storage::Physical(physical)).unwrap();
.add(storages::Storage::Physical(physical))
.unwrap();
storages.add(Storage::SubDirectory(directory)).unwrap(); storages.add(Storage::SubDirectory(directory)).unwrap();
// assert_eq!(directory.name(), "test_name"); // assert_eq!(directory.name(), "test_name");
assert_eq!( assert_eq!(

View file

@ -1,6 +1,7 @@
use std::path::{self, PathBuf}; use std::path::{self, PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use console::Style;
use crate::{ use crate::{
devices::Device, devices::Device,

View file

@ -9,7 +9,7 @@ mod integrated_test {
use dirs::home_dir; use dirs::home_dir;
use git2::Repository; use git2::Repository;
use log::trace; use log::trace;
use predicates::prelude::predicate; use predicates::{boolean::PredicateBooleanExt, prelude::predicate};
/// Setup global gitconfig if it doesn't exist. /// Setup global gitconfig if it doesn't exist.
/// ///
@ -157,7 +157,14 @@ mod integrated_test {
// set up upstream branch // set up upstream branch
let (mut repo_1_branch, _branch_type) = repo_1.branches(None)?.next().unwrap()?; let (mut repo_1_branch, _branch_type) = repo_1.branches(None)?.next().unwrap()?;
println!("head {}", repo_1.head().unwrap().name().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 // 2nd device
let config_dir_2 = assert_fs::TempDir::new()?; let config_dir_2 = assert_fs::TempDir::new()?;
@ -231,7 +238,14 @@ mod integrated_test {
println!("{:?}", bare_repo_dir.read_dir()?); println!("{:?}", bare_repo_dir.read_dir()?);
// set up upstream branch // set up upstream branch
let (mut repo_1_branch, _branch_type) = repo_1.branches(None)?.next().unwrap()?; 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 // 2nd device
let config_dir_2 = assert_fs::TempDir::new()?; let config_dir_2 = assert_fs::TempDir::new()?;
@ -358,6 +372,7 @@ mod integrated_test {
.assert() .assert()
.success(); .success();
// storage list
Command::cargo_bin("xdbm")? Command::cargo_bin("xdbm")?
.arg("-c") .arg("-c")
.arg(config_dir_2.path()) .arg(config_dir_2.path())
@ -365,7 +380,9 @@ mod integrated_test {
.arg("list") .arg("list")
.arg("-l") .arg("-l")
.assert() .assert()
.success(); .success()
.stdout(predicate::str::contains("gdrive_docs").and(predicate::str::contains("nas")));
// backup add // backup add
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)?;
@ -387,6 +404,7 @@ mod integrated_test {
.assert() .assert()
.success(); .success();
// backup add but with existing name
Command::cargo_bin("xdbm")? Command::cargo_bin("xdbm")?
.arg("-c") .arg("-c")
.arg(config_dir_2.path()) .arg(config_dir_2.path())
@ -404,6 +422,32 @@ mod integrated_test {
.failure() .failure()
.stderr(predicate::str::contains("already")); .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
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir_2.path())
.arg("backup")
.arg("done")
.arg("foodoc")
.arg("0")
.assert()
.success();
Ok(()) Ok(())
} }
} }