rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};

#[derive(Debug)]
pub struct FileLock {
    path: PathBuf,
    held: bool,
}

impl FileLock {
    pub fn acquire(path: impl Into<PathBuf>) -> io::Result<Self> {
        let path = path.into();
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }

        let mut lock_file = OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(&path)?;
        write!(lock_file, "{}", std::process::id())?;

        Ok(Self { path, held: true })
    }

    #[must_use]
    pub fn path(&self) -> &Path {
        &self.path
    }

    #[must_use]
    pub fn is_held(&self) -> bool {
        self.held
    }

    pub fn release(&mut self) -> io::Result<()> {
        if self.held {
            fs::remove_file(&self.path)?;
            self.held = false;
        }
        Ok(())
    }
}

impl Drop for FileLock {
    fn drop(&mut self) {
        let _ = self.release();
    }
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::{Path, PathBuf};
    use std::process;
    use std::sync::atomic::{AtomicU64, Ordering};
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::FileLock;

    static UNIQUE_COUNTER: AtomicU64 = AtomicU64::new(0);

    struct TestDir {
        path: PathBuf,
    }

    impl TestDir {
        fn new() -> Self {
            let nanos = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .expect("clock should be after epoch")
                .as_nanos();
            let counter = UNIQUE_COUNTER.fetch_add(1, Ordering::Relaxed);
            let path = std::env::temp_dir().join(format!(
                "rjango-file-lock-test-{}-{nanos}-{counter}",
                process::id()
            ));
            fs::create_dir_all(&path).expect("create test directory");
            Self { path }
        }

        fn path(&self) -> &Path {
            &self.path
        }
    }

    impl Drop for TestDir {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.path);
        }
    }

    #[test]
    fn acquire_creates_lock_file() {
        let dir = TestDir::new();
        let lock_path = dir.path().join("upload.lock");

        let lock = FileLock::acquire(lock_path.clone()).expect("acquire lock");

        assert!(lock.is_held());
        assert_eq!(lock.path(), lock_path.as_path());
        assert!(lock_path.exists());
    }

    #[test]
    fn second_acquire_fails_while_lock_is_held() {
        let dir = TestDir::new();
        let lock_path = dir.path().join("upload.lock");
        let first_lock = FileLock::acquire(lock_path.clone()).expect("acquire first lock");

        let error = FileLock::acquire(lock_path.clone()).expect_err("expected duplicate lock");

        assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
        drop(first_lock);
    }

    #[test]
    fn release_removes_lock_file() {
        let dir = TestDir::new();
        let lock_path = dir.path().join("upload.lock");
        let mut lock = FileLock::acquire(lock_path.clone()).expect("acquire lock");

        lock.release().expect("release lock");

        assert!(!lock.is_held());
        assert!(!lock_path.exists());
    }
}