mirror of https://github.com/dswd/zvault
Partial backups
This commit is contained in:
parent
a8ff046c04
commit
657952d4d7
|
@ -7,6 +7,7 @@ dependencies = [
|
||||||
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"clap 2.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"clap 2.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"mmap 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"mmap 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"murmurhash3 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"murmurhash3 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -21,6 +21,7 @@ log = "0.3"
|
||||||
byteorder = "1.0"
|
byteorder = "1.0"
|
||||||
ansi_term = "0.9"
|
ansi_term = "0.9"
|
||||||
sodiumoxide = "*"
|
sodiumoxide = "*"
|
||||||
|
libc = "*"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
pkg-config = "0.3"
|
pkg-config = "0.3"
|
||||||
|
|
|
@ -254,7 +254,7 @@ impl Bundle {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn load_encoded_contents(&self) -> Result<Vec<u8>, BundleError> {
|
fn load_encoded_contents(&self) -> Result<Vec<u8>, BundleError> {
|
||||||
debug!("Load bundle data {}", self.info.id);
|
debug!("Load bundle data {} ({:?})", self.info.id, self.info.mode);
|
||||||
let mut file = BufReader::new(try!(File::open(&self.path).context(&self.path as &Path)));
|
let mut file = BufReader::new(try!(File::open(&self.path).context(&self.path as &Path)));
|
||||||
try!(file.seek(SeekFrom::Start(self.content_start as u64)).context(&self.path as &Path));
|
try!(file.seek(SeekFrom::Start(self.content_start as u64)).context(&self.path as &Path));
|
||||||
let mut data = Vec::with_capacity(max(self.info.encoded_size, self.info.raw_size)+1024);
|
let mut data = Vec::with_capacity(max(self.info.encoded_size, self.info.raw_size)+1024);
|
||||||
|
|
|
@ -17,7 +17,8 @@ pub enum Arguments {
|
||||||
repo_path: String,
|
repo_path: String,
|
||||||
backup_name: String,
|
backup_name: String,
|
||||||
src_path: String,
|
src_path: String,
|
||||||
full: bool
|
full: bool,
|
||||||
|
reference: Option<String>
|
||||||
},
|
},
|
||||||
Restore {
|
Restore {
|
||||||
repo_path: String,
|
repo_path: String,
|
||||||
|
@ -204,6 +205,7 @@ pub fn parse() -> Arguments {
|
||||||
(@subcommand backup =>
|
(@subcommand backup =>
|
||||||
(about: "creates a new backup")
|
(about: "creates a new backup")
|
||||||
(@arg full: --full "create a full backup")
|
(@arg full: --full "create a full backup")
|
||||||
|
(@arg reference: --ref +takes_value "the reference backup to use for partial backup")
|
||||||
(@arg SRC: +required "source path to backup")
|
(@arg SRC: +required "source path to backup")
|
||||||
(@arg BACKUP: +required "repository::backup path")
|
(@arg BACKUP: +required "repository::backup path")
|
||||||
)
|
)
|
||||||
|
@ -313,7 +315,8 @@ pub fn parse() -> Arguments {
|
||||||
repo_path: repository.to_string(),
|
repo_path: repository.to_string(),
|
||||||
backup_name: backup.unwrap().to_string(),
|
backup_name: backup.unwrap().to_string(),
|
||||||
full: args.is_present("full"),
|
full: args.is_present("full"),
|
||||||
src_path: args.value_of("SRC").unwrap().to_string()
|
src_path: args.value_of("SRC").unwrap().to_string(),
|
||||||
|
reference: args.value_of("reference").map(|v| v.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(args) = args.subcommand_matches("restore") {
|
if let Some(args) = args.subcommand_matches("restore") {
|
||||||
|
|
|
@ -30,6 +30,22 @@ fn get_backup(repo: &Repository, backup_name: &str) -> Backup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_reference_backup(repo: &Repository, path: &str) -> Option<Backup> {
|
||||||
|
let mut matching = Vec::new();
|
||||||
|
let hostname = match get_hostname() {
|
||||||
|
Ok(hostname) => hostname,
|
||||||
|
Err(_) => return None
|
||||||
|
};
|
||||||
|
for (_, backup) in repo.list_backups().unwrap() {
|
||||||
|
if backup.host == hostname && backup.path == path {
|
||||||
|
matching.push(backup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matching.sort_by_key(|b| b.date);
|
||||||
|
matching.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[allow(unknown_lints,cyclomatic_complexity)]
|
#[allow(unknown_lints,cyclomatic_complexity)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
if let Err(err) = logger::init() {
|
if let Err(err) = logger::init() {
|
||||||
|
@ -54,12 +70,21 @@ pub fn run() {
|
||||||
repo.save_config().unwrap();
|
repo.save_config().unwrap();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Arguments::Backup{repo_path, backup_name, src_path, full} => {
|
Arguments::Backup{repo_path, backup_name, src_path, full, reference} => {
|
||||||
let mut repo = open_repository(&repo_path);
|
let mut repo = open_repository(&repo_path);
|
||||||
|
let mut reference_backup = None;
|
||||||
if !full {
|
if !full {
|
||||||
warn!("Partial backups are not implemented yet, creating full backup");
|
reference_backup = reference.map(|r| get_backup(&repo, &r));
|
||||||
|
if reference_backup.is_none() {
|
||||||
|
reference_backup = find_reference_backup(&repo, &src_path);
|
||||||
}
|
}
|
||||||
let backup = repo.create_full_backup(&src_path).unwrap();
|
if let Some(ref backup) = reference_backup {
|
||||||
|
info!("Using backup from {} as reference", Local.timestamp(backup.date, 0).to_rfc2822());
|
||||||
|
} else {
|
||||||
|
info!("No reference backup found, doing a full scan instead");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let backup = repo.create_backup(&src_path, reference_backup.as_ref()).unwrap();
|
||||||
repo.save_backup(&backup, &backup_name).unwrap();
|
repo.save_backup(&backup, &backup_name).unwrap();
|
||||||
},
|
},
|
||||||
Arguments::Restore{repo_path, backup_name, inode, dst_path} => {
|
Arguments::Restore{repo_path, backup_name, inode, dst_path} => {
|
||||||
|
|
|
@ -14,6 +14,7 @@ extern crate chrono;
|
||||||
extern crate byteorder;
|
extern crate byteorder;
|
||||||
extern crate sodiumoxide;
|
extern crate sodiumoxide;
|
||||||
extern crate ansi_term;
|
extern crate ansi_term;
|
||||||
|
extern crate libc;
|
||||||
|
|
||||||
|
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
@ -33,7 +34,6 @@ mod cli;
|
||||||
// TODO: Recompress & combine bundles
|
// TODO: Recompress & combine bundles
|
||||||
// TODO: Encrypt backup files too
|
// TODO: Encrypt backup files too
|
||||||
// TODO: list --tree
|
// TODO: list --tree
|
||||||
// TODO: Partial backups via reference inode
|
|
||||||
// TODO: Import repository from remote folder
|
// TODO: Import repository from remote folder
|
||||||
// TODO: Continue on errors
|
// TODO: Continue on errors
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,9 @@ pub struct Backup {
|
||||||
pub date: i64,
|
pub date: i64,
|
||||||
pub duration: f32,
|
pub duration: f32,
|
||||||
pub file_count: usize,
|
pub file_count: usize,
|
||||||
pub dir_count: usize
|
pub dir_count: usize,
|
||||||
|
pub host: String,
|
||||||
|
pub path: String
|
||||||
}
|
}
|
||||||
serde_impl!(Backup(u8) {
|
serde_impl!(Backup(u8) {
|
||||||
root: Vec<Chunk> => 0,
|
root: Vec<Chunk> => 0,
|
||||||
|
@ -37,7 +39,9 @@ serde_impl!(Backup(u8) {
|
||||||
date: i64 => 8,
|
date: i64 => 8,
|
||||||
duration: f32 => 9,
|
duration: f32 => 9,
|
||||||
file_count: usize => 10,
|
file_count: usize => 10,
|
||||||
dir_count: usize => 11
|
dir_count: usize => 11,
|
||||||
|
host: String => 12,
|
||||||
|
path: String => 13
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -168,19 +172,28 @@ impl Repository {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn create_full_backup<P: AsRef<Path>>(&mut self, path: P) -> Result<Backup, RepositoryError> {
|
pub fn create_backup<P: AsRef<Path>>(&mut self, path: P, reference: Option<&Backup>) -> Result<Backup, RepositoryError> {
|
||||||
let mut scan_stack = vec![path.as_ref().to_owned()];
|
let reference_inode = reference.and_then(|b| self.get_inode(&b.root).ok());
|
||||||
|
let mut scan_stack = vec![(path.as_ref().to_owned(), reference_inode)];
|
||||||
let mut save_stack = vec![];
|
let mut save_stack = vec![];
|
||||||
let mut directories = HashMap::new();
|
let mut directories = HashMap::new();
|
||||||
let mut backup = Backup::default();
|
let mut backup = Backup::default();
|
||||||
|
backup.host = get_hostname().unwrap_or_else(|_| "".to_string());
|
||||||
|
backup.path = path.as_ref().to_string_lossy().to_string();
|
||||||
let info_before = self.info();
|
let info_before = self.info();
|
||||||
let start = Local::now();
|
let start = Local::now();
|
||||||
while let Some(path) = scan_stack.pop() {
|
while let Some((path, reference_inode)) = scan_stack.pop() {
|
||||||
// Create an inode for this path containing all attributes and contents
|
// Create an inode for this path containing all attributes and contents
|
||||||
// (for files) but no children (for directories)
|
// (for files) but no children (for directories)
|
||||||
let mut inode = try!(self.create_inode(&path));
|
let mut inode = try!(self.create_inode(&path, reference_inode.as_ref()));
|
||||||
backup.total_data_size += inode.size;
|
backup.total_data_size += inode.size;
|
||||||
|
if let Some(ref ref_inode) = reference_inode {
|
||||||
|
if !ref_inode.is_unchanged(&inode) {
|
||||||
backup.changed_data_size += inode.size;
|
backup.changed_data_size += inode.size;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
backup.changed_data_size += inode.size;
|
||||||
|
}
|
||||||
if inode.file_type == FileType::Directory {
|
if inode.file_type == FileType::Directory {
|
||||||
backup.dir_count +=1;
|
backup.dir_count +=1;
|
||||||
// For directories we need to put all children on the stack too, so there will be inodes created for them
|
// For directories we need to put all children on the stack too, so there will be inodes created for them
|
||||||
|
@ -189,7 +202,13 @@ impl Repository {
|
||||||
inode.children = Some(HashMap::new());
|
inode.children = Some(HashMap::new());
|
||||||
directories.insert(path.clone(), inode);
|
directories.insert(path.clone(), inode);
|
||||||
for ch in try!(fs::read_dir(&path)) {
|
for ch in try!(fs::read_dir(&path)) {
|
||||||
scan_stack.push(try!(ch).path());
|
let child = try!(ch);
|
||||||
|
let name = child.file_name().to_string_lossy().to_string();
|
||||||
|
let ref_child = reference_inode.as_ref()
|
||||||
|
.and_then(|inode| inode.children.as_ref())
|
||||||
|
.and_then(|map| map.get(&name))
|
||||||
|
.and_then(|chunks| self.get_inode(chunks).ok());
|
||||||
|
scan_stack.push((child.path(), ref_child));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
backup.file_count +=1;
|
backup.file_count +=1;
|
||||||
|
|
|
@ -79,7 +79,7 @@ impl Repository {
|
||||||
let mut new = false;
|
let mut new = false;
|
||||||
for &(hash, _len) in chunks {
|
for &(hash, _len) in chunks {
|
||||||
if let Some(pos) = self.index.pos(&hash) {
|
if let Some(pos) = self.index.pos(&hash) {
|
||||||
new |= checked.get(pos);
|
new |= !checked.get(pos);
|
||||||
checked.set(pos);
|
checked.set(pos);
|
||||||
} else {
|
} else {
|
||||||
return Err(RepositoryIntegrityError::MissingChunk(hash).into())
|
return Err(RepositoryIntegrityError::MissingChunk(hash).into())
|
||||||
|
|
|
@ -24,7 +24,7 @@ serde_impl!(FileType(u8) {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum FileContents {
|
pub enum FileContents {
|
||||||
Inline(msgpack::Bytes),
|
Inline(msgpack::Bytes),
|
||||||
ChunkedDirect(ChunkList),
|
ChunkedDirect(ChunkList),
|
||||||
|
@ -85,6 +85,7 @@ serde_impl!(Inode(u8) {
|
||||||
children: HashMap<String, ChunkList> => 11
|
children: HashMap<String, ChunkList> => 11
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
impl Inode {
|
impl Inode {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn get_extended_attrs_from(&mut self, meta: &Metadata) -> Result<(), RepositoryError> {
|
fn get_extended_attrs_from(&mut self, meta: &Metadata) -> Result<(), RepositoryError> {
|
||||||
|
@ -145,13 +146,25 @@ impl Inode {
|
||||||
// https://crates.io/crates/filetime
|
// https://crates.io/crates/filetime
|
||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_unchanged(&self, other: &Inode) -> bool {
|
||||||
|
self.modify_time == other.modify_time
|
||||||
|
&& self.create_time == other.create_time
|
||||||
|
&& self.file_type == other.file_type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Repository {
|
impl Repository {
|
||||||
pub fn create_inode<P: AsRef<Path>>(&mut self, path: P) -> Result<Inode, RepositoryError> {
|
pub fn create_inode<P: AsRef<Path>>(&mut self, path: P, reference: Option<&Inode>) -> Result<Inode, RepositoryError> {
|
||||||
let mut inode = try!(Inode::get_from(path.as_ref()));
|
let mut inode = try!(Inode::get_from(path.as_ref()));
|
||||||
if inode.file_type == FileType::File && inode.size > 0 {
|
if inode.file_type == FileType::File && inode.size > 0 {
|
||||||
|
if let Some(reference) = reference {
|
||||||
|
if reference.is_unchanged(&inode) {
|
||||||
|
inode.contents = reference.contents.clone();
|
||||||
|
return Ok(inode)
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut file = try!(File::open(path));
|
let mut file = try!(File::open(path));
|
||||||
if inode.size < 100 {
|
if inode.size < 100 {
|
||||||
let mut data = Vec::with_capacity(inode.size as usize);
|
let mut data = Vec::with_capacity(inode.size as usize);
|
||||||
|
@ -164,7 +177,7 @@ impl Repository {
|
||||||
} else {
|
} else {
|
||||||
let mut chunk_data = Vec::with_capacity(chunks.encoded_size());
|
let mut chunk_data = Vec::with_capacity(chunks.encoded_size());
|
||||||
chunks.write_to(&mut chunk_data).unwrap();
|
chunks.write_to(&mut chunk_data).unwrap();
|
||||||
chunks = try!(self.put_data(BundleMode::Content, &chunk_data));
|
chunks = try!(self.put_data(BundleMode::Meta, &chunk_data));
|
||||||
inode.contents = Some(FileContents::ChunkedIndirect(chunks));
|
inode.contents = Some(FileContents::ChunkedIndirect(chunks));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
use libc;
|
||||||
|
use std::ffi;
|
||||||
|
|
||||||
|
extern {
|
||||||
|
fn gethostname(name: *mut libc::c_char, size: libc::size_t) -> libc::c_int;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_hostname() -> Result<String, ()> {
|
||||||
|
let mut buf = Vec::with_capacity(255);
|
||||||
|
buf.resize(255, 0u8);
|
||||||
|
if unsafe { gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len() as libc::size_t) } == 0 {
|
||||||
|
buf[254] = 0; //enforce null-termination
|
||||||
|
let name = unsafe { ffi::CStr::from_ptr(buf.as_ptr() as *const libc::c_char) };
|
||||||
|
name.to_str().map(|s| s.to_string()).map_err(|_| ())
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ mod chunk;
|
||||||
mod bitmap;
|
mod bitmap;
|
||||||
mod hex;
|
mod hex;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
mod hostname;
|
||||||
pub mod msgpack;
|
pub mod msgpack;
|
||||||
|
|
||||||
pub use self::chunk::*;
|
pub use self::chunk::*;
|
||||||
|
@ -17,3 +18,4 @@ pub use self::lru_cache::*;
|
||||||
pub use self::bitmap::*;
|
pub use self::bitmap::*;
|
||||||
pub use self::hex::*;
|
pub use self::hex::*;
|
||||||
pub use self::cli::*;
|
pub use self::cli::*;
|
||||||
|
pub use self::hostname::*;
|
||||||
|
|
Loading…
Reference in New Issue