#![forbid(unsafe_code)]
use std::{
fs::{self, File, OpenOptions},
io, fmt,
path::{Path, PathBuf}
};
#[cfg(not(feature = "log"))]
macro_rules! warn {
($($_:tt)*) => {()};
}
#[cfg(not(feature = "log"))]
macro_rules! debug {
($($_:tt)*) => {()};
}
#[cfg(feature = "log")]
use log::{debug, warn};
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Io(io::Error),
LockTaken,
}
impl Error {
pub fn into_inner(self) -> io::Error {
match self {
Error::Io(err) => err,
Error::LockTaken => io::Error::new(io::ErrorKind::AlreadyExists, "lockfile already exists"),
}
}
fn from_io(e: io::Error) -> Self {
match e.kind() {
io::ErrorKind::AlreadyExists => Error::LockTaken,
_ => Error::Io(e),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Io(err) => fmt::Display::fmt(err, f),
Error::LockTaken => f.write_str("lockfile already exists")
}
}
}
impl std::error::Error for Error {}
#[derive(Debug)]
pub struct Lockfile {
handle: Option<File>,
path: PathBuf,
}
impl Lockfile {
pub fn create(path: impl AsRef<Path>) -> Result<Lockfile, Error> {
let path = path.as_ref();
let mut lockfile_opts = OpenOptions::new();
lockfile_opts.create_new(true).read(true).write(true);
let lockfile = lockfile_opts.open(path).map_err(Error::from_io)?;
debug!(r#"lockfile created at "{}""#, path.display());
Ok(Lockfile {
handle: Some(lockfile),
path: path.to_owned(),
})
}
pub fn create_with_parents(path: impl AsRef<Path>) -> Result<Lockfile, Error> {
let path = path.as_ref();
let dir = path.parent().ok_or_else(|| Error::from_io(io::Error::new(io::ErrorKind::InvalidInput, "lockfile path must have a parent")))?;
fs::create_dir_all(dir).map_err(Error::Io)?;
debug!(
r#"lockfile parent directories created/found at "{}""#,
dir.display()
);
Self::create(path)
}
#[inline]
pub fn path(&self) -> &Path {
self.path.as_path()
}
pub fn release(mut self) -> Result<(), io::Error> {
self.handle.take().expect("handle already dropped");
fs::remove_file(&self.path)?;
debug!(r#"Removed lockfile at "{}""#, self.path.display());
Ok(())
}
}
impl Drop for Lockfile {
fn drop(&mut self) {
if let Some(handle) = self.handle.take() {
drop(handle);
#[allow(unused_variables)]
match fs::remove_file(&self.path) {
Ok(()) => debug!(r#"Removed lockfile at "{}""#, self.path.display()),
Err(e) =>
warn!(
r#"could not remove lockfile at "{}": {}"#,
self.path.display(),
e
)
,
}
}
}
}
impl AsRef<Path> for Lockfile {
#[inline]
fn as_ref(&self) -> &Path {
self.path.as_ref()
}
}
impl io::Read for Lockfile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
io::Read::read(&mut self.handle.as_ref().unwrap(), buf)
}
}
impl<'a> io::Read for &'a Lockfile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
io::Read::read(&mut self.handle.as_ref().unwrap(), buf)
}
}
impl io::Write for Lockfile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
io::Write::write(&mut self.handle.as_ref().unwrap(), buf)
}
fn flush(&mut self) -> io::Result<()> {
io::Write::flush(&mut self.handle.as_ref().unwrap())
}
}
impl<'a> io::Write for &'a Lockfile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
io::Write::write(&mut self.handle.as_ref().unwrap(), buf)
}
fn flush(&mut self) -> io::Result<()> {
io::Write::flush(&mut self.handle.as_ref().unwrap())
}
}
impl io::Seek for Lockfile {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
io::Seek::seek(&mut self.handle.as_ref().unwrap(), pos)
}
}
impl<'a> io::Seek for &'a Lockfile {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
io::Seek::seek(&mut self.handle.as_ref().unwrap(), pos)
}
}
#[cfg(test)]
mod tests {
extern crate tempfile;
use self::tempfile::NamedTempFile;
use super::{Error, Lockfile};
use std::fs;
use std::io;
use std::path::PathBuf;
fn tmp_path() -> PathBuf {
NamedTempFile::new().unwrap().into_temp_path().to_owned()
}
#[test]
fn smoke() {
let path = tmp_path();
let lockfile = Lockfile::create(&path).unwrap();
assert_eq!(lockfile.path(), path);
lockfile.release().unwrap();
assert_eq!(
fs::metadata(path).unwrap_err().kind(),
io::ErrorKind::NotFound
);
}
#[test]
fn lock_twice() {
let path = tmp_path();
let _lockfile = Lockfile::create(&path).unwrap();
assert!(matches!(
Lockfile::create(&path).unwrap_err(),
Error::LockTaken
));
}
}