From 828bcc6dc99b2dda39ad54278e7424ab7c0b5f82 Mon Sep 17 00:00:00 2001 From: Dennis Schwerdel Date: Fri, 24 Mar 2017 08:56:57 +0100 Subject: [PATCH] Locking --- README.md | 4 - src/repository/backup.rs | 4 + src/repository/error.rs | 6 ++ src/repository/mod.rs | 14 +++- src/repository/vacuum.rs | 2 + src/util/fs.rs | 2 +- src/util/lock.rs | 156 +++++++++++++++++++++++++++++++++++++++ src/util/mod.rs | 2 + 8 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 src/util/lock.rs diff --git a/README.md b/README.md index fcaf02c..5b8eed3 100644 --- a/README.md +++ b/README.md @@ -99,15 +99,11 @@ Recommended: Brotli/2-7 ### Core functionality - Keep backup files also remotely and sync them -- Lock during backup and vacuum - Options for creating backups (same filesystem, exclude/include patterns) - Recompress & combine bundles - Allow to use tar files for backup and restore (--tar, http://alexcrichton.com/tar-rs/tar/index.html) - File attributes - xattrs https://crates.io/crates/xattr - - gid/uid - - http://mahkoh.github.io/posix/doc/posix/unistd/fn.lchown.html - - http://mahkoh.github.io/posix/doc/posix/sys/time/fn.utimes.html ### CLI functionality - list --tree diff --git a/src/repository/backup.rs b/src/repository/backup.rs index 8b6a111..dfcbbd0 100644 --- a/src/repository/backup.rs +++ b/src/repository/backup.rs @@ -286,6 +286,7 @@ impl Repository { } pub fn restore_inode_tree>(&mut self, inode: Inode, path: P) -> Result<(), RepositoryError> { + let _lock = try!(self.lock(false)); let mut queue = VecDeque::new(); queue.push_back((path.as_ref().to_owned(), inode)); while let Some((path, inode)) = queue.pop_front() { @@ -303,6 +304,7 @@ impl Repository { #[inline] pub fn restore_backup>(&mut self, backup: &Backup, path: P) -> Result<(), RepositoryError> { + let _lock = try!(self.lock(false)); let inode = try!(self.get_inode(&backup.root)); self.restore_inode_tree(inode, path) } @@ -356,6 +358,7 @@ impl Repository { #[allow(dead_code)] pub fn create_backup_recursively>(&mut self, path: P, reference: Option<&Backup>) -> Result { + let _lock = try!(self.lock(false)); let reference_inode = reference.and_then(|b| self.get_inode(&b.root).ok()); let mut backup = Backup::default(); backup.config = self.config.clone(); @@ -383,6 +386,7 @@ impl Repository { } pub fn remove_backup_path>(&mut self, backup: &mut Backup, path: P) -> Result<(), RepositoryError> { + let _lock = try!(self.lock(false)); let mut inodes = try!(self.get_backup_path(backup, path)); let to_remove = inodes.pop().unwrap(); let mut remove_from = match inodes.pop() { diff --git a/src/repository/error.rs b/src/repository/error.rs index 5bbeddb..654a4ee 100644 --- a/src/repository/error.rs +++ b/src/repository/error.rs @@ -79,6 +79,12 @@ quick_error!{ description("Failed to create a backup") display("Repository error: failed to create backup\n\tcaused by: {}", err) } + Lock(err: LockError) { + from() + cause(err) + description("Failed to obtain lock") + display("Repository error: failed to obtain lock\n\tcaused by: {}", err) + } Io(err: io::Error) { from() diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 25333f8..4c402e2 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -37,7 +37,8 @@ pub struct Repository { bundles: BundleDb, content_bundle: Option, meta_bundle: Option, - chunker: Chunker + chunker: Chunker, + locks: LockFolder } @@ -48,6 +49,8 @@ impl Repository { try!(fs::create_dir(path.join("keys"))); let crypto = Arc::new(Mutex::new(try!(Crypto::open(path.join("keys"))))); try!(symlink(remote, path.join("remote"))); + try!(fs::create_dir_all(path.join("remote/locks"))); + let locks = LockFolder::new(path.join("remote/locks")); let bundles = try!(BundleDb::create( path.join("remote/bundles"), path.join("bundles"), @@ -69,13 +72,15 @@ impl Repository { bundles: bundles, content_bundle: None, meta_bundle: None, - crypto: crypto + crypto: crypto, + locks: locks }) } pub fn open>(path: P) -> Result { let path = path.as_ref().to_owned(); let config = try!(Config::load(path.join("config.yaml"))); + let locks = LockFolder::new(path.join("remote/locks")); let crypto = Arc::new(Mutex::new(try!(Crypto::open(path.join("keys"))))); let (bundles, new, gone) = try!(BundleDb::open( path.join("remote/bundles"), @@ -96,6 +101,7 @@ impl Repository { bundles: bundles, content_bundle: None, meta_bundle: None, + locks: locks }; for bundle in new { try!(repo.add_new_remote_bundle(bundle)) @@ -212,6 +218,10 @@ impl Repository { } Ok(()) } + + fn lock(&self, exclusive: bool) -> Result { + Ok(try!(self.locks.lock(exclusive))) + } } impl Drop for Repository { diff --git a/src/repository/vacuum.rs b/src/repository/vacuum.rs index 52c9722..1c62c26 100644 --- a/src/repository/vacuum.rs +++ b/src/repository/vacuum.rs @@ -92,6 +92,8 @@ impl Repository { pub fn vacuum(&mut self, ratio: f32, force: bool) -> Result<(), RepositoryError> { try!(self.flush()); + info!("Locking repository"); + let _lock = try!(self.lock(true)); info!("Analyzing chunk usage"); let usage = try!(self.analyze_usage()); let total = usage.values().map(|b| b.total_size).sum::(); diff --git a/src/util/fs.rs b/src/util/fs.rs index e640151..581edba 100644 --- a/src/util/fs.rs +++ b/src/util/fs.rs @@ -8,7 +8,7 @@ mod linux { pub fn chown>(path: P, uid: libc::uid_t, gid: libc::gid_t) -> Result<(), io::Error> { let path = CString::new(path.as_ref().to_path_buf().into_os_string().into_vec()).unwrap(); - let result = unsafe { libc::chown((&path).as_ptr(), uid, gid) }; + let result = unsafe { libc::lchown((&path).as_ptr(), uid, gid) }; match result { 0 => Ok(()), err => Err(io::Error::from_raw_os_error(err)) diff --git a/src/util/lock.rs b/src/util/lock.rs new file mode 100644 index 0000000..166b4c6 --- /dev/null +++ b/src/util/lock.rs @@ -0,0 +1,156 @@ +use ::prelude::*; + +use serde_yaml; +use chrono::prelude::*; +use libc; + +use std::path::{Path, PathBuf}; +use std::io; +use std::fs::{self, File}; + + +quick_error!{ + #[derive(Debug)] + pub enum LockError { + Io(err: io::Error) { + from() + cause(err) + description("IO error") + display("Lock error: IO error\n\tcaused by: {}", err) + } + Yaml(err: serde_yaml::Error) { + from() + cause(err) + description("Yaml format error") + display("Lock error: yaml format error\n\tcaused by: {}", err) + } + InvalidLockState(reason: &'static str) { + description("Invalid lock state") + display("Lock error: invalid lock state: {}", reason) + } + Locked { + description("Locked") + display("Lock error: locked") + } + } +} + + +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct LockFile { + pub hostname: String, + pub processid: usize, + pub date: i64, + pub exclusive: bool +} +serde_impl!(LockFile(String) { + hostname: String => "hostname", + processid: usize => "processid", + date: i64 => "date", + exclusive: bool => "exclusive" +}); + +impl LockFile { + pub fn load>(path: P) -> Result { + let f = try!(File::open(path)); + Ok(try!(serde_yaml::from_reader(f))) + } + + pub fn save>(&self, path: P) -> Result<(), LockError> { + let mut f = try!(File::create(path)); + Ok(try!(serde_yaml::to_writer(&mut f, &self))) + } +} + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub enum LockLevel { + Free, + Shared, + Exclusive +} + + +pub struct LockHandle { + path: PathBuf +} + +impl LockHandle { + pub fn release(&self) -> Result<(), LockError> { + if self.path.exists() { + try!(fs::remove_file(&self.path)) + } + Ok(()) + } + + pub fn refresh(&self) -> Result<(), LockError> { + let mut file = try!(LockFile::load(&self.path)); + file.date = UTC::now().timestamp(); + file.save(&self.path) + } +} + +impl Drop for LockHandle { + fn drop(&mut self) { + self.release().unwrap() + } +} + + + +pub struct LockFolder { + path: PathBuf +} + +impl LockFolder { + pub fn new>(path: P) -> Self { + LockFolder { path: path.as_ref().to_path_buf() } + } + + fn get_locks(&self) -> Result, LockError> { + let mut locks = vec![]; + for entry in try!(fs::read_dir(&self.path)) { + let entry = try!(entry); + locks.push(try!(LockFile::load(entry.path()))); + } + Ok(locks) + } + + pub fn get_lock_level(&self) -> Result { + let mut level = LockLevel::Free; + for lock in try!(self.get_locks()) { + if lock.exclusive { + if level == LockLevel::Exclusive { + return Err(LockError::InvalidLockState("multiple exclusive locks")) + } else { + level = LockLevel::Exclusive + } + } else if level == LockLevel::Exclusive { + return Err(LockError::InvalidLockState("exclusive lock and shared locks")) + } else { + level = LockLevel::Shared + } + } + Ok(level) + } + + pub fn lock(&self, exclusive: bool) -> Result { + let level = try!(self.get_lock_level()); + if level == LockLevel::Exclusive || level == LockLevel::Shared && exclusive { + return Err(LockError::Locked) + } + let lockfile = LockFile { + hostname: get_hostname().unwrap(), + processid: unsafe { libc::getpid() } as usize, + date: UTC::now().timestamp(), + exclusive: exclusive + }; + let path = self.path.join(format!("{}-{}.lock", &lockfile.hostname, lockfile.processid)); + try!(lockfile.save(&path)); + let handle = LockHandle{path: path}; + if self.get_lock_level().is_err() { + try!(handle.release()); + return Err(LockError::Locked) + } + Ok(handle) + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 4fa6553..c162fe5 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -9,6 +9,7 @@ mod hex; mod cli; mod hostname; mod fs; +mod lock; pub mod msgpack; pub use self::fs::*; @@ -21,3 +22,4 @@ pub use self::bitmap::*; pub use self::hex::*; pub use self::cli::*; pub use self::hostname::*; +pub use self::lock::*;