diff --git a/Cargo.lock b/Cargo.lock index 334ad4e4..6439e894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3701,7 +3701,7 @@ dependencies = [ "rhai", "rstest", "rustic_backend", - "rustic_core", + "rustic_core 0.4.0 (git+https://github.com/rustic-rs/rustic_core.git?branch=lock)", "rustic_testing", "scopeguard", "self_update", @@ -3721,8 +3721,7 @@ dependencies = [ [[package]] name = "rustic_backend" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c795aff2332a69ba31fb6cc91237db3eb88d5d0d160177522ba49a21dd5911" +source = "git+https://github.com/rustic-rs/rustic_core.git?branch=lock#51980305e44a8a4997ea2f61edab4df9b6470575" dependencies = [ "aho-corasick", "anyhow", @@ -3741,7 +3740,7 @@ dependencies = [ "rand", "rayon", "reqwest", - "rustic_core", + "rustic_core 0.4.0 (git+https://github.com/rustic-rs/rustic_core.git?branch=lock)", "semver", "serde", "strum", @@ -3757,6 +3756,59 @@ name = "rustic_core" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "666cbd4da5ab6060f77326e73c13fe2d5043c95161393e913b910aecbdc894d0" +dependencies = [ + "aes256ctr_poly1305aes", + "anyhow", + "binrw", + "bytes", + "bytesize", + "cached", + "cachedir", + "chrono", + "crossbeam-channel", + "derivative", + "derive_more", + "derive_setters", + "dirs", + "displaydoc", + "dunce", + "enum-map", + "enum-map-derive", + "enumset", + "filetime", + "gethostname", + "hex", + "humantime", + "ignore", + "integer-sqrt", + "itertools", + "log", + "nix", + "pariter", + "path-dedot", + "quick_cache", + "rand", + "rayon", + "runtime-format", + "scrypt", + "serde", + "serde-aux", + "serde_derive", + "serde_json", + "serde_with", + "sha2", + "shell-words", + "strum", + "thiserror", + "walkdir", + "xattr", + "zstd", +] + +[[package]] +name = "rustic_core" +version = "0.4.0" +source = "git+https://github.com/rustic-rs/rustic_core.git?branch=lock#51980305e44a8a4997ea2f61edab4df9b6470575" dependencies = [ "aes256ctr_poly1305aes", "anyhow", @@ -3821,7 +3873,7 @@ dependencies = [ "bytes", "enum-map", "once_cell", - "rustic_core", + "rustic_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile", ] diff --git a/Cargo.toml b/Cargo.toml index 9f7407b1..36280941 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,8 +41,8 @@ rustdoc-args = ["--document-private-items", "--generate-link-to-definition"] [dependencies] abscissa_core = { version = "0.7.0", default-features = false, features = ["application"] } -rustic_backend = { version = "0.3.0", features = ["cli"] } -rustic_core = { version = "0.4.0", features = ["cli"] } +rustic_backend = { git = "https://github.com/rustic-rs/rustic_core.git", branch = "lock", features = ["cli"] } +rustic_core = { git = "https://github.com/rustic-rs/rustic_core.git", branch = "lock", features = ["cli"] } # allocators jemallocator-global = { version = "0.3.2", optional = true } diff --git a/src/commands.rs b/src/commands.rs index 7bb7a498..c88fbfcf 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,6 +13,7 @@ pub(crate) mod forget; pub(crate) mod init; pub(crate) mod key; pub(crate) mod list; +pub(crate) mod lock; pub(crate) mod ls; pub(crate) mod merge; pub(crate) mod prune; @@ -39,9 +40,10 @@ use crate::{ commands::{ backup::BackupCmd, cat::CatCmd, check::CheckCmd, completions::CompletionsCmd, config::ConfigCmd, copy::CopyCmd, diff::DiffCmd, dump::DumpCmd, forget::ForgetCmd, - init::InitCmd, key::KeyCmd, list::ListCmd, ls::LsCmd, merge::MergeCmd, prune::PruneCmd, - repair::RepairCmd, repoinfo::RepoInfoCmd, restore::RestoreCmd, self_update::SelfUpdateCmd, - show_config::ShowConfigCmd, snapshots::SnapshotCmd, tag::TagCmd, + init::InitCmd, key::KeyCmd, list::ListCmd, lock::LockCmd, ls::LsCmd, merge::MergeCmd, + prune::PruneCmd, repair::RepairCmd, repoinfo::RepoInfoCmd, restore::RestoreCmd, + self_update::SelfUpdateCmd, show_config::ShowConfigCmd, snapshots::SnapshotCmd, + tag::TagCmd, }, config::{progress_options::ProgressOptions, AllRepositoryOptions, RusticConfig}, {Application, RUSTIC_APP}, @@ -113,6 +115,9 @@ enum RusticCmd { /// List repository files List(ListCmd), + /// Lock snapshots + Lock(LockCmd), + /// List file contents of a snapshot Ls(LsCmd), diff --git a/src/commands/forget.rs b/src/commands/forget.rs index 30b0c23b..db844acc 100644 --- a/src/commands/forget.rs +++ b/src/commands/forget.rs @@ -119,7 +119,13 @@ impl ForgetCmd { .get_snapshots(&self.ids)? .into_iter() .map(|sn| { - if sn.must_keep(now) { + if sn.is_locked(now) { + ForgetSnapshot { + snapshot: sn, + keep: true, + reasons: vec!["locked".to_string()], + } + } else if sn.must_keep(now) { ForgetSnapshot { snapshot: sn, keep: true, diff --git a/src/commands/lock.rs b/src/commands/lock.rs new file mode 100644 index 00000000..a4c5a82a --- /dev/null +++ b/src/commands/lock.rs @@ -0,0 +1,153 @@ +//! `lock` subcommand + +use std::str::FromStr; + +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; +use abscissa_core::{Command, Runnable, Shutdown}; + +use anyhow::Result; +use chrono::{DateTime, Duration, Local}; + +use rustic_core::{repofile::KeyId, LockOptions}; + +/// `lock` subcommand +#[derive(clap::Parser, Command, Debug)] +pub(crate) struct LockCmd { + /// Subcommand to run + #[clap(subcommand)] + cmd: LockSubCmd, +} + +impl Runnable for LockCmd { + fn run(&self) { + let config = RUSTIC_APP.config(); + if config.global.dry_run { + println!("lock is not supported in dry-run mode"); + } else { + self.cmd.run(); + } + } +} + +/// `lock` subcommand +#[derive(clap::Subcommand, Debug, Runnable)] +enum LockSubCmd { + /// Lock the complete repository + Repository(RepoSubCmd), + /// Lock all key files + Keys(KeysSubCmd), + /// Lock snapshots and relevant pack files + Snapshots(SnapSubCmd), +} + +#[derive(clap::Parser, Command, Debug, Clone)] +pub(crate) struct RepoSubCmd { + #[clap(long)] + /// Duration for how long to extend the locks (e.g. "10d"). "forever" is also allowed + duration: LockDuration, +} + +impl Runnable for RepoSubCmd { + fn run(&self) { + if let Err(err) = self.inner_run() { + status_err!("{}", err); + RUSTIC_APP.shutdown(Shutdown::Crash); + }; + } +} + +impl RepoSubCmd { + fn inner_run(&self) -> Result<()> { + let config = RUSTIC_APP.config(); + let repo = open_repository(&config.repository)?; + repo.lock_repo(self.duration.0)?; + Ok(()) + } +} + +#[derive(clap::Parser, Command, Debug, Clone)] +pub(crate) struct KeysSubCmd { + #[clap(long)] + /// Duration for how long to extend the locks (e.g. "10d"). "forever" is also allowed + duration: LockDuration, +} + +impl Runnable for KeysSubCmd { + fn run(&self) { + if let Err(err) = self.inner_run() { + status_err!("{}", err); + RUSTIC_APP.shutdown(Shutdown::Crash); + }; + } +} + +impl KeysSubCmd { + fn inner_run(&self) -> Result<()> { + let config = RUSTIC_APP.config(); + let repo = open_repository(&config.repository)?; + repo.lock_repo_files::(self.duration.0)?; + Ok(()) + } +} + +#[derive(clap::Parser, Command, Debug, Clone)] +pub(crate) struct SnapSubCmd { + /// Extend locks even if the files are already locked long enough + #[clap(long)] + always_extend_lock: bool, + + #[clap(long)] + /// Duration for how long to extend the locks (e.g. "10d"). "forever" is also allowed + duration: LockDuration, + + /// Snapshots to lock. If none is given, use filter options to filter from all snapshots + #[clap(value_name = "ID")] + ids: Vec, +} + +#[derive(Debug, Clone)] +struct LockDuration(Option>); + +impl FromStr for LockDuration { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + match s { + "forever" => Ok(Self(None)), + d => { + let duration = humantime::Duration::from_str(d)?; + let duration = Duration::from_std(*duration)?; + Ok(Self(Some(Local::now() + duration))) + } + } + } +} + +impl Runnable for SnapSubCmd { + fn run(&self) { + if let Err(err) = self.inner_run() { + status_err!("{}", err); + RUSTIC_APP.shutdown(Shutdown::Crash); + }; + } +} + +impl SnapSubCmd { + fn inner_run(&self) -> Result<()> { + let config = RUSTIC_APP.config(); + let repo = open_repository(&config.repository)?; + + let snapshots = if self.ids.is_empty() { + repo.get_matching_snapshots(|sn| config.snapshot_filter.matches(sn))? + } else { + repo.get_snapshots(&self.ids)? + }; + + let lock_opts = LockOptions::default() + .always_extend_lock(self.always_extend_lock) + .until(self.duration.0); + + repo.lock_snaphots(&lock_opts, &snapshots)?; + + Ok(()) + } +} diff --git a/src/commands/snapshots.rs b/src/commands/snapshots.rs index 09753998..aeb90281 100644 --- a/src/commands/snapshots.rs +++ b/src/commands/snapshots.rs @@ -184,6 +184,10 @@ pub fn fill_table(snap: &SnapshotFile, mut add_entry: impl FnMut(&str, String)) DeleteOption::NotSet => "not set".to_string(), DeleteOption::Never => "never".to_string(), DeleteOption::After(t) => format!("after {}", t.format("%Y-%m-%d %H:%M:%S")), + DeleteOption::LockedUntil(t) => { + format!("locked until {}", t.format("%Y-%m-%d %H:%M:%S")) + } + DeleteOption::LockedForever => "locked forever".to_string(), }; add_entry("Delete", delete); add_entry("Paths", snap.paths.formatln()); diff --git a/src/commands/tag.rs b/src/commands/tag.rs index 850a62ed..d3cad661 100644 --- a/src/commands/tag.rs +++ b/src/commands/tag.rs @@ -104,7 +104,7 @@ impl TagCmd { println!("would have modified the following snapshots:\n {old_snap_ids:?}"); } (false, false) => { - repo.save_snapshots(snapshots)?; + _ = repo.save_snapshots(snapshots)?; repo.delete_snapshots(&old_snap_ids)?; } } diff --git a/src/commands/tui/snapshots.rs b/src/commands/tui/snapshots.rs index 26d8fb32..ee70d3e4 100644 --- a/src/commands/tui/snapshots.rs +++ b/src/commands/tui/snapshots.rs @@ -695,7 +695,7 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> { .zip(self.snaps_status.iter()) .filter_map(|(snap, status)| status.to_forget.then_some(snap.id)); let delete_ids: Vec<_> = old_snap_ids.chain(snap_ids_to_forget).collect(); - self.repo.save_snapshots(save_snaps)?; + _ = self.repo.save_snapshots(save_snaps)?; self.repo.delete_snapshots(&delete_ids)?; // re-read snapshots self.reread()