Add grouping of snapshots

This commit is contained in:
Alexander Weiss 2022-04-25 13:08:41 +02:00
parent d09fbee1cc
commit de6f93bacb
3 changed files with 156 additions and 35 deletions

View File

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

View File

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

View File

@ -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<String>,
}
#[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<Self> {
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<String>,
paths: Option<StringList>,
tags: Option<StringList>,
}
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<B: DecryptReadBackend>(be: &B, id: &Id) -> Result<Self> {
@ -166,8 +212,74 @@ impl SnapshotFile {
SnapshotFile::from_backend(be, &id).await
}
/// Get all SnapshotFiles from the backend
pub async fn all_from_backend<B: DecryptReadBackend>(be: &B) -> Result<Vec<Self>> {
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<B: DecryptReadBackend>(
be: &B,
filter: &SnapshotFilter,
crit: &SnapshotGroupCriterion,
) -> Result<Vec<(SnapshotGroup, Vec<Self>)>> {
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<B: DecryptReadBackend>(
be: &B,
filter: &SnapshotFilter,
) -> Result<Vec<Self>> {
Ok(be
.stream_all::<SnapshotFile>(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<String>);
impl FromStr for StringList {