diff --git a/crates/rustic_core/examples/forget.rs b/crates/rustic_core/examples/forget.rs new file mode 100644 index 000000000..34af19b2c --- /dev/null +++ b/crates/rustic_core/examples/forget.rs @@ -0,0 +1,24 @@ +//! `forget` example +use rustic_core::{KeepOptions, Repository, RepositoryOptions, SnapshotGroupCriterion}; +use simplelog::{Config, LevelFilter, SimpleLogger}; + +fn main() { + // Display info logs + let _ = SimpleLogger::init(LevelFilter::Info, Config::default()); + + // Open repository + let mut repo_opts = RepositoryOptions::default(); + repo_opts.repository = Some("/tmp/repo".to_string()); + repo_opts.password = Some("test".to_string()); + let repo = Repository::new(&repo_opts).unwrap().open().unwrap(); + + // Check respository with standard options + let group_by = SnapshotGroupCriterion::default(); + let mut keep = KeepOptions::default(); + keep.keep_daily = 5; + keep.keep_weekly = 10; + let snaps = repo + .get_forget_snapshots(&keep, group_by, |_| true) + .unwrap(); + println!("{snaps:?}"); +} diff --git a/crates/rustic_core/src/commands.rs b/crates/rustic_core/src/commands.rs index 100d85284..f1720ec51 100644 --- a/crates/rustic_core/src/commands.rs +++ b/crates/rustic_core/src/commands.rs @@ -1,5 +1,6 @@ pub mod cat; pub mod check; +pub mod forget; pub mod prune; pub mod repoinfo; pub mod snapshots; diff --git a/crates/rustic_core/src/commands/forget.rs b/crates/rustic_core/src/commands/forget.rs new file mode 100644 index 000000000..aaeba767a --- /dev/null +++ b/crates/rustic_core/src/commands/forget.rs @@ -0,0 +1,412 @@ +//! `forget` subcommand + +use chrono::{DateTime, Datelike, Duration, Local, Timelike}; +use derivative::Derivative; +use serde::Deserialize; +use serde_with::{serde_as, DisplayFromStr}; + +use crate::{ + Id, OpenRepository, ProgressBars, RusticResult, SnapshotFile, SnapshotGroup, + SnapshotGroupCriterion, StringList, +}; + +type CheckFunction = fn(&SnapshotFile, &SnapshotFile) -> bool; + +#[derive(Debug)] +pub struct ForgetGroups(pub Vec); + +#[derive(Debug)] +pub struct ForgetGroup { + pub group: SnapshotGroup, + pub snapshots: Vec, +} + +#[derive(Debug)] +pub struct ForgetSnapshot { + pub snapshot: SnapshotFile, + pub keep: bool, + pub reasons: Vec, +} + +impl ForgetGroups { + pub fn into_forget_ids(self) -> Vec { + self.0 + .into_iter() + .flat_map(|fg| { + fg.snapshots + .into_iter() + .filter_map(|fsn| (!fsn.keep).then_some(fsn.snapshot.id)) + }) + .collect() + } +} + +pub(crate) fn get_forget_snapshots( + repo: &OpenRepository

, + keep: &KeepOptions, + group_by: SnapshotGroupCriterion, + filter: impl FnMut(&SnapshotFile) -> bool, +) -> RusticResult { + let now = Local::now(); + + let groups = repo + .get_snapshot_group(&[], group_by, filter)? + .into_iter() + .map(|(group, snapshots)| ForgetGroup { + group, + snapshots: keep.apply(snapshots, now), + }) + .collect(); + + Ok(ForgetGroups(groups)) +} + +#[cfg_attr(feature = "clap", derive(clap::Parser))] +#[cfg_attr(feature = "merge", derive(merge::Merge))] +#[serde_as] +#[derive(Clone, Debug, PartialEq, Eq, Derivative, Deserialize)] +#[derivative(Default)] +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] +#[non_exhaustive] +pub struct KeepOptions { + /// Keep snapshots with this taglist (can be specified multiple times) + #[cfg_attr(feature = "clap", clap(long, value_name = "TAG[,TAG,..]"))] + #[serde_as(as = "Vec")] + #[cfg_attr(feature = "merge", merge(strategy=merge::vec::overwrite_empty))] + pub keep_tags: Vec, + + /// Keep snapshots ids that start with ID (can be specified multiple times) + #[cfg_attr(feature = "clap", clap(long = "keep-id", value_name = "ID"))] + #[cfg_attr(feature = "merge", merge(strategy=merge::vec::overwrite_empty))] + pub keep_ids: Vec, + + /// Keep the last N snapshots (N == -1: keep all snapshots) + #[cfg_attr( + feature = "clap", + clap(long, short = 'l', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..)) + )] + #[cfg_attr(feature = "merge", merge(strategy=merge::num::overwrite_zero))] + pub keep_last: i32, + + /// Keep the last N hourly snapshots (N == -1: keep all hourly snapshots) + #[cfg_attr( + feature = "clap", + clap(long, short = 'H', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..)) + )] + #[cfg_attr(feature = "merge", merge(strategy=merge::num::overwrite_zero))] + pub keep_hourly: i32, + + /// Keep the last N daily snapshots (N == -1: keep all daily snapshots) + #[cfg_attr( + feature = "clap", + clap(long, short = 'd', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..)) + )] + #[cfg_attr(feature = "merge", merge(strategy=merge::num::overwrite_zero))] + pub keep_daily: i32, + + /// Keep the last N weekly snapshots (N == -1: keep all weekly snapshots) + #[cfg_attr( + feature = "clap", + clap(long, short = 'w', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..)) + )] + #[cfg_attr(feature = "merge", merge(strategy=merge::num::overwrite_zero))] + pub keep_weekly: i32, + + /// Keep the last N monthly snapshots (N == -1: keep all monthly snapshots) + #[cfg_attr( + feature = "clap", + clap(long, short = 'm', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..)) + )] + #[cfg_attr(feature = "merge", merge(strategy=merge::num::overwrite_zero))] + pub keep_monthly: i32, + + /// Keep the last N quarter-yearly snapshots (N == -1: keep all quarter-yearly snapshots) + #[cfg_attr( + feature = "clap", + clap(long, value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..)) + )] + #[cfg_attr(feature = "merge", merge(strategy=merge::num::overwrite_zero))] + pub keep_quarter_yearly: i32, + + /// Keep the last N half-yearly snapshots (N == -1: keep all half-yearly snapshots) + #[cfg_attr( + feature = "clap", + clap(long, value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..)) + )] + #[cfg_attr(feature = "merge", merge(strategy=merge::num::overwrite_zero))] + pub keep_half_yearly: i32, + + /// Keep the last N yearly snapshots (N == -1: keep all yearly snapshots) + #[cfg_attr( + feature = "clap", + clap(long, short = 'y', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..)) + )] + #[cfg_attr(feature = "merge", merge(strategy=merge::num::overwrite_zero))] + pub keep_yearly: i32, + + /// Keep snapshots newer than DURATION relative to latest snapshot + #[cfg_attr( + feature = "clap", + clap(long, value_name = "DURATION", default_value = "0h") + )] + #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] + #[serde_as(as = "DisplayFromStr")] + #[cfg_attr(feature = "merge", merge(strategy=overwrite_zero_duration))] + pub keep_within: humantime::Duration, + + /// Keep hourly snapshots newer than DURATION relative to latest snapshot + #[cfg_attr( + feature = "clap", + clap(long, value_name = "DURATION", default_value = "0h") + )] + #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] + #[serde_as(as = "DisplayFromStr")] + #[cfg_attr(feature = "merge", merge(strategy=overwrite_zero_duration))] + pub keep_within_hourly: humantime::Duration, + + /// Keep daily snapshots newer than DURATION relative to latest snapshot + #[cfg_attr( + feature = "clap", + clap(long, value_name = "DURATION", default_value = "0d") + )] + #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] + #[serde_as(as = "DisplayFromStr")] + #[cfg_attr(feature = "merge", merge(strategy=overwrite_zero_duration))] + pub keep_within_daily: humantime::Duration, + + /// Keep weekly snapshots newer than DURATION relative to latest snapshot + #[cfg_attr( + feature = "clap", + clap(long, value_name = "DURATION", default_value = "0w") + )] + #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] + #[serde_as(as = "DisplayFromStr")] + #[cfg_attr(feature = "merge", merge(strategy=overwrite_zero_duration))] + pub keep_within_weekly: humantime::Duration, + + /// Keep monthly snapshots newer than DURATION relative to latest snapshot + #[cfg_attr( + feature = "clap", + clap(long, value_name = "DURATION", default_value = "0m") + )] + #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] + #[serde_as(as = "DisplayFromStr")] + #[cfg_attr(feature = "merge", merge(strategy=overwrite_zero_duration))] + pub keep_within_monthly: humantime::Duration, + + /// Keep quarter-yearly snapshots newer than DURATION relative to latest snapshot + #[cfg_attr( + feature = "clap", + clap(long, value_name = "DURATION", default_value = "0y") + )] + #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] + #[serde_as(as = "DisplayFromStr")] + #[cfg_attr(feature = "merge", merge(strategy=overwrite_zero_duration))] + pub keep_within_quarter_yearly: humantime::Duration, + + /// Keep half-yearly snapshots newer than DURATION relative to latest snapshot + #[cfg_attr( + feature = "clap", + clap(long, value_name = "DURATION", default_value = "0y") + )] + #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] + #[serde_as(as = "DisplayFromStr")] + #[cfg_attr(feature = "merge", merge(strategy=overwrite_zero_duration))] + pub keep_within_half_yearly: humantime::Duration, + + /// Keep yearly snapshots newer than DURATION relative to latest snapshot + #[cfg_attr( + feature = "clap", + clap(long, value_name = "DURATION", default_value = "0y") + )] + #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] + #[serde_as(as = "DisplayFromStr")] + #[cfg_attr(feature = "merge", merge(strategy=overwrite_zero_duration))] + pub keep_within_yearly: humantime::Duration, +} + +#[cfg(feature = "merge")] +fn overwrite_zero_duration(left: &mut humantime::Duration, right: humantime::Duration) { + if *left == std::time::Duration::ZERO.into() { + *left = right; + } +} + +const fn always_false(_sn1: &SnapshotFile, _sn2: &SnapshotFile) -> bool { + false +} + +fn equal_year(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { + let (t1, t2) = (sn1.time, sn2.time); + t1.year() == t2.year() +} + +fn equal_half_year(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { + let (t1, t2) = (sn1.time, sn2.time); + t1.year() == t2.year() && t1.month0() / 6 == t2.month0() / 6 +} + +fn equal_quarter_year(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { + let (t1, t2) = (sn1.time, sn2.time); + t1.year() == t2.year() && t1.month0() / 3 == t2.month0() / 3 +} + +fn equal_month(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { + let (t1, t2) = (sn1.time, sn2.time); + t1.year() == t2.year() && t1.month() == t2.month() +} + +fn equal_week(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { + let (t1, t2) = (sn1.time, sn2.time); + t1.year() == t2.year() && t1.iso_week().week() == t2.iso_week().week() +} + +fn equal_day(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { + let (t1, t2) = (sn1.time, sn2.time); + t1.year() == t2.year() && t1.ordinal() == t2.ordinal() +} + +fn equal_hour(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { + let (t1, t2) = (sn1.time, sn2.time); + t1.year() == t2.year() && t1.ordinal() == t2.ordinal() && t1.hour() == t2.hour() +} + +impl KeepOptions { + fn matches( + &mut self, + sn: &SnapshotFile, + last: Option<&SnapshotFile>, + has_next: bool, + latest_time: DateTime, + ) -> Vec<&str> { + let mut reason = Vec::new(); + + let snapshot_id_hex = sn.id.to_hex(); + if self + .keep_ids + .iter() + .any(|id| snapshot_id_hex.starts_with(id)) + { + reason.push("id"); + } + + if !self.keep_tags.is_empty() && sn.tags.matches(&self.keep_tags) { + reason.push("tags"); + } + + let keep_checks: [(CheckFunction, &mut i32, &str, humantime::Duration, &str); 8] = [ + ( + always_false, + &mut self.keep_last, + "last", + self.keep_within, + "within", + ), + ( + equal_hour, + &mut self.keep_hourly, + "hourly", + self.keep_within_hourly, + "within hourly", + ), + ( + equal_day, + &mut self.keep_daily, + "daily", + self.keep_within_daily, + "within daily", + ), + ( + equal_week, + &mut self.keep_weekly, + "weekly", + self.keep_within_weekly, + "within weekly", + ), + ( + equal_month, + &mut self.keep_monthly, + "monthly", + self.keep_within_monthly, + "within monthly", + ), + ( + equal_quarter_year, + &mut self.keep_quarter_yearly, + "quarter-yearly", + self.keep_within_quarter_yearly, + "within quarter-yearly", + ), + ( + equal_half_year, + &mut self.keep_half_yearly, + "half-yearly", + self.keep_within_half_yearly, + "within half-yearly", + ), + ( + equal_year, + &mut self.keep_yearly, + "yearly", + self.keep_within_yearly, + "within yearly", + ), + ]; + + for (check_fun, counter, reason1, within, reason2) in keep_checks { + if !has_next || last.is_none() || !check_fun(sn, last.unwrap()) { + if *counter != 0 { + reason.push(reason1); + if *counter > 0 { + *counter -= 1; + } + } + if sn.time + Duration::from_std(*within).unwrap() > latest_time { + reason.push(reason2); + } + } + } + reason + } + + pub fn apply( + &self, + mut snapshots: Vec, + now: DateTime, + ) -> Vec { + let mut group_keep = self.clone(); + let mut snaps = Vec::new(); + if snapshots.is_empty() { + return snaps; + } + + snapshots.sort_unstable_by(|sn1, sn2| sn1.cmp(sn2).reverse()); + let latest_time = snapshots[0].time; + let mut last = None; + + let mut iter = snapshots.into_iter().peekable(); + + while let Some(sn) = iter.next() { + let (keep, reasons) = { + if sn.must_keep(now) { + (true, vec!["snapshot"]) + } else if sn.must_delete(now) { + (false, vec!["snapshot"]) + } else { + let reasons = + group_keep.matches(&sn, last.as_ref(), iter.peek().is_some(), latest_time); + let keep = !reasons.is_empty(); + (keep, reasons) + } + }; + last = Some(sn.clone()); + + snaps.push(ForgetSnapshot { + snapshot: sn, + keep, + reasons: reasons.iter().map(ToString::to_string).collect(), + }); + } + snaps + } +} diff --git a/crates/rustic_core/src/lib.rs b/crates/rustic_core/src/lib.rs index c0f9eada0..72ed03617 100644 --- a/crates/rustic_core/src/lib.rs +++ b/crates/rustic_core/src/lib.rs @@ -122,6 +122,7 @@ pub use crate::{ chunker::random_poly, commands::{ check::CheckOpts, + forget::{ForgetGroup, ForgetGroups, ForgetSnapshot, KeepOptions}, prune::{PruneOpts, PrunePlan, PruneStats}, repoinfo::{BlobInfo, IndexInfos, PackInfo, RepoFileInfo, RepoFileInfos}, }, diff --git a/crates/rustic_core/src/repofile/snapshotfile.rs b/crates/rustic_core/src/repofile/snapshotfile.rs index 5a5fc905f..323cd2168 100644 --- a/crates/rustic_core/src/repofile/snapshotfile.rs +++ b/crates/rustic_core/src/repofile/snapshotfile.rs @@ -469,7 +469,7 @@ impl Ord for SnapshotFile { } #[allow(clippy::struct_excessive_bools)] -#[derive(DeserializeFromStr, Clone, Default, Debug, Copy)] +#[derive(DeserializeFromStr, Clone, Debug, Copy)] pub struct SnapshotGroupCriterion { hostname: bool, label: bool, @@ -477,6 +477,17 @@ pub struct SnapshotGroupCriterion { tags: bool, } +impl Default for SnapshotGroupCriterion { + fn default() -> Self { + Self { + hostname: true, + label: true, + paths: true, + tags: false, + } + } +} + impl FromStr for SnapshotGroupCriterion { type Err = RusticError; fn from_str(s: &str) -> RusticResult { diff --git a/crates/rustic_core/src/repository.rs b/crates/rustic_core/src/repository.rs index de8cf31ae..fd24a9c0c 100644 --- a/crates/rustic_core/src/repository.rs +++ b/crates/rustic_core/src/repository.rs @@ -31,6 +31,7 @@ use crate::{ commands::{ self, check::CheckOpts, + forget::{ForgetGroups, KeepOptions}, repoinfo::{IndexInfos, RepoFileInfos}, }, crypto::aespoly1305::Key, @@ -388,6 +389,27 @@ impl OpenRepository

{ commands::snapshots::get_snapshot_group(self, ids, group_by, filter) } + pub fn get_snapshots(&self, ids: &[String]) -> RusticResult> { + let p = self.pb.progress_counter("getting snapshots..."); + SnapshotFile::from_ids(&self.dbe, ids, &p) + } + + pub fn get_forget_snapshots( + &self, + keep: &KeepOptions, + group_by: SnapshotGroupCriterion, + filter: impl FnMut(&SnapshotFile) -> bool, + ) -> RusticResult { + commands::forget::get_forget_snapshots(self, keep, group_by, filter) + } + + pub fn delete_snapshots(&self, ids: &[Id]) -> RusticResult<()> { + let p = self.pb.progress_counter("removing snapshots..."); + self.dbe + .delete_list(FileType::Snapshot, true, ids.iter(), p)?; + Ok(()) + } + pub fn cat_file(&self, tpe: FileType, id: &str) -> RusticResult { commands::cat::cat_file(self, tpe, id) } diff --git a/src/commands/forget.rs b/src/commands/forget.rs index 94e8640aa..1313eefab 100644 --- a/src/commands/forget.rs +++ b/src/commands/forget.rs @@ -3,20 +3,14 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. use crate::{ - commands::{get_repository, open_repository}, - helpers::table_with_titles, - status_err, Application, RusticConfig, RUSTIC_APP, + commands::get_repository, helpers::table_with_titles, status_err, Application, RusticConfig, + RUSTIC_APP, }; use abscissa_core::{config::Override, Shutdown}; use abscissa_core::{Command, FrameworkError, Runnable}; use anyhow::Result; -use std::str::FromStr; - -use chrono::{DateTime, Datelike, Duration, Local, Timelike}; - -use derivative::Derivative; use merge::Merge; use serde::Deserialize; use serde_with::{serde_as, DisplayFromStr}; @@ -24,12 +18,9 @@ use serde_with::{serde_as, DisplayFromStr}; use crate::{commands::prune::PruneCmd, filtering::SnapshotFilter}; use rustic_core::{ - DecryptWriteBackend, FileType, ProgressBars, SnapshotFile, SnapshotGroup, - SnapshotGroupCriterion, StringList, + ForgetGroup, ForgetGroups, ForgetSnapshot, KeepOptions, SnapshotGroup, SnapshotGroupCriterion, }; -type CheckFunction = fn(&SnapshotFile, &SnapshotFile) -> bool; - /// `forget` subcommand #[derive(clap::Parser, Command, Debug)] pub(super) struct ForgetCmd { @@ -97,78 +88,49 @@ impl Runnable for ForgetCmd { impl ForgetCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let progress_options = &config.global.progress_options; - - let repo = open_repository(get_repository(&config)); + let repo = get_repository(&config).open()?; - let be = &repo.dbe; + let group_by = config.forget.group_by.unwrap_or_default(); - let group_by = config - .forget - .group_by - .unwrap_or_else(|| SnapshotGroupCriterion::from_str("host,label,paths").unwrap()); - - let p = progress_options.progress_hidden(); let groups = if self.ids.is_empty() { - SnapshotFile::group_from_backend( - be, - |sn| config.forget.filter.matches(sn), - group_by, - &p, - )? + repo.get_forget_snapshots(&config.forget.keep, group_by, |sn| { + config.forget.filter.matches(sn) + })? } else { - let item = ( - SnapshotGroup::default(), - SnapshotFile::from_ids(be, &self.ids, &p)?, - ); - vec![item] + let item = ForgetGroup { + group: SnapshotGroup::default(), + snapshots: repo + .get_snapshots(&self.ids)? + .into_iter() + .map(|sn| ForgetSnapshot { + snapshot: sn, + keep: true, + reasons: vec!["id argument".to_string()], + }) + .collect(), + }; + ForgetGroups(vec![item]) }; - let mut forget_snaps = Vec::new(); - for (group, mut snapshots) in groups { + for ForgetGroup { group, snapshots } in &groups.0 { if !group.is_empty() { println!("snapshots for {group}"); } - snapshots.sort_unstable_by(|sn1, sn2| sn1.cmp(sn2).reverse()); - let latest_time = snapshots[0].time; - let mut group_keep = config.forget.keep.clone(); let mut table = table_with_titles([ "ID", "Time", "Host", "Label", "Tags", "Paths", "Action", "Reason", ]); - let mut iter = snapshots.iter().peekable(); - let mut last = None; - let now = Local::now(); - // snapshots that have no reason to be kept are removed. The only exception - // is if no IDs are explicitly given and no keep option is set. In this - // case, the default is to keep the snapshots. - let default_keep = self.ids.is_empty() && group_keep == KeepOptions::default(); - - while let Some(sn) = iter.next() { - let (action, reason) = { - if sn.must_keep(now) { - ("keep", "snapshot".to_string()) - } else if sn.must_delete(now) { - forget_snaps.push(sn.id); - ("remove", "snapshot".to_string()) - } else if !self.ids.is_empty() { - forget_snaps.push(sn.id); - ("remove", "id argument".to_string()) - } else { - match group_keep.matches(sn, last, iter.peek().is_some(), latest_time) { - None if default_keep => ("keep", String::new()), - None => { - forget_snaps.push(sn.id); - ("remove", String::new()) - } - Some(reason) => ("keep", reason), - } - } - }; - + for ForgetSnapshot { + snapshot: sn, + keep, + reasons, + } in snapshots + { + let time = sn.time.format("%Y-%m-%d %H:%M:%S").to_string(); let tags = sn.tags.formatln(); let paths = sn.paths.formatln(); - let time = sn.time.format("%Y-%m-%d %H:%M:%S").to_string(); + let action = if *keep { "keep" } else { "remove" }; + let reason = reasons.join("\n"); _ = table.add_row([ &sn.id.to_string(), &time, @@ -179,8 +141,6 @@ impl ForgetCmd { action, &reason, ]); - - last = Some(sn); } println!(); @@ -188,14 +148,15 @@ impl ForgetCmd { println!(); } + let forget_snaps = groups.into_forget_ids(); + match (forget_snaps.is_empty(), config.global.dry_run) { (true, _) => println!("nothing to remove"), (false, true) => { println!("would have removed the following snapshots:\n {forget_snaps:?}"); } (false, false) => { - let p = progress_options.progress_counter("removing snapshots..."); - be.delete_list(FileType::Snapshot, true, forget_snaps.iter(), p)?; + repo.delete_snapshots(&forget_snaps)?; } } @@ -208,266 +169,3 @@ impl ForgetCmd { Ok(()) } } - -#[serde_as] -#[derive(Clone, Debug, PartialEq, Derivative, clap::Parser, Deserialize, Merge)] -#[derivative(Default)] -#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] -pub(super) struct KeepOptions { - /// Keep snapshots with this taglist (can be specified multiple times) - #[clap(long, value_name = "TAG[,TAG,..]")] - #[serde_as(as = "Vec")] - #[merge(strategy=merge::vec::overwrite_empty)] - keep_tags: Vec, - - /// Keep snapshots ids that start with ID (can be specified multiple times) - #[clap(long = "keep-id", value_name = "ID")] - #[merge(strategy=merge::vec::overwrite_empty)] - keep_ids: Vec, - - /// Keep the last N snapshots (N == -1: keep all snapshots) - #[clap(long, short = 'l', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))] - #[merge(strategy=merge::num::overwrite_zero)] - keep_last: i32, - - /// Keep the last N hourly snapshots (N == -1: keep all hourly snapshots) - #[clap(long, short = 'H', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))] - #[merge(strategy=merge::num::overwrite_zero)] - keep_hourly: i32, - - /// Keep the last N daily snapshots (N == -1: keep all daily snapshots) - #[clap(long, short = 'd', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))] - #[merge(strategy=merge::num::overwrite_zero)] - keep_daily: i32, - - /// Keep the last N weekly snapshots (N == -1: keep all weekly snapshots) - #[clap(long, short = 'w', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))] - #[merge(strategy=merge::num::overwrite_zero)] - keep_weekly: i32, - - /// Keep the last N monthly snapshots (N == -1: keep all monthly snapshots) - #[clap(long, short = 'm', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))] - #[merge(strategy=merge::num::overwrite_zero)] - keep_monthly: i32, - - /// Keep the last N quarter-yearly snapshots (N == -1: keep all quarter-yearly snapshots) - #[clap(long, value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))] - #[merge(strategy=merge::num::overwrite_zero)] - keep_quarter_yearly: i32, - - /// Keep the last N half-yearly snapshots (N == -1: keep all half-yearly snapshots) - #[clap(long, value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))] - #[merge(strategy=merge::num::overwrite_zero)] - keep_half_yearly: i32, - - /// Keep the last N yearly snapshots (N == -1: keep all yearly snapshots) - #[clap(long, short = 'y', value_name = "N", default_value = "0", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))] - #[merge(strategy=merge::num::overwrite_zero)] - keep_yearly: i32, - - /// Keep snapshots newer than DURATION relative to latest snapshot - #[clap(long, value_name = "DURATION", default_value = "0h")] - #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] - #[serde_as(as = "DisplayFromStr")] - #[merge(strategy=overwrite_zero_duration)] - keep_within: humantime::Duration, - - /// Keep hourly snapshots newer than DURATION relative to latest snapshot - #[clap(long, value_name = "DURATION", default_value = "0h")] - #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] - #[serde_as(as = "DisplayFromStr")] - #[merge(strategy=overwrite_zero_duration)] - keep_within_hourly: humantime::Duration, - - /// Keep daily snapshots newer than DURATION relative to latest snapshot - #[clap(long, value_name = "DURATION", default_value = "0d")] - #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] - #[serde_as(as = "DisplayFromStr")] - #[merge(strategy=overwrite_zero_duration)] - keep_within_daily: humantime::Duration, - - /// Keep weekly snapshots newer than DURATION relative to latest snapshot - #[clap(long, value_name = "DURATION", default_value = "0w")] - #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] - #[serde_as(as = "DisplayFromStr")] - #[merge(strategy=overwrite_zero_duration)] - keep_within_weekly: humantime::Duration, - - /// Keep monthly snapshots newer than DURATION relative to latest snapshot - #[clap(long, value_name = "DURATION", default_value = "0m")] - #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] - #[serde_as(as = "DisplayFromStr")] - #[merge(strategy=overwrite_zero_duration)] - keep_within_monthly: humantime::Duration, - - /// Keep quarter-yearly snapshots newer than DURATION relative to latest snapshot - #[clap(long, value_name = "DURATION", default_value = "0y")] - #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] - #[serde_as(as = "DisplayFromStr")] - #[merge(strategy=overwrite_zero_duration)] - keep_within_quarter_yearly: humantime::Duration, - - /// Keep half-yearly snapshots newer than DURATION relative to latest snapshot - #[clap(long, value_name = "DURATION", default_value = "0y")] - #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] - #[serde_as(as = "DisplayFromStr")] - #[merge(strategy=overwrite_zero_duration)] - keep_within_half_yearly: humantime::Duration, - - /// Keep yearly snapshots newer than DURATION relative to latest snapshot - #[clap(long, value_name = "DURATION", default_value = "0y")] - #[derivative(Default(value = "std::time::Duration::ZERO.into()"))] - #[serde_as(as = "DisplayFromStr")] - #[merge(strategy=overwrite_zero_duration)] - keep_within_yearly: humantime::Duration, -} - -fn overwrite_zero_duration(left: &mut humantime::Duration, right: humantime::Duration) { - if *left == std::time::Duration::ZERO.into() { - *left = right; - } -} - -const fn always_false(_sn1: &SnapshotFile, _sn2: &SnapshotFile) -> bool { - false -} - -fn equal_year(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { - let (t1, t2) = (sn1.time, sn2.time); - t1.year() == t2.year() -} - -fn equal_half_year(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { - let (t1, t2) = (sn1.time, sn2.time); - t1.year() == t2.year() && t1.month0() / 6 == t2.month0() / 6 -} - -fn equal_quarter_year(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { - let (t1, t2) = (sn1.time, sn2.time); - t1.year() == t2.year() && t1.month0() / 3 == t2.month0() / 3 -} - -fn equal_month(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { - let (t1, t2) = (sn1.time, sn2.time); - t1.year() == t2.year() && t1.month() == t2.month() -} - -fn equal_week(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { - let (t1, t2) = (sn1.time, sn2.time); - t1.year() == t2.year() && t1.iso_week().week() == t2.iso_week().week() -} - -fn equal_day(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { - let (t1, t2) = (sn1.time, sn2.time); - t1.year() == t2.year() && t1.ordinal() == t2.ordinal() -} - -fn equal_hour(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool { - let (t1, t2) = (sn1.time, sn2.time); - t1.year() == t2.year() && t1.ordinal() == t2.ordinal() && t1.hour() == t2.hour() -} - -impl KeepOptions { - fn matches( - &mut self, - sn: &SnapshotFile, - last: Option<&SnapshotFile>, - has_next: bool, - latest_time: DateTime, - ) -> Option { - let mut keep = false; - let mut reason = Vec::new(); - - let snapshot_id_hex = sn.id.to_hex(); - if self - .keep_ids - .iter() - .any(|id| snapshot_id_hex.starts_with(id)) - { - keep = true; - reason.push("id"); - } - - if !self.keep_tags.is_empty() && sn.tags.matches(&self.keep_tags) { - keep = true; - reason.push("tags"); - } - - let keep_checks: [(CheckFunction, &mut i32, &str, humantime::Duration, &str); 8] = [ - ( - always_false, - &mut self.keep_last, - "last", - self.keep_within, - "within", - ), - ( - equal_hour, - &mut self.keep_hourly, - "hourly", - self.keep_within_hourly, - "within hourly", - ), - ( - equal_day, - &mut self.keep_daily, - "daily", - self.keep_within_daily, - "within daily", - ), - ( - equal_week, - &mut self.keep_weekly, - "weekly", - self.keep_within_weekly, - "within weekly", - ), - ( - equal_month, - &mut self.keep_monthly, - "monthly", - self.keep_within_monthly, - "within monthly", - ), - ( - equal_quarter_year, - &mut self.keep_quarter_yearly, - "quarter-yearly", - self.keep_within_quarter_yearly, - "within quarter-yearly", - ), - ( - equal_half_year, - &mut self.keep_half_yearly, - "half-yearly", - self.keep_within_half_yearly, - "within half-yearly", - ), - ( - equal_year, - &mut self.keep_yearly, - "yearly", - self.keep_within_yearly, - "within yearly", - ), - ]; - - for (check_fun, counter, reason1, within, reason2) in keep_checks { - if !has_next || last.is_none() || !check_fun(sn, last.unwrap()) { - if *counter != 0 { - keep = true; - reason.push(reason1); - if *counter > 0 { - *counter -= 1; - } - } - if sn.time + Duration::from_std(*within).unwrap() > latest_time { - keep = true; - reason.push(reason2); - } - } - } - - keep.then_some(reason.join("\n")) - } -}