diff --git a/README.md b/README.md index fe98747..566dc0d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/backups.rs b/src/backups.rs index 3cb9565..3c55c98 100644 --- a/src/backups.rs +++ b/src/backups.rs @@ -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 { + 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) -> 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)] diff --git a/src/cmd_args.rs b/src/cmd_args.rs index 63a6d3c..3a4796e 100644 --- a/src/cmd_args.rs +++ b/src/cmd_args.rs @@ -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, + #[arg(long)] + dest: Option, + #[arg(long)] + device: Option, + #[arg(short, long)] + long: bool, + }, /// Record xdbm that the backup with the name has finished right now. Done { /// Name of the backup config. diff --git a/src/cmd_backup.rs b/src/cmd_backup.rs index 91ffb01..9d5e3aa 100644 --- a/src/cmd_backup.rs +++ b/src/cmd_backup.rs @@ -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, + dest_storage: Option, + device_name: Option, + 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, +) -> 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: 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,