diff --git a/README.md b/README.md index 3f2e8ce..92b9cc5 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,7 @@ Recommended: Brotli/2-7 ## TODO ### Core functionality -- Subcommand 'versions': find different versions of a file in different backups - Subcommand 'diff': find differences between two backups (add, mod, del) -- Default excludes in repository - Fix vacuum inconsistencies (either index related, or bundle syncing related) - Recompress & combine bundles - Allow to use tar files for backup and restore (--tar, http://alexcrichton.com/tar-rs/tar/index.html) diff --git a/src/cli/args.rs b/src/cli/args.rs index 81377f4..11ee48d 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -72,6 +72,10 @@ pub enum Arguments { inode: Option, mount_point: String }, + Versions { + repo_path: String, + path: String + }, Analyze { repo_path: String }, @@ -304,6 +308,11 @@ pub fn parse() -> Arguments { (about: "analyze the used and reclaimable space of bundles") (@arg REPO: +required "repository path") ) + (@subcommand versions => + (about: "display different versions of a file in all backups") + (@arg REPO: +required "repository path") + (@arg PATH: +required "the file path") + ) (@subcommand config => (about: "changes the configuration") (@arg REPO: +required "path of the repository") @@ -444,6 +453,13 @@ pub fn parse() -> Arguments { mount_point: args.value_of("MOUNTPOINT").unwrap().to_string() } } + if let Some(args) = args.subcommand_matches("versions") { + let (repository, _backup, _inode) = parse_repo_path(args.value_of("REPO").unwrap(), Some(false), Some(false)); + return Arguments::Versions { + repo_path: repository.to_string(), + path: args.value_of("PATH").unwrap().to_string() + } + } if let Some(args) = args.subcommand_matches("analyze") { let (repository, _backup, _inode) = parse_repo_path(args.value_of("REPO").unwrap(), Some(false), Some(false)); return Arguments::Analyze { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index ea007b3..c1407fc 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -83,7 +83,7 @@ fn print_backup(backup: &Backup) { pub fn format_inode_one_line(inode: &Inode) -> String { match inode.file_type { FileType::Directory => format!("{:25}\t{} entries", format!("{}/", inode.name), inode.children.as_ref().unwrap().len()), - FileType::File => format!("{:25}\t{}", inode.name, to_file_size(inode.size)), + FileType::File => format!("{:25}\t{:>10}\t{}", inode.name, to_file_size(inode.size), Local.timestamp(inode.modify_time, 0).to_rfc2822()), FileType::Symlink => format!("{:25}\t -> {}", inode.name, inode.symlink_target.as_ref().unwrap()), } } @@ -110,7 +110,7 @@ fn print_inode(inode: &Inode) { fn print_backups(backup_map: &HashMap) { for (name, backup) in backup_map { - println!("{:25} {:>32} {:5} files, {:4} dirs, {:>10}", + println!("{:40} {:>32} {:5} files, {:4} dirs, {:>10}", name, Local.timestamp(backup.date, 0).to_rfc2822(), backup.file_count, backup.dir_count, to_file_size(backup.total_data_size)); } @@ -418,6 +418,13 @@ pub fn run() { Arguments::Import{repo_path, remote_path, key_files} => { checked(Repository::import(repo_path, remote_path, key_files), "import repository"); }, + Arguments::Versions{repo_path, path} => { + let mut repo = open_repository(&repo_path); + for (name, mut inode) in checked(repo.find_versions(&path), "find versions") { + inode.name = format!("{}::{}", name, &path); + println!("{}", format_inode_one_line(&inode)); + } + }, Arguments::Config{repo_path, bundle_size, chunker, compression, encryption, hash} => { let mut repo = open_repository(&repo_path); if let Some(bundle_size) = bundle_size { diff --git a/src/repository/backup.rs b/src/repository/backup.rs index 02616ae..4fdbc9c 100644 --- a/src/repository/backup.rs +++ b/src/repository/backup.rs @@ -277,4 +277,22 @@ impl Repository { pub fn get_backup_inode>(&mut self, backup: &Backup, path: P) -> Result { self.get_backup_path(backup, path).map(|mut inodes| inodes.pop().unwrap()) } + + #[inline] + pub fn find_versions>(&mut self, path: P) -> Result, RepositoryError> { + let path = path.as_ref(); + let mut versions = HashMap::new(); + for (name, backup) in try!(self.get_backups()) { + match self.get_backup_inode(&backup, path) { + Ok(inode) => { + versions.insert((inode.file_type, inode.modify_time, inode.size), (name, inode)); + }, + Err(RepositoryError::NoSuchFileInBackup(..)) => continue, + Err(err) => return Err(err) + } + } + let mut versions: Vec<_> = versions.into_iter().map(|(_, v)| v).collect(); + versions.sort_by_key(|v| v.1.modify_time); + Ok(versions) + } } diff --git a/src/repository/metadata.rs b/src/repository/metadata.rs index 31615f8..5b1e69c 100644 --- a/src/repository/metadata.rs +++ b/src/repository/metadata.rs @@ -68,7 +68,7 @@ quick_error!{ } -#[derive(Debug, Eq, PartialEq, Clone, Copy)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] pub enum FileType { File, Directory, @@ -90,7 +90,7 @@ impl fmt::Display for FileType { } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum FileContents { Inline(msgpack::Bytes), ChunkedDirect(ChunkList), @@ -103,7 +103,7 @@ serde_impl!(FileContents(u8) { }); -#[derive(Debug)] +#[derive(Debug, Hash, Eq, PartialEq)] pub struct Inode { pub name: String, pub size: u64,