-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
442 additions
and
338 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
pub mod cat; | ||
pub mod check; | ||
pub mod forget; | ||
pub mod prune; | ||
pub mod repoinfo; | ||
pub mod snapshots; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,371 @@ | ||
//! `forget` subcommand | ||
|
||
use chrono::{DateTime, Datelike, Duration, Local, Timelike}; | ||
use derivative::Derivative; | ||
use merge::Merge; | ||
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<ForgetGroup>); | ||
|
||
#[derive(Debug)] | ||
pub struct ForgetGroup { | ||
pub group: SnapshotGroup, | ||
pub snapshots: Vec<ForgetSnapshot>, | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct ForgetSnapshot { | ||
pub snapshot: SnapshotFile, | ||
pub forget: bool, | ||
pub reasons: Vec<String>, | ||
} | ||
|
||
impl ForgetGroups { | ||
pub fn into_forget_ids(self) -> Vec<Id> { | ||
self.0 | ||
.into_iter() | ||
.flat_map(|fg| { | ||
fg.snapshots | ||
.into_iter() | ||
.filter_map(|fsn| fsn.forget.then_some(fsn.snapshot.id)) | ||
}) | ||
.collect() | ||
} | ||
} | ||
|
||
pub(crate) fn get_forget_snapshots<P: ProgressBars>( | ||
repo: &OpenRepository<P>, | ||
keep: &KeepOptions, | ||
group_by: SnapshotGroupCriterion, | ||
filter: impl FnMut(&SnapshotFile) -> bool, | ||
) -> RusticResult<ForgetGroups> { | ||
let be = &repo.dbe; | ||
let pb = &repo.pb; | ||
|
||
let p = pb.progress_hidden(); | ||
let groups = SnapshotFile::group_from_backend(be, filter, group_by, &p)?; | ||
let mut forget_snaps = Vec::new(); | ||
Check failure on line 56 in crates/rustic_core/src/commands/forget.rs GitHub Actions / Clippy Outputcollection is never read
|
||
|
||
let groups = groups | ||
.into_iter() | ||
.map(|(group, mut snapshots)| -> RusticResult<_> { | ||
let mut snaps = Vec::new(); | ||
snapshots.sort_unstable_by(|sn1, sn2| sn1.cmp(sn2).reverse()); | ||
let latest_time = snapshots[0].time; | ||
let mut group_keep = keep.clone(); | ||
|
||
let mut iter = snapshots.into_iter().peekable(); | ||
let mut last = None; | ||
let now = Local::now(); | ||
|
||
while let Some(sn) = iter.next() { | ||
let (forget, reasons) = { | ||
if sn.must_keep(now) { | ||
(false, vec!["snapshot".to_string()]) | ||
} else if sn.must_delete(now) { | ||
forget_snaps.push(sn.id); | ||
(true, vec!["snapshot".to_string()]) | ||
} else { | ||
match group_keep.matches( | ||
&sn, | ||
last.as_ref(), | ||
iter.peek().is_some(), | ||
latest_time, | ||
) { | ||
None => { | ||
forget_snaps.push(sn.id); | ||
(true, Vec::new()) | ||
} | ||
Some(reason) => (false, vec![reason]), | ||
} | ||
} | ||
}; | ||
last = Some(sn.clone()); | ||
|
||
snaps.push(ForgetSnapshot { | ||
snapshot: sn, | ||
forget, | ||
reasons, | ||
}); | ||
} | ||
Ok(ForgetGroup { | ||
group, | ||
snapshots: snaps, | ||
}) | ||
}) | ||
.collect::<RusticResult<_>>()?; | ||
|
||
Ok(ForgetGroups(groups)) | ||
} | ||
|
||
#[serde_as] | ||
#[derive(Clone, Debug, PartialEq, Derivative, clap::Parser, Deserialize, Merge)] | ||
Check failure on line 111 in crates/rustic_core/src/commands/forget.rs GitHub Actions / Clippy Outputyou are deriving `PartialEq` and can implement `Eq`
|
||
#[derivative(Default)] | ||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] | ||
pub struct KeepOptions { | ||
/// Keep snapshots with this taglist (can be specified multiple times) | ||
#[clap(long, value_name = "TAG[,TAG,..]")] | ||
#[serde_as(as = "Vec<DisplayFromStr>")] | ||
#[merge(strategy=merge::vec::overwrite_empty)] | ||
keep_tags: Vec<StringList>, | ||
|
||
/// 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<String>, | ||
|
||
/// 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<Local>, | ||
) -> Option<String> { | ||
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")) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.