From fa28427126e8cf51f87fced229ea81a021b7cb2f Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Tue, 21 Mar 2023 22:08:38 +0100 Subject: [PATCH] Add scriptable snapshot filter option --- Cargo.lock | 99 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +- changelog/new.txt | 1 + src/backend/rclone.rs | 2 +- src/repofile/snapshotfile.rs | 39 ++++++++++++++ 5 files changed, 142 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9ce2dc..72cef2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "const-random", + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -326,6 +339,28 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +dependencies = [ + "getrandom", + "once_cell", + "proc-macro-hack", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -424,6 +459,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1497,6 +1538,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.52" @@ -1676,6 +1723,33 @@ dependencies = [ "winreg", ] +[[package]] +name = "rhai" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd29fa1f740be6dc91982013957e08c3c4232d7efcfe19e12da87d50bad47758" +dependencies = [ + "ahash", + "bitflags", + "instant", + "num-traits", + "rhai_codegen", + "serde", + "smallvec", + "smartstring", +] + +[[package]] +name = "rhai_codegen" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db74e3fdd29d969a0ec1f8e79171a6f0f71d0429293656901db382d248c4c021" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ring" version = "0.16.20" @@ -1793,6 +1867,7 @@ dependencies = [ "rand", "rayon", "reqwest", + "rhai", "rpassword", "rstest", "scrypt", @@ -2093,6 +2168,21 @@ name = "smallvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +dependencies = [ + "serde", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "serde", + "static_assertions", + "version_check", +] [[package]] name = "socket2" @@ -2267,6 +2357,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index e6ab439..518c4bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,8 @@ humantime = "2" itertools = "0.10" simplelog = "0.12" comfy-table = "6.1.4" -libc="0.2" +libc = "0.2" +rhai = {version = "1.13", features = ["sync", "serde", "no_optimize", "no_module", "no_custom_syntax", "only_i64"]} [target.'cfg(not(windows))'.dependencies] users = "0.11" diff --git a/changelog/new.txt b/changelog/new.txt index cf290f1..2412d70 100644 --- a/changelog/new.txt +++ b/changelog/new.txt @@ -9,6 +9,7 @@ Bugs fixed: New features: - Experimental windows support has been added. +- New option --filter-fn allows to implement your own snapshot filter using the Rhai language. - New command dump has been added. - New command merge has been added. - Extra or wrong fields in the config file now lead to rustic complaining and aborting. diff --git a/src/backend/rclone.rs b/src/backend/rclone.rs index c56674c..4a6ff64 100644 --- a/src/backend/rclone.rs +++ b/src/backend/rclone.rs @@ -114,7 +114,7 @@ impl RcloneBackend { bail!("url must start with http://! url: {url}"); } - let url = "http://".to_string() + &user + ":" + &password + "@" + &url[7..]; + let url = "http://".to_string() + user.as_str() + ":" + password.as_str() + "@" + &url[7..]; debug!("using REST backend with url {url}."); let rest = RestBackend::new(&url)?; diff --git a/src/repofile/snapshotfile.rs b/src/repofile/snapshotfile.rs index 0165464..8ef2ef5 100644 --- a/src/repofile/snapshotfile.rs +++ b/src/repofile/snapshotfile.rs @@ -14,6 +14,8 @@ use itertools::Itertools; use log::*; use merge::Merge; use path_dedot::ParseDot; +use rhai::serde::to_dynamic; +use rhai::{Dynamic, Engine, FnPtr, AST}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; @@ -339,6 +341,19 @@ impl SnapshotFile { } pub fn matches(&self, filter: &SnapshotFilter) -> bool { + if let Some(filter_fn) = &filter.filter_fn { + match filter_fn.call::(self) { + Ok(result) => { + if !result { + return false; + } + } + Err(err) => { + warn!("Error evaluating filter-fn for snapshot {}: {err}", self.id); + } + } + } + self.paths.matches(&filter.filter_paths) && self.tags.matches(&filter.filter_tags) && (filter.filter_host.is_empty() || filter.filter_host.contains(&self.hostname)) @@ -404,6 +419,25 @@ impl Ord for SnapshotFile { } } +struct SnapshotFn(FnPtr, AST); +impl FromStr for SnapshotFn { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + let engine = Engine::new(); + let ast = engine.compile(s)?; + let func = engine.eval_ast::(&ast)?; + Ok(Self(func, ast)) + } +} + +impl SnapshotFn { + fn call(&self, sn: &SnapshotFile) -> Result { + let engine = Engine::new(); + let sn: Dynamic = to_dynamic(sn)?; + Ok(self.0.call::(&engine, &self.1, (sn,))?) + } +} + #[serde_as] #[derive(Default, Parser, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] @@ -429,6 +463,11 @@ pub struct SnapshotFilter { #[serde_as(as = "Vec")] #[merge(strategy=merge::vec::overwrite_empty)] filter_tags: Vec, + + /// Function to filter snapshots + #[clap(long, value_name = "FUNC")] + #[serde_as(as = "Option")] + filter_fn: Option, } #[derive(Clone, Default, Deserialize)]