extern crate tempfile;
use std::fs::{remove_file, File, Metadata, Permissions};
use std::io::{Error, ErrorKind, Read, Result, Seek, SeekFrom, Write};
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::thread::sleep;
use std::time::{Duration, SystemTime};
use tempfile::Builder;
const DEFAULT_PAUSE: Duration = Duration::from_secs(5);
const DEFAULT_TRIES: usize = 10;
fn meta_eq(a: &Metadata, b: &Metadata) -> bool {
a.dev() == b.dev() && a.ino() == b.ino()
}
#[derive(Debug)]
pub struct Dotlock {
file: File,
path: Option<PathBuf>,
}
impl Dotlock {
fn create_in(path: &Path, options: DotlockOptions, tempdir: &Path) -> Result<File> {
let mut trynum = 0;
loop {
let temp = Builder::new().tempfile_in(tempdir)?;
let tempmeta = temp.as_file().metadata()?;
std::fs::hard_link(temp.path(), &path).ok();
let temp = temp.into_file();
let destmeta = match std::fs::metadata(&path) {
Ok(meta) => meta,
Err(_) => continue,
};
if meta_eq(&destmeta, &tempmeta) {
if let Some(perm) = options.permissions {
temp.set_permissions(perm)?;
}
break Ok(temp);
}
if let Some(stale_age) = options.stale_age {
let now = SystemTime::now();
if let Ok(modtime) = destmeta.modified() {
if let Ok(age) = now.duration_since(modtime) {
if age >= stale_age {
remove_file(&path).ok();
continue;
}
}
}
}
trynum += 1;
if trynum >= options.tries {
break Err(Error::new(ErrorKind::TimedOut, "Timed out"));
}
sleep(options.pause);
}
}
fn create_with(path: PathBuf, options: DotlockOptions) -> Result<Self> {
let file = Self::create_in(&path, options, &path.parent().unwrap_or(Path::new(".")))?;
Ok(Self {
file,
path: Some(path),
})
}
pub fn create<T: Into<PathBuf>>(path: T) -> Result<Self> {
DotlockOptions::new().create(path.into())
}
pub fn unlock(&mut self) -> Result<()> {
self.path.take().map_or(Ok(()), |path| remove_file(path))
}
pub fn sync_all(&self) -> Result<()> {
self.file.sync_all()
}
pub fn sync_data(&self) -> Result<()> {
self.file.sync_all()
}
pub fn set_len(&self, size: u64) -> Result<()> {
self.file.set_len(size)
}
pub fn metadata(&self) -> Result<Metadata> {
self.file.metadata()
}
pub fn set_permissions(&self, perm: Permissions) -> Result<()> {
self.file.set_permissions(perm)
}
}
impl Drop for Dotlock {
fn drop(&mut self) {
self.unlock().ok();
}
}
impl Read for Dotlock {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
self.file.read(buf)
}
}
impl Seek for Dotlock {
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
self.file.seek(pos)
}
}
impl Write for Dotlock {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
self.file.write(buf)
}
fn flush(&mut self) -> Result<()> {
self.file.flush()
}
}
#[derive(Debug)]
pub struct DotlockOptions {
pause: Duration,
tries: usize,
permissions: Option<Permissions>,
stale_age: Option<Duration>,
}
impl DotlockOptions {
pub fn new() -> Self {
Self {
pause: DEFAULT_PAUSE,
tries: DEFAULT_TRIES,
permissions: None,
stale_age: None,
}
}
pub fn pause<T: Into<Duration>>(mut self, pause: T) -> Self {
self.pause = pause.into();
self
}
pub fn tries(mut self, tries: usize) -> Self {
self.tries = tries.max(1);
self
}
pub fn permissions(mut self, perm: Permissions) -> Self {
self.permissions = Some(perm);
self
}
pub fn stale_age<T: Into<Duration>>(mut self, age: T) -> Self {
self.stale_age = Some(age.into());
self
}
pub fn create<T: Into<PathBuf>>(self, path: T) -> Result<Dotlock> {
Dotlock::create_with(path.into(), self)
}
}