From de6f93bacb8dd0f67d71f28681007ee519306497 Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Mon, 25 Apr 2022 13:08:41 +0200 Subject: [PATCH] Add grouping of snapshots --- src/commands/snapshots.rs | 64 +++++++++++--------- src/commands/tag.rs | 4 +- src/repo/snapshotfile.rs | 123 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 156 insertions(+), 35 deletions(-) diff --git a/src/commands/snapshots.rs b/src/commands/snapshots.rs index 985a06d..6e3bbb7 100644 --- a/src/commands/snapshots.rs +++ b/src/commands/snapshots.rs @@ -4,43 +4,51 @@ use clap::Parser; use prettytable::{cell, format, row, Table}; use crate::backend::DecryptReadBackend; -use crate::repo::{SnapshotFile, SnapshotFilter}; +use crate::repo::{SnapshotFile, SnapshotFilter, SnapshotGroupCriterion}; #[derive(Parser)] pub(super) struct Opts { #[clap(flatten)] filter: SnapshotFilter, + + /// group snapshots by any combination of host,paths,tags + #[clap(long, short = 'g', value_name = "CRITERION", default_value = "")] + group_by: SnapshotGroupCriterion, } pub(super) async fn execute(be: &impl DecryptReadBackend, opts: Opts) -> Result<()> { - let mut snapshots = SnapshotFile::all_from_backend(be).await?; - snapshots.sort(); + let groups = SnapshotFile::group_from_backend(be, &opts.filter, &opts.group_by).await?; - let mut table: Table = snapshots - .into_iter() - .filter(|sn| sn.matches(&opts.filter)) - .map(|sn| { - let tags = sn.tags.formatln(); - let paths = sn.paths.formatln(); - let time = sn.time.format("%Y-%m-%d %H:%M:%S"); - let nodes = sn - .node_count - .map(|c| c.to_string()) - .unwrap_or_else(|| "?".to_string()); - let size = sn - .size - .map(|b| ByteSize(b).to_string_as(true)) - .unwrap_or_else(|| "?".to_string()); - row![sn.id, time, sn.hostname, tags, paths, r->nodes, r->size] - }) - .collect(); - let count = table.len(); - table.set_titles( - row![b->"ID", b->"Time", b->"Host", b->"Tags", b->"Paths", br->"Nodes", br->"Size"], - ); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.printstd(); - println!("{} snapshot(s)", count); + for (group, mut snapshots) in groups { + if !group.is_empty() { + println!("snapshots for {:?}", group); + } + snapshots.sort_unstable(); + let mut table: Table = snapshots + .into_iter() + .map(|sn| { + let tags = sn.tags.formatln(); + let paths = sn.paths.formatln(); + let time = sn.time.format("%Y-%m-%d %H:%M:%S"); + let nodes = sn + .node_count + .map(|c| c.to_string()) + .unwrap_or_else(|| "?".to_string()); + let size = sn + .size + .map(|b| ByteSize(b).to_string_as(true)) + .unwrap_or_else(|| "?".to_string()); + row![sn.id, time, sn.hostname, tags, paths, r->nodes, r->size] + }) + .collect(); + let count = table.len(); + table.set_titles( + row![b->"ID", b->"Time", b->"Host", b->"Tags", b->"Paths", br->"Nodes", br->"Size"], + ); + table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.printstd(); + println!("{} snapshot(s)", count); + } Ok(()) } diff --git a/src/commands/tag.rs b/src/commands/tag.rs index 5fd084e..741d4ce 100644 --- a/src/commands/tag.rs +++ b/src/commands/tag.rs @@ -24,10 +24,10 @@ pub(super) struct Opts { } pub(super) async fn execute(be: &impl DecryptFullBackend, opts: Opts) -> Result<()> { - let snapshots = SnapshotFile::all_from_backend(be).await?; + let snapshots = SnapshotFile::all_from_backend(be, &opts.filter).await?; let mut count = 0; - for sn in snapshots.into_iter().filter(|sn| sn.matches(&opts.filter)) { + for sn in snapshots.into_iter() { if modify_sn(sn, be, &opts).await? { count += 1; } diff --git a/src/repo/snapshotfile.rs b/src/repo/snapshotfile.rs index 15159a8..6bff0f1 100644 --- a/src/repo/snapshotfile.rs +++ b/src/repo/snapshotfile.rs @@ -1,9 +1,10 @@ +use std::cmp::Ordering; use std::str::FromStr; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use chrono::{DateTime, Local}; use clap::Parser; -use futures::TryStreamExt; +use futures::{future, TryStreamExt}; use indicatif::ProgressBar; use serde::{Deserialize, Serialize}; use vlog::*; @@ -112,6 +113,51 @@ pub struct SnapshotFilter { hostnames: Vec, } +#[derive(Default)] +pub struct SnapshotGroupCriterion { + hostname: bool, + paths: bool, + tags: bool, +} + +impl FromStr for SnapshotGroupCriterion { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + let mut crit = SnapshotGroupCriterion::default(); + for val in s.split(',') { + match val { + "host" => crit.hostname = true, + "paths" => crit.paths = true, + "tags" => crit.tags = true, + "" => continue, + v => bail!("{} not allowed", v), + } + } + Ok(crit) + } +} + +#[derive(Default, Debug)] +pub struct SnapshotGroup { + hostname: Option, + paths: Option, + tags: Option, +} + +impl SnapshotGroup { + pub fn from_sn(sn: &SnapshotFile, crit: &SnapshotGroupCriterion) -> Self { + Self { + hostname: crit.hostname.then(|| sn.hostname.clone()), + paths: crit.paths.then(|| sn.paths.clone()), + tags: crit.tags.then(|| sn.tags.clone()), + } + } + + pub fn is_empty(&self) -> bool { + self.hostname.is_none() && self.paths.is_none() && self.tags.is_none() + } +} + impl SnapshotFile { /// Get a SnapshotFile from the backend pub async fn from_backend(be: &B, id: &Id) -> Result { @@ -166,8 +212,74 @@ impl SnapshotFile { SnapshotFile::from_backend(be, &id).await } - /// Get all SnapshotFiles from the backend - pub async fn all_from_backend(be: &B) -> Result> { + fn cmp_group(&self, crit: &SnapshotGroupCriterion, other: &Self) -> Ordering { + match crit.hostname { + false => Ordering::Equal, + true => self.hostname.cmp(&other.hostname), + } + .then_with(|| match crit.paths { + false => Ordering::Equal, + true => self.paths.cmp(&other.paths), + }) + .then_with(|| match crit.tags { + false => Ordering::Equal, + true => self.tags.cmp(&other.tags), + }) + } + + fn has_group(&self, group: &SnapshotGroup) -> bool { + (match &group.hostname { + Some(val) => val == &self.hostname, + None => true, + }) && (match &group.paths { + Some(val) => val == &self.paths, + None => true, + }) && (match &group.tags { + Some(val) => val == &self.tags, + None => true, + }) + } + + /// Get SnapshotFiles which match the filter grouped by the group criterion + /// from the backend + pub async fn group_from_backend( + be: &B, + filter: &SnapshotFilter, + crit: &SnapshotGroupCriterion, + ) -> Result)>> { + let mut snaps = Self::all_from_backend(be, filter).await?; + snaps.sort_unstable_by(|sn1, sn2| sn1.cmp_group(crit, sn2)); + + let mut result = Vec::new(); + + if snaps.is_empty() { + return Ok(result); + } + + let mut iter = snaps.into_iter(); + + let snap = iter.next().unwrap(); + let mut group = SnapshotGroup::from_sn(&snap, crit); + let mut result_group = vec![snap]; + + for snap in iter { + if snap.has_group(&group) { + result_group.push(snap); + } else { + result.push((group, result_group)); + group = SnapshotGroup::from_sn(&snap, crit); + result_group = vec![snap] + } + } + result.push((group, result_group)); + + Ok(result) + } + + pub async fn all_from_backend( + be: &B, + filter: &SnapshotFilter, + ) -> Result> { Ok(be .stream_all::(ProgressBar::hidden()) .await? @@ -175,6 +287,7 @@ impl SnapshotFile { snap.id = id; snap }) + .try_filter(|sn| future::ready(sn.matches(filter))) .try_collect() .await?) } @@ -223,7 +336,7 @@ impl Ord for SnapshotFile { } } -#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)] pub struct StringList(Vec); impl FromStr for StringList {