#[cfg(feature = "rhai")]
use crate::error::RhaiErrorKinds;
#[cfg(feature = "rhai")]
use std::error::Error;
use std::{
fmt::{Debug, Display},
str::FromStr,
};
#[cfg(feature = "jq")]
use anyhow::{anyhow, bail};
use bytesize::ByteSize;
use derive_more::derive::Display;
use jiff::{Zoned, civil::Time, tz::TimeZone};
use log::warn;
use rustic_core::{
StringList,
repofile::{RusticTime, SnapshotFile},
};
use cached::proc_macro::cached;
use conflate::Merge;
#[cfg(feature = "jq")]
use jaq_core::{
Compiler, Ctx, Filter, Native, RcIter,
load::{Arena, File, Loader},
};
#[cfg(feature = "jq")]
use jaq_json::Val;
#[cfg(feature = "rhai")]
use rhai::{AST, Dynamic, Engine, FnPtr, serde::to_dynamic};
use serde::{Deserialize, Serialize};
#[cfg(feature = "jq")]
use serde_json::Value;
use serde_with::{DisplayFromStr, serde_as};
#[cfg(feature = "rhai")]
#[derive(Clone, Debug)]
pub(crate) struct SnapshotFn(FnPtr, AST);
#[cfg(feature = "rhai")]
impl FromStr for SnapshotFn {
type Err = RhaiErrorKinds;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let engine = Engine::new();
let ast = engine.compile(s)?;
let func = engine.eval_ast::<FnPtr>(&ast)?;
Ok(Self(func, ast))
}
}
#[cfg(feature = "rhai")]
impl SnapshotFn {
fn call<T: Clone + Send + Sync + 'static>(
&self,
sn: &SnapshotFile,
) -> Result<T, Box<dyn Error>> {
let engine = Engine::new();
let sn: Dynamic = to_dynamic(sn)?;
Ok(self.0.call::<T>(&engine, &self.1, (sn,))?)
}
}
#[cfg(feature = "rhai")]
#[cached(key = "String", convert = r#"{ s.to_string() }"#, size = 1)]
fn string_to_fn(s: &str) -> Option<SnapshotFn> {
match SnapshotFn::from_str(s) {
Ok(filter_fn) => Some(filter_fn),
Err(err) => {
warn!("Error evaluating filter-fn {s}: {err}",);
None
}
}
}
#[cfg(feature = "jq")]
#[derive(Clone)]
pub(crate) struct SnapshotJq(Filter<Native<Val>>);
#[cfg(feature = "jq")]
impl FromStr for SnapshotJq {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let program = File { code: s, path: () };
let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs()));
let arena = Arena::default();
let modules = loader
.load(&arena, program)
.map_err(|errs| anyhow!("errors loading modules in jq: {errs:?}"))?;
let filter = Compiler::<_, Native<_>>::default()
.with_funs(jaq_std::funs().chain(jaq_json::funs()))
.compile(modules)
.map_err(|errs| anyhow!("errors during compiling filters in jq: {errs:?}"))?;
Ok(Self(filter))
}
}
#[cfg(feature = "jq")]
impl SnapshotJq {
fn call(&self, snap: &SnapshotFile) -> Result<bool, anyhow::Error> {
let input = serde_json::to_value(snap)?;
let inputs = RcIter::new(core::iter::empty());
let res = self.0.run((Ctx::new([], &inputs), Val::from(input))).next();
match res {
Some(Ok(val)) => {
let val: Value = val.into();
match val.as_bool() {
Some(true) => Ok(true),
Some(false) => Ok(false),
None => bail!("expression does not return bool"),
}
}
_ => bail!("expression does not return bool"),
}
}
}
#[cfg(feature = "jq")]
#[cached(key = "String", convert = r#"{ s.to_string() }"#, size = 1)]
fn string_to_jq(s: &str) -> Option<SnapshotJq> {
match SnapshotJq::from_str(s) {
Ok(filter_jq) => Some(filter_jq),
Err(err) => {
warn!("Error evaluating filter-fn {s}: {err}",);
None
}
}
}
#[serde_as]
#[derive(Clone, Default, Debug, Serialize, Deserialize, Merge, clap::Parser)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct SnapshotFilter {
#[clap(long = "filter-host", global = true, value_name = "HOSTNAME")]
#[merge(strategy=conflate::vec::overwrite_empty)]
filter_hosts: Vec<String>,
#[clap(long = "filter-label", global = true, value_name = "LABEL")]
#[merge(strategy=conflate::vec::overwrite_empty)]
filter_labels: Vec<String>,
#[clap(long, global = true, value_name = "PATH[,PATH,..]")]
#[serde_as(as = "Vec<DisplayFromStr>")]
#[merge(strategy=conflate::vec::overwrite_empty)]
filter_paths: Vec<StringList>,
#[clap(long, global = true, value_name = "PATH[,PATH,..]")]
#[serde_as(as = "Vec<DisplayFromStr>")]
#[merge(strategy=conflate::vec::overwrite_empty)]
filter_paths_exact: Vec<StringList>,
#[clap(long, global = true, value_name = "TAG[,TAG,..]")]
#[serde_as(as = "Vec<DisplayFromStr>")]
#[merge(strategy=conflate::vec::overwrite_empty)]
filter_tags: Vec<StringList>,
#[clap(long, global = true, value_name = "TAG[,TAG,..]")]
#[serde_as(as = "Vec<DisplayFromStr>")]
#[merge(strategy=conflate::vec::overwrite_empty)]
filter_tags_exact: Vec<StringList>,
#[serde_as(as = "Option<DisplayFromStr>")]
#[clap(long, global = true, value_name = "DATE(TIME)")]
#[merge(strategy=conflate::option::overwrite_none)]
filter_after: Option<AfterDate>,
#[serde_as(as = "Option<DisplayFromStr>")]
#[clap(long, global = true, value_name = "DATE(TIME)")]
#[merge(strategy=conflate::option::overwrite_none)]
filter_before: Option<BeforeDate>,
#[serde_as(as = "Option<DisplayFromStr>")]
#[clap(long, global = true, value_name = "SIZE")]
#[merge(strategy=conflate::option::overwrite_none)]
filter_size: Option<SizeRange>,
#[serde_as(as = "Option<DisplayFromStr>")]
#[clap(long, global = true, value_name = "SIZE")]
#[merge(strategy=conflate::option::overwrite_none)]
filter_size_added: Option<SizeRange>,
#[serde_as(as = "Option<DisplayFromStr>")]
#[clap(long, global = true, value_name = "COUNT")]
#[merge(strategy=conflate::option::overwrite_none)]
filter_last: Option<usize>,
#[cfg(feature = "rhai")]
#[clap(long, global = true, value_name = "FUNC")]
#[serde_as(as = "Option<DisplayFromStr>")]
#[merge(strategy=conflate::option::overwrite_none)]
filter_fn: Option<String>,
#[cfg(feature = "jq")]
#[clap(long, global = true, value_name = "JQ")]
#[serde_as(as = "Option<DisplayFromStr>")]
#[merge(strategy=conflate::option::overwrite_none)]
filter_jq: Option<String>,
}
impl SnapshotFilter {
#[must_use]
pub fn matches(&self, snapshot: &SnapshotFile) -> bool {
#[cfg(feature = "rhai")]
if let Some(filter_fn) = &self.filter_fn
&& let Some(func) = string_to_fn(filter_fn)
{
match func.call::<bool>(snapshot) {
Ok(result) => {
if !result {
return false;
}
}
Err(err) => {
warn!(
"Error evaluating filter-fn for snapshot {}: {err}",
snapshot.id
);
return false;
}
}
}
#[cfg(feature = "jq")]
if let Some(filter_jq) = &self.filter_jq
&& let Some(jq) = string_to_jq(filter_jq)
{
match jq.call(snapshot) {
Ok(result) => {
if !result {
return false;
}
}
Err(err) => {
warn!(
"Error evaluating filter-jq for snapshot {}: {err}",
snapshot.id
);
return false;
}
}
}
if matches!(&self.filter_after, Some(after) if !after.matches(&snapshot.time))
|| matches!(&self.filter_before, Some(before) if !before.matches(&snapshot.time))
|| matches!((&self.filter_size,&snapshot.summary), (Some(size),Some(summary)) if !size.matches(summary.total_bytes_processed))
|| matches!((&self.filter_size_added,&snapshot.summary), (Some(size),Some(summary)) if !size.matches(summary.data_added))
{
return false;
}
snapshot.paths.matches(&self.filter_paths)
&& snapshot.tags.matches(&self.filter_tags)
&& (self.filter_paths_exact.is_empty()
|| self.filter_paths_exact.contains(&snapshot.paths))
&& (self.filter_tags_exact.is_empty()
|| self.filter_tags_exact.contains(&snapshot.tags))
&& (self.filter_hosts.is_empty() || self.filter_hosts.contains(&snapshot.hostname))
&& (self.filter_labels.is_empty() || self.filter_labels.contains(&snapshot.label))
}
pub fn post_process(&self, snapshots: &mut Vec<SnapshotFile>) {
snapshots.sort_unstable();
if let Some(last) = self.filter_last {
let count = snapshots.len();
if last < count {
let new = snapshots.split_off(count - last);
let _ = std::mem::replace(snapshots, new);
}
}
}
}
#[derive(Debug, Clone, Display)]
struct AfterDate(Zoned);
impl AfterDate {
fn matches(&self, datetime: &Zoned) -> bool {
&self.0 < datetime
}
}
impl FromStr for AfterDate {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(RusticTime::parse(s, Time::MAX, TimeZone::system())?))
}
}
#[derive(Debug, Clone, Display)]
struct BeforeDate(Zoned);
impl BeforeDate {
fn matches(&self, datetime: &Zoned) -> bool {
datetime < self.0
}
}
impl FromStr for BeforeDate {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(RusticTime::parse_system(s)?))
}
}
#[derive(Debug, Clone)]
struct SizeRange {
from: Option<ByteSize>,
to: Option<ByteSize>,
}
impl SizeRange {
fn matches(&self, size: u64) -> bool {
!matches!(self.from, Some(from) if size < from.0)
&& !matches!(self.to, Some(to) if size > to.0)
}
}
fn parse_size(s: &str) -> Result<Option<ByteSize>, String> {
let s = s.trim();
if s.is_empty() {
return Ok(None);
}
Ok(Some(s.parse()?))
}
impl FromStr for SizeRange {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (from, to) = match s.split_once("..") {
Some((s1, s2)) => (parse_size(s1)?, parse_size(s2)?),
None => (parse_size(s)?, None),
};
Ok(Self { from, to })
}
}
impl Display for SizeRange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(from) = self.from {
Display::fmt(&from.display(), f)?;
}
f.write_str("..")?;
if let Some(to) = self.to {
Display::fmt(&to.display(), f)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case("..", None, None)]
#[case("10", Some(10), None)]
#[case("..10k", None, Some(10_000))]
#[case("1MB..", Some(1_000_000), None)]
#[case("1 MB .. 1 GiB", Some(1_000_000), Some(1_073_741_824))]
#[case("10 .. 20 ", Some(10), Some(20))]
#[case(" 2G ", Some(2_000_000_000), None)]
fn size_range_from_str(
#[case] input: SizeRange,
#[case] from: Option<u64>,
#[case] to: Option<u64>,
) {
assert_eq!(input.from.map(|v| v.0), from);
assert_eq!(input.to.map(|v| v.0), to);
}
}