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};
use std::process;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

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

pub struct TempFile {
    path: PathBuf,
    persisted: bool,
}

impl TempFile {
    pub fn new(prefix: &str, suffix: &str) -> io::Result<Self> {
        let path = unique_temp_path(prefix, suffix);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(&path)?;
        Ok(Self {
            path,
            persisted: false,
        })
    }

    pub fn with_content(prefix: &str, suffix: &str, content: &[u8]) -> io::Result<Self> {
        let mut file = Self::new(prefix, suffix)?;
        file.write_all(content)?;
        Ok(file)
    }

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

    pub fn read_all(&self) -> io::Result<Vec<u8>> {
        fs::read(&self.path)
    }

    pub fn write_all(&mut self, content: &[u8]) -> io::Result<()> {
        let mut file = OpenOptions::new()
            .write(true)
            .truncate(true)
            .open(&self.path)?;
        file.write_all(content)
    }

    pub fn persist(mut self, destination: impl AsRef<Path>) -> io::Result<PathBuf> {
        let destination = destination.as_ref().to_path_buf();
        if let Some(parent) = destination.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::rename(&self.path, &destination)?;
        self.persisted = true;
        Ok(destination)
    }
}

impl Drop for TempFile {
    fn drop(&mut self) {
        if !self.persisted {
            let _ = fs::remove_file(&self.path);
        }
    }
}

fn unique_temp_path(prefix: &str, suffix: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock should be after epoch")
        .as_nanos();
    let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
    let file_name = format!("{prefix}-{}-{nanos}-{counter}{suffix}", process::id());
    std::env::temp_dir().join(file_name)
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::PathBuf;

    use super::TempFile;

    #[test]
    fn temp_file_new_creates_file_on_disk() {
        let temp_file = TempFile::new("rjango-temp", ".tmp").expect("create temp file");

        assert!(temp_file.path().exists());
    }

    #[test]
    fn temp_file_with_content_reads_back_bytes() {
        let temp_file = TempFile::with_content("rjango-temp", ".txt", b"hello")
            .expect("create temp file with content");

        assert_eq!(temp_file.read_all().expect("read temp file"), b"hello");
    }

    #[test]
    fn temp_file_write_all_replaces_existing_content() {
        let mut temp_file = TempFile::with_content("rjango-temp", ".txt", b"before")
            .expect("create temp file with content");

        temp_file
            .write_all(b"after")
            .expect("write replacement content");

        assert_eq!(temp_file.read_all().expect("read temp file"), b"after");
    }

    #[test]
    fn temp_file_persist_moves_file_to_destination() {
        let temp_file = TempFile::with_content("rjango-temp", ".txt", b"persist")
            .expect("create temp file with content");
        let destination = std::env::temp_dir().join("rjango-persisted-file.txt");
        if destination.exists() {
            fs::remove_file(&destination).expect("remove stale persisted file");
        }

        let persisted_path = temp_file.persist(&destination).expect("persist temp file");

        assert_eq!(persisted_path, destination);
        assert_eq!(
            fs::read(&persisted_path).expect("read persisted file"),
            b"persist"
        );
        fs::remove_file(persisted_path).expect("cleanup persisted file");
    }

    #[test]
    fn dropping_non_persisted_temp_file_removes_it() {
        let path = {
            let temp_file = TempFile::new("rjango-temp", ".tmp").expect("create temp file");
            temp_file.path().to_path_buf()
        };

        assert!(!path.exists());
        let _ = PathBuf::from(path);
    }
}