diff --git a/src/lib.rs b/src/lib.rs index b18bbc1..5e65526 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,7 +48,15 @@ //! [`AdvisoryFileLock`]: struct.AdvisoryFileLock.html //! [`RwLock`]: https://doc.rust-lang.org/stable/std/sync/struct.RwLock.html //! [`File`]: https://doc.rust-lang.org/stable/std/fs/struct.File.html -use std::{fmt, io}; +use std::{ + fmt, + fs::{File, OpenOptions}, + io::{self, Read, Seek, Write}, + mem::ManuallyDrop, + ops::Deref, + path::Path, + ptr::addr_of, +}; #[cfg(windows)] mod windows; @@ -112,6 +120,192 @@ pub trait AdvisoryFileLock { fn unlock(&self) -> Result<(), FileLockError>; } +/// A guard to a locked file that will automatically unlock it's underlying file when dropped. +/// +/// ## Example +/// ```rust +/// use std::fs::File; +/// use advisory_lock::{LockGuard, FileLockMode, OpenOptionsExt}; +/// +/// // Opens the file with an exclusive lock +/// let mut file: LockGuard = File::options() +/// .write(true) +/// .truncate(true) +/// .open_locked("test_file.txt", FileLockMode::Exclusive) +/// .unwrap(); +/// +/// file.write(b"Hello world!").unwrap(); +/// // When the `file` variable drops, it's underlying file will be unlocked +/// ``` +#[derive(Debug)] +pub struct LockGuard { + inner: T, +} + +impl LockGuard { + /// Acquire the advisory file lock. + /// + /// `lock` is blocking; it will block the current thread until it succeeds or errors. + pub fn lock(file: T, file_lock_mode: FileLockMode) -> std::io::Result { + match file.lock(file_lock_mode) { + Ok(()) => Ok(Self { inner: file }), + Err(FileLockError::AlreadyLocked) => Err(std::io::ErrorKind::PermissionDenied.into()), + Err(FileLockError::Io(e)) => Err(e), + } + } + + /// Try to acquire the advisory file lock. + /// + /// `try_lock` returns immediately. + pub fn try_lock(file: T, file_lock_mode: FileLockMode) -> std::io::Result> { + match file.try_lock(file_lock_mode) { + Ok(()) => Ok(Some(Self { inner: file })), + Err(FileLockError::AlreadyLocked) => Ok(None), + Err(FileLockError::Io(e)) => Err(e), + } + } + + /// Returns the underlying file handle, unlocking the file in the process. + pub fn try_unlock(self) -> Result { + if let Err(e) = self.inner.unlock() { + return Err((e, self)); + } + + let this = ManuallyDrop::new(self); + return Ok(unsafe { std::ptr::read(addr_of!(this.inner)) }); + } +} + +impl Read for LockGuard { + #[inline] + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.inner.read(buf) + } +} + +impl Write for LockGuard { + #[inline] + fn write(&mut self, buf: &[u8]) -> io::Result { + self.inner.write(buf) + } + + #[inline] + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } +} + +impl Seek for LockGuard { + #[inline] + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + self.inner.seek(pos) + } +} + +impl Deref for LockGuard { + type Target = T; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Drop for LockGuard { + #[inline] + fn drop(&mut self) { + let _ = self.inner.unlock(); + } +} + +/// Extended file locking funtionality for [`File`] +pub trait FileExt { + /// Opens a file and acquires the advisory file lock. + /// + /// See [`open`](std::fs::File::open) and [`lock`](AdvisoryFileLock::lock). + fn open_locked

(path: P, file_lock_mode: FileLockMode) -> std::io::Result> + where + P: AsRef; + + /// Opens a file and tries to acquire the advisory file lock. + /// + /// See [`open`](std::fs::File::open) and [`try_lock`](AdvisoryFileLock::try_lock). + fn try_open_locked

( + path: P, + file_lock_mode: FileLockMode, + ) -> std::io::Result>> + where + P: AsRef; +} + +impl FileExt for File { + fn open_locked

(path: P, file_lock_mode: FileLockMode) -> std::io::Result> + where + P: AsRef, + { + return LockGuard::lock(File::open(path)?, file_lock_mode); + } + + fn try_open_locked

( + path: P, + file_lock_mode: FileLockMode, + ) -> std::io::Result>> + where + P: AsRef, + { + return LockGuard::try_lock(File::open(path)?, file_lock_mode); + } +} + +/// Extended file locking funtionality for [`OpenOptions`] +pub trait OpenOptionsExt { + /// Opens a file and acquires the advisory file lock. + /// + /// See [`open`](std::fs::File::open) and [`lock`](AdvisoryFileLock::lock). + fn open_locked

( + &self, + path: P, + file_lock_mode: FileLockMode, + ) -> std::io::Result> + where + P: AsRef; + + /// Opens a file and tries to acquire the advisory file lock. + /// + /// See [`open`](std::fs::File::open) and [`try_lock`](AdvisoryFileLock::try_lock). + fn try_open_locked

( + &self, + path: P, + file_lock_mode: FileLockMode, + ) -> std::io::Result>> + where + P: AsRef; +} + +impl OpenOptionsExt for OpenOptions { + fn open_locked

( + &self, + path: P, + file_lock_mode: FileLockMode, + ) -> std::io::Result> + where + P: AsRef, + { + return LockGuard::lock(self.open(path)?, file_lock_mode); + } + + fn try_open_locked

( + &self, + path: P, + file_lock_mode: FileLockMode, + ) -> std::io::Result>> + where + P: AsRef, + { + return LockGuard::try_lock(self.open(path)?, file_lock_mode); + } +} + #[cfg(test)] mod tests { use super::*; @@ -176,4 +370,18 @@ mod tests { } std::fs::remove_file(&test_file).unwrap(); } + + #[test] + fn simple_lock_guard() { + let mut test_file = temp_dir(); + test_file.push("lock_guard"); + File::create(&test_file).unwrap(); + { + let _f1 = File::open_locked(&test_file, FileLockMode::Exclusive).unwrap(); + assert!(File::try_open_locked(&test_file, FileLockMode::Shared) + .unwrap() + .is_none()); + } + std::fs::remove_file(&test_file).unwrap(); + } }