diff --git a/src/commands/backup.rs b/src/commands/backup.rs index 17f900c..d4d3eda 100644 --- a/src/commands/backup.rs +++ b/src/commands/backup.rs @@ -46,8 +46,10 @@ pub(super) async fn execute( opts: Opts, command: String, ) -> Result<()> { - let mut snap = SnapshotFile::default(); - snap.command = Some(command); + let mut snap = SnapshotFile { + command: Some(command), + ..Default::default() + }; let config: ConfigFile = be.get_file(&Id::default()).await?; let be = DryRunBackend::new(be.clone(), opts.dry_run); @@ -67,9 +69,7 @@ pub(super) async fn execute( .ok_or_else(|| anyhow!("non-unicode hostname {:?}", hostname))? .to_string(); - for tags in opts.tag { - snap.tags.add_all(tags); - } + snap.set_tags(opts.tag); let parent = match (opts.force, opts.parent) { (true, _) => None, diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 46e9418..9866088 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -17,6 +17,7 @@ mod prune; mod repoinfo; mod restore; mod snapshots; +mod tag; use helpers::*; use vlog::*; @@ -76,6 +77,9 @@ enum Command { /// show general information about repository Repoinfo(repoinfo::Opts), + + /// change tags of snapshots + Tag(tag::Opts), } pub async fn execute() -> Result<()> { @@ -107,5 +111,6 @@ pub async fn execute() -> Result<()> { Command::Prune(opts) => prune::execute(&dbe, opts).await, Command::Restore(opts) => restore::execute(&dbe, opts).await, Command::Repoinfo(opts) => repoinfo::execute(&dbe, opts).await, + Command::Tag(opts) => tag::execute(&dbe, opts).await, } } diff --git a/src/commands/tag.rs b/src/commands/tag.rs new file mode 100644 index 0000000..5fd084e --- /dev/null +++ b/src/commands/tag.rs @@ -0,0 +1,63 @@ +use anyhow::Result; +use clap::Parser; + +use crate::backend::{DecryptFullBackend, FileType}; +use crate::repo::{SnapshotFile, SnapshotFilter, StringList}; + +#[derive(Parser)] +pub(super) struct Opts { + #[clap(flatten)] + filter: SnapshotFilter, + + /// Tags to add add (can be specified multiple times) + #[clap(long, value_name = "TAG[,TAG,..]", conflicts_with = "remove")] + add: Vec, + + /// Tags to remove (can be specified multiple times) + #[clap(long, value_name = "TAG[,TAG,..]")] + remove: Vec, + + /// Tag list to set (can be specified multiple times) + #[clap(long, value_name = "TAG[,TAG,..]", conflicts_with = "remove")] + set: Vec, + // TODO: allow giving specific snapshots +} + +pub(super) async fn execute(be: &impl DecryptFullBackend, opts: Opts) -> Result<()> { + let snapshots = SnapshotFile::all_from_backend(be).await?; + + let mut count = 0; + for sn in snapshots.into_iter().filter(|sn| sn.matches(&opts.filter)) { + if modify_sn(sn, be, &opts).await? { + count += 1; + } + } + + println!("changed {} snapshot(s)", count); + + Ok(()) +} + +async fn modify_sn( + mut sn: SnapshotFile, + be: &impl DecryptFullBackend, + opts: &Opts, +) -> Result { + let mut changed = false; + + if !opts.set.is_empty() { + changed |= sn.set_tags(opts.set.clone()); + } + changed |= sn.add_tags(opts.add.clone()); + changed |= sn.remove_tags(opts.remove.clone()); + + // FIXME: For some reason, changed is always true...?!? + if changed { + // TODO: Save original snapshot ID + // TODO: Save and delete in parallel + be.save_file(&sn).await?; + be.remove(FileType::Snapshot, &sn.id).await?; + } + + Ok(changed) +} diff --git a/src/repo/snapshotfile.rs b/src/repo/snapshotfile.rs index 5f832c0..15159a8 100644 --- a/src/repo/snapshotfile.rs +++ b/src/repo/snapshotfile.rs @@ -184,6 +184,32 @@ impl SnapshotFile { && self.tags.matches(&filter.tags) && (filter.hostnames.is_empty() || filter.hostnames.contains(&self.hostname)) } + + /// Add tag lists to snapshot. return wheter snapshot was changed + pub fn add_tags(&mut self, tag_lists: Vec) -> bool { + let old_tags = self.tags.clone(); + self.tags.add_all(tag_lists); + self.tags.sort(); + + old_tags == self.tags + } + + /// Set tag lists to snapshot. return wheter snapshot was changed + pub fn set_tags(&mut self, tag_lists: Vec) -> bool { + let old_tags = std::mem::take(&mut self.tags); + self.tags.add_all(tag_lists); + self.tags.sort(); + + old_tags == self.tags + } + + /// Remove tag lists from snapshot. return wheter snapshot was changed + pub fn remove_tags(&mut self, tag_lists: Vec) -> bool { + let old_tags = self.tags.clone(); + self.tags.remove_all(tag_lists); + + old_tags == self.tags + } } impl PartialOrd for SnapshotFile { @@ -197,7 +223,7 @@ impl Ord for SnapshotFile { } } -#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] +#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Clone, Serialize, Deserialize)] pub struct StringList(Vec); impl FromStr for StringList { @@ -226,12 +252,27 @@ impl StringList { } } - pub fn add_all(&mut self, sl: StringList) { + pub fn add_list(&mut self, sl: StringList) { for s in sl.0 { self.add(s) } } + pub fn add_all(&mut self, string_lists: Vec) { + for sl in string_lists { + self.add_list(sl) + } + } + + pub fn remove_all(&mut self, string_lists: Vec) { + self.0 + .retain(|s| !string_lists.iter().any(|sl| sl.contains(s))); + } + + pub fn sort(&mut self) { + self.0.sort_unstable(); + } + pub fn formatln(&self) -> String { self.0 .iter()