use std::{
fs::{
File, Metadata, OpenOptions, Permissions, ReadDir, canonicalize, copy, create_dir,
create_dir_all, exists, hard_link, metadata, read, read_dir, read_link, read_to_string,
remove_dir, remove_dir_all, remove_file, rename, set_permissions, symlink_metadata, write,
},
io::{self},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
};
#[derive(Debug)]
pub struct FileLockGuard {
file: File,
}
impl Drop for FileLockGuard {
fn drop(&mut self) {
drop(self.file.unlock())
}
}
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>;
fn is_executable(&self) -> bool;
fn metadata(&self) -> io::Result<Metadata>;
fn canonicalize(&self) -> io::Result<PathBuf>;
fn copy_to(&self, to: impl AsRef<Path>) -> io::Result<u64>;
fn exists(&self) -> io::Result<bool>;
fn hard_link_to(&self, to: impl AsRef<Path>) -> io::Result<()>;
fn read(&self) -> io::Result<Vec<u8>>;
fn read_dir(&self) -> io::Result<ReadDir>;
fn read_link(&self) -> io::Result<PathBuf>;
fn read_to_string(&self) -> io::Result<String>;
fn rename_to(&self, to: impl AsRef<Path>) -> io::Result<()>;
fn set_permissions(&self, p: Permissions) -> io::Result<()>;
fn symlink_metadata(&self) -> io::Result<Metadata>;
fn write(&self, content: impl AsRef<[u8]>) -> io::Result<()>;
}
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(unix)]
fn is_executable(&self) -> bool {
use std::os::unix::prelude::*;
self.metadata()
.map(|metadata| {
const EXEC_MASK: u32 = (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) as u32;
const _: () = assert!(EXEC_MASK == 0o111, "bits mismatch");
metadata.is_file() && metadata.permissions().mode() & EXEC_MASK != 0
})
.unwrap_or(false)
}
#[cfg(windows)]
fn is_executable(&self) -> bool {
self.is_file()
}
#[cfg(not(any(unix, windows)))]
fn is_executable(&self) -> bool {
false
}
fn metadata(&self) -> io::Result<Metadata> {
metadata(self)
}
fn canonicalize(&self) -> io::Result<PathBuf> {
canonicalize(self)
}
fn copy_to(&self, to: impl AsRef<Path>) -> io::Result<u64> {
copy(self, to)
}
fn exists(&self) -> io::Result<bool> {
exists(self)
}
fn hard_link_to(&self, to: impl AsRef<Path>) -> io::Result<()> {
hard_link(self, to)
}
fn read(&self) -> io::Result<Vec<u8>> {
read(self)
}
fn read_dir(&self) -> io::Result<ReadDir> {
read_dir(self)
}
fn read_link(&self) -> io::Result<PathBuf> {
read_link(self)
}
fn read_to_string(&self) -> io::Result<String> {
read_to_string(self)
}
fn rename_to(&self, to: impl AsRef<Path>) -> io::Result<()> {
rename(self, to)
}
fn set_permissions(&self, permissions: Permissions) -> io::Result<()> {
set_permissions(self, permissions)
}
fn symlink_metadata(&self) -> io::Result<Metadata> {
symlink_metadata(self)
}
fn write(&self, contents: impl AsRef<[u8]>) -> io::Result<()> {
write(self, contents)
}
}
#[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));
}
}