From 7cadaaf359468dd8ec36138b10b228f1033597b2 Mon Sep 17 00:00:00 2001 From: Dennis Schwerdel Date: Mon, 20 Mar 2017 15:38:33 +0100 Subject: [PATCH] Prune --- src/cli/args.rs | 40 +++++++++++++- src/cli/mod.rs | 15 +++++- src/main.rs | 1 - src/repository/backup.rs | 101 ++++++++++++++++++++++++++++++++++-- src/repository/integrity.rs | 3 +- src/repository/vacuum.rs | 3 +- 6 files changed, 152 insertions(+), 11 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index e348a8b..55c1e7f 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -31,6 +31,16 @@ pub enum Arguments { vacuum: bool, inode: Option }, + Prune { + repo_path: String, + prefix: String, + daily: Option, + weekly: Option, + monthly: Option, + yearly: Option, + vacuum: bool, + simulate: bool + }, Vacuum { repo_path: String, ratio: f32, @@ -209,10 +219,21 @@ pub fn parse() -> Arguments { (@arg vacuum: --vacuum "run vacuum afterwards to reclaim space") (@arg BACKUP: +required "repository::backup[::subpath] path") ) + (@subcommand prune => + (about: "removes backups based on age") + (@arg prefix: --prefix +takes_value "only consider backups starting with this prefix") + (@arg daily: --daily +takes_value "keep this number of daily backups") + (@arg weekly: --weekly +takes_value "keep this number of weekly backups") + (@arg monthly: --monthly +takes_value "keep this number of monthly backups") + (@arg yearly: --yearly +takes_value "keep this number of yearly backups") + (@arg vacuum: --vacuum "run vacuum afterwards to reclaim space") + (@arg simulate: --simulate "only simulate the prune, do not remove any backups") + (@arg REPO: +required "path of the repository") + ) (@subcommand vacuum => (about: "saves space by combining and recompressing bundles") (@arg ratio: --ratio -r +takes_value "ratio of unused chunks in a bundle to rewrite that bundle") - (@arg ratio: --simulate "only simulate the vacuum, do not remove any bundles") + (@arg simulate: --simulate "only simulate the vacuum, do not remove any bundles") (@arg REPO: +required "path of the repository") ) (@subcommand check => @@ -325,6 +346,23 @@ pub fn parse() -> Arguments { inode: inode.map(|v| v.to_string()) } } + if let Some(args) = args.subcommand_matches("prune") { + let (repository, backup, inode) = split_repo_path(args.value_of("REPO").unwrap()); + if backup.is_some() || inode.is_some() { + println!("No backups or subpaths may be given here"); + exit(1); + } + return Arguments::Prune { + repo_path: repository.to_string(), + prefix: args.value_of("prefix").unwrap_or("").to_string(), + vacuum: args.is_present("vacuum"), + simulate: args.is_present("simulate"), + daily: args.value_of("daily").map(|v| parse_num(v, "daily backups") as usize), + weekly: args.value_of("weekly").map(|v| parse_num(v, "weekly backups") as usize), + monthly: args.value_of("monthly").map(|v| parse_num(v, "monthly backups") as usize), + yearly: args.value_of("yearly").map(|v| parse_num(v, "yearly backups") as usize), + } + } if let Some(args) = args.subcommand_matches("vacuum") { let (repository, backup, inode) = split_repo_path(args.value_of("REPO").unwrap()); if backup.is_some() || inode.is_some() { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 1ce8c4b..0d89345 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -85,6 +85,17 @@ pub fn run() { repo.vacuum(0.5, false).unwrap(); } }, + Arguments::Prune{repo_path, prefix, daily, weekly, monthly, yearly, simulate, vacuum} => { + let mut repo = open_repository(&repo_path); + if daily.is_none() && weekly.is_none() && monthly.is_none() && yearly.is_none() { + error!("This would remove all those backups"); + exit(1); + } + repo.prune_backups(&prefix, daily, weekly, monthly, yearly, simulate).unwrap(); + if !simulate && vacuum { + repo.vacuum(0.5, false).unwrap(); + } + }, Arguments::Vacuum{repo_path, ratio, simulate} => { let mut repo = open_repository(&repo_path); repo.vacuum(ratio, simulate).unwrap(); @@ -118,8 +129,8 @@ pub fn run() { } } } else { - for backup in repo.list_backups().unwrap() { - println!("{}", backup); + for (name, backup) in repo.list_backups().unwrap() { + println!("{} - {} - {} files, {} dirs, {}", name, Local.timestamp(backup.date, 0).to_rfc2822(), backup.file_count, backup.dir_count, to_file_size(backup.total_data_size)); } } }, diff --git a/src/main.rs b/src/main.rs index b9f9a95..ac413fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,6 @@ mod cli; // TODO: - Write backup files there as well // TODO: Remove backup subtrees // TODO: Recompress & combine bundles -// TODO: Prune backups (based on age like attic) // TODO: Encrypt backup files too // TODO: list --tree // TODO: Partial backups diff --git a/src/repository/backup.rs b/src/repository/backup.rs index 2d8c252..b4397ba 100644 --- a/src/repository/backup.rs +++ b/src/repository/backup.rs @@ -42,8 +42,8 @@ serde_impl!(Backup(u8) { impl Repository { - pub fn list_backups(&self) -> Result, RepositoryError> { - let mut backups = Vec::new(); + pub fn list_backups(&self) -> Result, RepositoryError> { + let mut backups = HashMap::new(); let mut paths = Vec::new(); let base_path = self.path.join("backups"); paths.push(base_path.clone()); @@ -55,7 +55,9 @@ impl Repository { paths.push(path); } else { let relpath = path.strip_prefix(&base_path).unwrap(); - backups.push(relpath.to_string_lossy().to_string()); + let name = relpath.to_string_lossy().to_string(); + let backup = try!(self.get_backup(&name)); + backups.insert(name, backup); } } } @@ -86,6 +88,99 @@ impl Repository { Ok(()) } + pub fn prune_backups(&self, prefix: &str, daily: Option, weekly: Option, monthly: Option, yearly: Option, simulate: bool) -> Result<(), RepositoryError> { + let mut backups = Vec::new(); + for (name, backup) in try!(self.list_backups()) { + if name.starts_with(prefix) { + let date = Local.timestamp(backup.date, 0); + backups.push((name, date, backup)); + } + } + backups.sort_by_key(|backup| backup.2.date); + let mut keep = Bitmap::new(backups.len()); + if let Some(max) = yearly { + let mut unique = VecDeque::with_capacity(max+1); + let mut last = None; + for (i, backup) in backups.iter().enumerate() { + let val = backup.1.year(); + if Some(val) != last { + last = Some(val); + unique.push_back(i); + if unique.len() > max { + unique.pop_front(); + } + } + } + for i in unique { + keep.set(i); + } + } + if let Some(max) = monthly { + let mut unique = VecDeque::with_capacity(max+1); + let mut last = None; + for (i, backup) in backups.iter().enumerate() { + let val = (backup.1.year(), backup.1.month()); + if Some(val) != last { + last = Some(val); + unique.push_back(i); + if unique.len() > max { + unique.pop_front(); + } + } + } + for i in unique { + keep.set(i); + } + } + if let Some(max) = weekly { + let mut unique = VecDeque::with_capacity(max+1); + let mut last = None; + for (i, backup) in backups.iter().enumerate() { + let val = (backup.1.isoweekdate().0, backup.1.isoweekdate().1); + if Some(val) != last { + last = Some(val); + unique.push_back(i); + if unique.len() > max { + unique.pop_front(); + } + } + } + for i in unique { + keep.set(i); + } + } + if let Some(max) = daily { + let mut unique = VecDeque::with_capacity(max+1); + let mut last = None; + for (i, backup) in backups.iter().enumerate() { + let val = (backup.1.year(), backup.1.month(), backup.1.day()); + if Some(val) != last { + last = Some(val); + unique.push_back(i); + if unique.len() > max { + unique.pop_front(); + } + } + } + for i in unique { + keep.set(i); + } + } + let mut remove = Vec::new(); + for (i, backup) in backups.into_iter().enumerate() { + if !keep.get(i) { + remove.push(backup.0); + } + } + info!("Removing the following backups: {:?}", remove); + if !simulate { + for name in remove { + try!(self.delete_backup(&name)); + } + } + Ok(()) + } + pub fn restore_inode_tree>(&mut self, inode: Inode, path: P) -> Result<(), RepositoryError> { let mut queue = VecDeque::new(); queue.push_back((path.as_ref().to_owned(), inode)); diff --git a/src/repository/integrity.rs b/src/repository/integrity.rs index 7ec92e9..5078858 100644 --- a/src/repository/integrity.rs +++ b/src/repository/integrity.rs @@ -90,8 +90,7 @@ impl Repository { fn check_backups(&mut self) -> Result<(), RepositoryError> { let mut checked = Bitmap::new(self.index.capacity()); - for name in try!(self.list_backups()) { - let backup = try!(self.get_backup(&name)); + for (_name, backup) in try!(self.list_backups()) { let mut todo = VecDeque::new(); todo.push_back(backup.root); while let Some(chunks) = todo.pop_front() { diff --git a/src/repository/vacuum.rs b/src/repository/vacuum.rs index 1660132..52b5851 100644 --- a/src/repository/vacuum.rs +++ b/src/repository/vacuum.rs @@ -48,8 +48,7 @@ impl Repository { used_size: 0 }); } - for name in try!(self.list_backups()) { - let backup = try!(self.get_backup(&name)); + for (_name, backup) in try!(self.list_backups()).into_iter() { let mut todo = VecDeque::new(); todo.push_back(backup.root); while let Some(chunks) = todo.pop_front() {