use std::{
fs::{File, OpenOptions, create_dir, create_dir_all, remove_dir, remove_dir_all, remove_file},
io,
ops::{Deref, DerefMut},
path::Path,
};
#[derive(Debug)]
pub struct FileLockGuard {
file: File,
}
impl Drop for FileLockGuard {
fn drop(&mut self) {}
}
impl Deref for FileLockGuard {
type Target = File;
fn deref(&self) -> &Self::Target {
&self.file
}
}
impl DerefMut for FileLockGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.file
}
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
pub enum MkdirOptions {
WithoutParents,
WithParents,
}
mod sealed {
use std::path::Path;
pub trait Sealed {}
impl Sealed for Path {}
}
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
pub enum ShouldBlock {
No,
Yes,
}
pub trait PathExt: sealed::Sealed {
fn touch(&self) -> io::Result<File>;
fn mkdir(&self, opts: MkdirOptions) -> io::Result<()>;
fn rmdir(&self) -> io::Result<()>;
fn rmtree(&self) -> io::Result<()>;
fn rm(&self) -> io::Result<()>;
fn lock(&self, should_block: ShouldBlock) -> io::Result<FileLockGuard>;
fn lock_shared(&self, should_block: ShouldBlock) -> io::Result<FileLockGuard>;
}
impl PathExt for Path {
fn touch(&self) -> io::Result<File> {
if let Some(parent) = self.parent() {
parent.mkdir(MkdirOptions::WithParents)?;
}
let mut opts = OpenOptions::new();
opts.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
if let Ok(metadata) = self.metadata() {
const MASK: u32 = 0o666;
let permissions = metadata.permissions().mode();
opts.mode(permissions & MASK);
}
}
opts.open(self)
}
fn mkdir(&self, opts: MkdirOptions) -> io::Result<()> {
match opts {
MkdirOptions::WithoutParents => create_dir(self),
MkdirOptions::WithParents => create_dir_all(self),
}
}
fn rmdir(&self) -> io::Result<()> {
remove_dir(self)
}
fn rmtree(&self) -> io::Result<()> {
remove_dir_all(self)
}
fn rm(&self) -> io::Result<()> {
remove_file(self)
}
fn lock(&self, should_block: ShouldBlock) -> io::Result<FileLockGuard> {
let file = self.touch()?;
let result = if matches!(should_block, ShouldBlock::Yes) {
file.lock()
} else {
file.try_lock().map_err(|err| match err {
std::fs::TryLockError::Error(error) => error,
std::fs::TryLockError::WouldBlock => io::Error::from(io::ErrorKind::WouldBlock),
})
};
result.map(|_| FileLockGuard { file })
}
fn lock_shared(&self, should_block: ShouldBlock) -> io::Result<FileLockGuard> {
let file = self.touch()?;
let result = if matches!(should_block, ShouldBlock::Yes) {
file.lock_shared()
} else {
file.try_lock_shared().map_err(|err| match err {
std::fs::TryLockError::Error(error) => error,
std::fs::TryLockError::WouldBlock => io::Error::from(io::ErrorKind::WouldBlock),
})
};
result.map(|_| FileLockGuard { file })
}
}
#[cfg(test)]
mod tests {
use super::*;
use claim::{assert_err, assert_ok};
use tempfile::tempdir;
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Duration;
use tempfile::NamedTempFile;
#[test]
fn create_new_file_should_work() {
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
assert_ok!(new_file.touch());
}
#[test]
fn create_dirs() {
{
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
new_file.push("y");
assert_ok!(new_file.mkdir(MkdirOptions::WithParents));
}
{
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
assert_ok!(new_file.mkdir(MkdirOptions::WithParents));
}
{
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
assert_ok!(new_file.mkdir(MkdirOptions::WithoutParents));
}
{
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
new_file.push("y");
assert_err!(new_file.mkdir(MkdirOptions::WithoutParents));
}
}
#[test]
fn lock_blocking_should_work() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let lock = assert_ok!(path.lock(ShouldBlock::Yes));
drop(lock);
}
#[test]
fn lock_non_blocking_should_work() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let lock = assert_ok!(path.lock(ShouldBlock::No));
drop(lock);
}
#[test]
fn lock_shared_blocking_should_work() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let lock = assert_ok!(path.lock_shared(ShouldBlock::Yes));
drop(lock);
}
#[test]
fn lock_shared_non_blocking_should_work() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let lock = assert_ok!(path.lock_shared(ShouldBlock::No));
drop(lock);
}
#[test]
fn multiple_shared_locks_can_coexist() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let lock1 = assert_ok!(path.lock_shared(ShouldBlock::Yes));
let lock2 = assert_ok!(path.lock_shared(ShouldBlock::No));
let lock3 = assert_ok!(path.lock_shared(ShouldBlock::No));
drop(lock1);
drop(lock2);
drop(lock3);
}
#[test]
fn exclusive_lock_prevents_another_exclusive_lock() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let _lock1 = assert_ok!(path.lock(ShouldBlock::Yes));
let result = path.lock(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
}
#[test]
fn exclusive_lock_prevents_shared_lock() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let _lock1 = assert_ok!(path.lock(ShouldBlock::Yes));
let result = path.lock_shared(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
}
#[test]
fn shared_lock_prevents_exclusive_lock() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let _lock1 = assert_ok!(path.lock_shared(ShouldBlock::Yes));
let result = path.lock(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
}
#[test]
fn multithreaded_exclusive_locks_are_mutually_exclusive() {
let lockfile = NamedTempFile::new().expect("needed for tests");
let lockpath = Arc::new(lockfile.path().to_path_buf());
let barrier = Arc::new(Barrier::new(2));
let lockpath_clone = Arc::clone(&lockpath);
let barrier_clone = Arc::clone(&barrier);
let handle = thread::spawn(move || {
let lock = assert_ok!(lockpath_clone.as_path().lock(ShouldBlock::Yes));
barrier_clone.wait();
thread::sleep(Duration::from_millis(100));
drop(lock);
});
barrier.wait();
thread::sleep(Duration::from_millis(10));
let result = lockpath.as_path().lock(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
handle.join().expect("thread should not panic");
assert_ok!(lockpath.as_path().lock(ShouldBlock::No));
}
#[test]
fn multithreaded_shared_locks_can_coexist() {
let lockfile = NamedTempFile::new().expect("needed for tests");
let lockpath = Arc::new(lockfile.path().to_path_buf());
let barrier = Arc::new(Barrier::new(3));
let mut handles = vec![];
for _ in 0..2 {
let lockpath_clone = Arc::clone(&lockpath);
let barrier_clone = Arc::clone(&barrier);
let handle = thread::spawn(move || {
let _lock = assert_ok!(lockpath_clone.as_path().lock_shared(ShouldBlock::Yes));
barrier_clone.wait();
});
handles.push(handle);
}
let _lock = assert_ok!(lockpath.as_path().lock_shared(ShouldBlock::Yes));
barrier.wait();
for handle in handles {
handle.join().expect("thread should not panic");
}
}
#[test]
fn multithreaded_exclusive_lock_blocks_shared_locks() {
let lockfile = NamedTempFile::new().expect("needed for tests");
let lockpath = Arc::new(lockfile.path().to_path_buf());
let barrier = Arc::new(Barrier::new(2));
let lockpath_clone = Arc::clone(&lockpath);
let barrier_clone = Arc::clone(&barrier);
let handle = thread::spawn(move || {
let lock = assert_ok!(lockpath_clone.as_path().lock(ShouldBlock::Yes));
barrier_clone.wait();
thread::sleep(Duration::from_millis(100));
drop(lock);
});
barrier.wait();
thread::sleep(Duration::from_millis(10));
let result = lockpath.as_path().lock_shared(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
handle.join().expect("thread should not panic");
assert_ok!(lockpath.as_path().lock_shared(ShouldBlock::No));
}
#[test]
fn multithreaded_shared_lock_blocks_exclusive_lock() {
let lockfile = NamedTempFile::new().expect("needed for tests");
let lockpath = Arc::new(lockfile.path().to_path_buf());
let barrier = Arc::new(Barrier::new(2));
let lockpath_clone = Arc::clone(&lockpath);
let barrier_clone = Arc::clone(&barrier);
let handle = thread::spawn(move || {
let lock = assert_ok!(lockpath_clone.as_path().lock_shared(ShouldBlock::Yes));
barrier_clone.wait();
thread::sleep(Duration::from_millis(100));
drop(lock);
});
barrier.wait();
thread::sleep(Duration::from_millis(10));
let result = lockpath.as_path().lock(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
handle.join().expect("thread should not panic");
assert_ok!(lockpath.as_path().lock(ShouldBlock::No));
}
#[test]
fn multithreaded_multiple_shared_then_exclusive() {
let lockfile = NamedTempFile::new().expect("needed for tests");
let lockpath = Arc::new(lockfile.path().to_path_buf());
let barrier = Arc::new(Barrier::new(4));
let mut handles = vec![];
for _ in 0..3 {
let lockpath_clone = Arc::clone(&lockpath);
let barrier_clone = Arc::clone(&barrier);
let handle = thread::spawn(move || {
let lock = assert_ok!(lockpath_clone.as_path().lock_shared(ShouldBlock::Yes));
barrier_clone.wait();
thread::sleep(Duration::from_millis(50));
drop(lock);
});
handles.push(handle);
}
barrier.wait();
thread::sleep(Duration::from_millis(10));
let result = lockpath.as_path().lock(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
for handle in handles {
handle.join().expect("thread should not panic");
}
assert_ok!(lockpath.as_path().lock(ShouldBlock::Yes));
}
}