new subcommand: backup list

- todo: fancy print
This commit is contained in:
qwjyh 2024-03-15 04:16:57 +09:00
parent d947dd35e0
commit ff32996360
6 changed files with 221 additions and 11 deletions

View file

@ -18,9 +18,11 @@
- [x] use subcommand
- [ ] backup subcommands
- [ ] backup add
- [?] test for backup add
- [ ] test for backup add
- [ ] backup list
- [ ] status printing
- [ ] backup done
- [ ] fancy display
- [ ] no commit option
<!-- vim: set sw=2 ts=2: -->

View file

@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
use crate::{
devices::Device,
storages::{self, Storage},
storages::{self, Storage, StorageExt, Storages},
};
/// Directory to store backup configs for each devices.
@ -28,9 +28,9 @@ pub fn backups_file(device: &Device) -> PathBuf {
pub struct BackupTarget {
/// `name()` of [`Storage`].
/// Use `String` for serialization/deserialization.
storage: String,
pub storage: String,
/// Relative path to the `storage`.
path: PathBuf,
pub path: PathBuf,
}
impl BackupTarget {
@ -40,6 +40,12 @@ impl BackupTarget {
path: relative_path,
}
}
pub fn path(&self, storages: &Storages, device: &Device) -> Result<PathBuf> {
let parent = storages.get(&self.storage).unwrap();
let parent_path = parent.mount_path(device, storages)?;
Ok(parent_path.join(self.path.clone()))
}
}
/// Type of backup commands.
@ -48,6 +54,26 @@ pub enum BackupCommand {
ExternallyInvoked(ExternallyInvoked),
}
pub trait BackupCommandExt {
fn name(&self) -> &String;
fn note(&self) -> &String;
}
impl BackupCommandExt for BackupCommand {
fn name(&self) -> &String {
match self {
BackupCommand::ExternallyInvoked(cmd) => cmd.name(),
}
}
fn note(&self) -> &String {
match self {
BackupCommand::ExternallyInvoked(cmd) => cmd.note(),
}
}
}
/// Backup commands which is not invoked from xdbm itself.
/// Call xdbm externally to record backup datetime and status.
#[derive(Debug, Serialize, Deserialize)]
@ -62,6 +88,16 @@ impl ExternallyInvoked {
}
}
impl BackupCommandExt for ExternallyInvoked {
fn name(&self) -> &String {
&self.name
}
fn note(&self) -> &String {
&self.note
}
}
/// Backup execution log.
#[derive(Debug, Serialize, Deserialize)]
pub struct BackupLog {
@ -112,6 +148,22 @@ impl Backup {
pub fn name(&self) -> &String {
&self.name
}
pub fn device<'a>(&'a self, devices: &'a Vec<Device>) -> Option<&Device> {
devices.iter().find(|dev| dev.name() == self.device)
}
pub fn source(&self) -> &BackupTarget {
&self.from
}
pub fn destination(&self) -> &BackupTarget {
&self.to
}
pub fn command(&self) -> &BackupCommand {
&self.command
}
}
#[derive(Debug, Serialize, Deserialize)]

View file

@ -154,7 +154,17 @@ pub(crate) enum BackupSubCommands {
cmd: BackupAddCommands,
},
/// Print configured backups.
List {},
/// Filter by src/dest storage or device.
List {
#[arg(long)]
src: Option<String>,
#[arg(long)]
dest: Option<String>,
#[arg(long)]
device: Option<String>,
#[arg(short, long)]
long: bool,
},
/// Record xdbm that the backup with the name has finished right now.
Done {
/// Name of the backup config.

View file

@ -1,11 +1,18 @@
use std::{io::stdout, path::{Path, PathBuf}};
use std::{
collections::HashMap,
io::{self, stdout, Write},
path::PathBuf,
};
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use git2::Repository;
use unicode_width::UnicodeWidthStr;
use crate::{
add_and_commit,
backups::{self, Backup, BackupCommand, BackupTarget, Backups, ExternallyInvoked},
backups::{
self, Backup, BackupCommand, BackupCommandExt, BackupTarget, Backups, ExternallyInvoked,
},
cmd_args::BackupAddCommands,
devices::{self, Device},
storages::{StorageExt, Storages},
@ -79,3 +86,132 @@ fn new_backup(
command,
))
}
#[cfg(test)]
mod test {
use anyhow::Result;
#[test]
fn test_new_backup() -> Result<()> {
todo!()
}
}
pub fn cmd_backup_list(
src_storage: Option<String>,
dest_storage: Option<String>,
device_name: Option<String>,
longprint: bool,
config_dir: &PathBuf,
storages: &Storages,
) -> Result<()> {
let devices = devices::get_devices(&config_dir)?;
let backups: HashMap<(String, String), Backup> = match device_name {
Some(device_name) => {
let device = devices
.iter()
.find(|dev| dev.name() == device_name)
.context(format!("Device with name {} doesn't exist", device_name))?;
let backups = Backups::read(&config_dir, device)?;
let mut allbackups = HashMap::new();
for (name, backup) in backups.list {
if allbackups.insert((device.name(), name), backup).is_some() {
return Err(anyhow!("unexpected duplication in backups hashmap"));
};
}
allbackups
}
None => {
let mut allbackups = HashMap::new();
for device in &devices {
let backups = Backups::read(&config_dir, &device)?;
for (name, backup) in backups.list {
if allbackups.insert((device.name(), name), backup).is_some() {
return Err(anyhow!("unexpected duplication in backups hashmap"));
};
}
}
allbackups
}
};
// source/destination filtering
let backups: HashMap<(String, String), Backup> = backups
.into_iter()
.filter(|((_dev, _name), backup)| {
let src_matched = match &src_storage {
Some(src_storage) => &backup.source().storage == src_storage,
None => true,
};
let dest_matched = match &dest_storage {
Some(dest_storage) => &backup.destination().storage == dest_storage,
None => true,
};
src_matched && dest_matched
})
.collect();
let mut stdout = io::BufWriter::new(io::stdout());
write_backups_list(&mut stdout, backups, longprint, storages, &devices)?;
stdout.flush()?;
Ok(())
}
/// TODO: status printing
fn write_backups_list(
mut writer: impl io::Write,
backups: HashMap<(String, String), Backup>,
longprint: bool,
storages: &Storages,
devices: &Vec<Device>,
) -> Result<()> {
let mut name_width = 0;
let mut dev_width = 0;
let mut src_width = 0;
let mut src_storage_width = 0;
let mut dest_width = 0;
let mut dest_storage_width = 0;
// get widths
for ((dev, _name), backup) in &backups {
let device = backup.device(devices).context(format!(
"Couldn't find device specified in backup config {}",
backup.name()
))?;
name_width = name_width.max(backup.name().width());
dev_width = dev_width.max(dev.width());
let src = backup.source().path(&storages, &device)?;
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)?;
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();
}
// main printing
for ((dev, _name), backup) in &backups {
let device = backup.device(devices).context(format!(
"Couldn't find device specified in backup config {}",
backup.name()
))?;
let src = backup.source().path(&storages, &device)?;
let dest = backup.destination().path(&storages, &device)?;
let cmd_name = backup.command().name();
writeln!(
writer,
"{name:<name_width$} [{dev:<dev_width$}] {src:<src_storage_width$} → {dest:<dest_storage_width$} {cmd_name}",
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!(
writer,
" dest: {dest:<dest_width$}",
dest = dest.display()
)?;
writeln!(writer, " {note}", note = cmd_note,)?;
} else {
}
}
Ok(())
}

View file

@ -1,5 +1,8 @@
use inquire::{
autocompletion::{Autocomplete, Replacement},
CustomUserError,
};
use std::io::ErrorKind;
use inquire::{autocompletion::{Autocomplete, Replacement}, CustomUserError};
#[derive(Clone, Default)]
pub struct FilePathCompleter {

View file

@ -107,8 +107,15 @@ fn main() -> Result<()> {
src,
dest,
cmd,
} => cmd_backup::cmd_backup_add(name, src, dest, cmd, repo, &config_dir, &storages)?,
BackupSubCommands::List {} => todo!(),
} => {
cmd_backup::cmd_backup_add(name, src, dest, cmd, repo, &config_dir, &storages)?
}
BackupSubCommands::List {
src,
dest,
device,
long,
} => cmd_backup::cmd_backup_list(src, dest, device, long, &config_dir, &storages)?,
BackupSubCommands::Done {
name,
exit_status,