This commit is contained in:
Dennis Schwerdel 2017-03-20 15:38:33 +01:00
parent fc45fa4e33
commit 7cadaaf359
6 changed files with 152 additions and 11 deletions

View File

@ -31,6 +31,16 @@ pub enum Arguments {
vacuum: bool,
inode: Option<String>
},
Prune {
repo_path: String,
prefix: String,
daily: Option<usize>,
weekly: Option<usize>,
monthly: Option<usize>,
yearly: Option<usize>,
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() {

View File

@ -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));
}
}
},

View File

@ -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

View File

@ -42,8 +42,8 @@ serde_impl!(Backup(u8) {
impl Repository {
pub fn list_backups(&self) -> Result<Vec<String>, RepositoryError> {
let mut backups = Vec::new();
pub fn list_backups(&self) -> Result<HashMap<String, Backup>, 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<usize>, weekly: Option<usize>, monthly: Option<usize>, yearly: Option<usize>, 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<P: AsRef<Path>>(&mut self, inode: Inode, path: P) -> Result<(), RepositoryError> {
let mut queue = VecDeque::new();
queue.push_back((path.as_ref().to_owned(), inode));

View File

@ -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() {

View File

@ -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() {