use anyhow::Context;
use fd_lock;
use std::ffi::OsString;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, ErrorKind, Write};
use std::path::PathBuf;
use tracing::*;
#[derive(Debug)]
pub struct SeqFile<'a> {
path: PathBuf,
next: u64,
#[allow(dead_code)]
lock: fd_lock::RwLockWriteGuard<'a, File>,
}
#[instrument(level = "debug")]
pub fn prepare_seqfile_lock(
path: &OsString,
create_if_missing: bool,
) -> Result<fd_lock::RwLock<File>, anyhow::Error> {
let mut lockpath = path.clone();
lockpath.push(".lock");
debug!("Attempting to prepare lock at {:?}", lockpath);
let lockfile = OpenOptions::new()
.read(true)
.write(true)
.create(create_if_missing)
.open(&lockpath)
.context(format!(
"Opening lock file at {:?}; if missing, this may be an append-only queue",
lockpath
))?;
let lock = fd_lock::RwLock::new(lockfile);
Ok(lock)
}
impl<'a> SeqFile<'a> {
#[instrument(level = "debug", skip(lock))]
pub fn open(
path: &OsString,
lock: &'a mut fd_lock::RwLock<File>,
) -> Result<Self, anyhow::Error> {
let path = PathBuf::from(path);
debug!("Attempting to acquire write lock");
let retval = Self {
path,
lock: lock.try_write()?,
next: 1,
};
debug!("Attempting to open file {:?}", retval.path);
let file = match File::open(&retval.path) {
Err(e) => {
if e.kind() == ErrorKind::NotFound {
retval.write()?;
return Ok(retval);
} else {
return Err(anyhow::Error::from(e));
}
}
Ok(f) => f,
};
let next = SeqFile::read(file)?;
Ok(Self { next, ..retval })
}
pub fn set(&mut self, next: u64) -> Result<u64, anyhow::Error> {
let retval = self.next;
self.next = next;
self.write()?;
Ok(retval)
}
pub fn increment(&mut self) -> Result<u64, anyhow::Error> {
self.set(self.next + 1)
}
pub fn get_next(&self) -> u64 {
self.next
}
fn read(file: File) -> Result<u64, anyhow::Error> {
let mut br = BufReader::new(file);
let mut buf = String::new();
br.read_line(&mut buf)?;
let res: u64 = buf.trim().parse()?;
debug!("Read next ID {} from seqfile", res);
Ok(res)
}
fn write(&self) -> Result<(), anyhow::Error> {
let mut tmppath = OsString::from(self.path.clone());
tmppath.push(".temp");
trace!("Writing {} to {:?}", self.next, tmppath);
let mut file = File::create(PathBuf::from(&tmppath))?;
writeln!(file, "{}", self.next)?;
file.flush()?;
file.sync_all()?;
std::mem::drop(file);
trace!("Renaming {:?} to {:?}", &tmppath, self.path);
std::fs::rename(tmppath, &self.path)?;
debug!("Sequence {} written to {:?}", self.next, self.path);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn basic() {
let dir = tempdir().unwrap();
let path = OsString::from(dir.path().join("seqfile"));
{
let mut lock = prepare_seqfile_lock(&path, true).unwrap();
let mut sf = SeqFile::open(&path, &mut lock).unwrap();
assert_eq!(sf.get_next(), 1);
sf.increment().unwrap();
sf.increment().unwrap();
assert_eq!(sf.get_next(), 3);
let mut lock2 = prepare_seqfile_lock(&path, false).unwrap();
let _ = SeqFile::open(&path, &mut lock2).unwrap_err();
}
{
let mut lock = prepare_seqfile_lock(&path, false).unwrap();
let sf = SeqFile::open(&path, &mut lock).unwrap();
assert_eq!(sf.get_next(), 3);
}
File::create(&path).unwrap();
{
let mut lock = prepare_seqfile_lock(&path, false).unwrap();
let _sf = SeqFile::open(&path, &mut lock).unwrap_err();
}
}
}