pulith-fs 0.1.0

Cross-platform atomic filesystem primitives
Documentation
use crate::permissions::PermissionMode;
use crate::{Error, Result};
use std::fs;
use std::path::Path;

#[derive(Clone, Copy, Debug, Default)]
pub struct Options {
    pub permissions: Option<PermissionMode>,
    pub sync: bool,
}

impl Options {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn permissions(mut self, mode: PermissionMode) -> Self {
        self.permissions = Some(mode);
        self
    }
    pub fn sync(mut self, sync: bool) -> Self {
        self.sync = sync;
        self
    }
}

pub fn atomic_write(path: impl AsRef<Path>, content: &[u8], options: Options) -> Result<()> {
    let path = path.as_ref();
    let parent = path.parent().ok_or_else(|| Error::Write {
        path: path.to_path_buf(),
        source: std::io::Error::other("no parent directory"),
    })?;

    let mut tmp_path = parent.to_path_buf();
    tmp_path.push(format!(".tmp.{}.pulith", uuid::Uuid::new_v4()));

    fs::write(&tmp_path, content).map_err(|e| Error::Write {
        path: tmp_path.clone(),
        source: e,
    })?;

    if let Some(mode) = options.permissions {
        mode.apply_to_path(&tmp_path)?;
    }

    if options.sync {
        let file = fs::File::open(&tmp_path).map_err(|e| Error::Write {
            path: tmp_path.clone(),
            source: e,
        })?;
        file.sync_all().map_err(|e| Error::Write {
            path: tmp_path.clone(),
            source: e,
        })?;
    }

    fs::rename(&tmp_path, path).map_err(|e| {
        let _ = fs::remove_file(&tmp_path);
        Error::Write {
            path: path.to_path_buf(),
            source: e,
        }
    })?;

    Ok(())
}

pub fn atomic_read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
    let path = path.as_ref();
    std::fs::read(path).map_err(|e| Error::Read {
        path: path.to_path_buf(),
        source: e,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[test]
    fn test_atomic_write() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("test.txt");
        atomic_write(&path, b"hello world", Options::new()).unwrap();
        assert_eq!(fs::read(&path).unwrap(), b"hello world");
    }

    #[test]
    fn test_atomic_write_with_custom_permissions() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("test.txt");
        atomic_write(
            &path,
            b"data",
            Options::new().permissions(PermissionMode::Custom(0o755)),
        )
        .unwrap();
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let metadata = fs::metadata(&path).unwrap();
            assert_eq!(metadata.permissions().mode() & 0o777, 0o755);
        }
    }

    #[test]
    fn test_atomic_write_with_readonly() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("test.txt");
        atomic_write(
            &path,
            b"data",
            Options::new().permissions(PermissionMode::ReadOnly),
        )
        .unwrap();
        let metadata = fs::metadata(&path).unwrap();
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            assert_eq!(metadata.permissions().mode() & 0o777, 0o444);
        }
        #[cfg(windows)]
        assert!(metadata.permissions().readonly());
    }

    #[test]
    fn test_atomic_write_with_inherit() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("test.txt");
        atomic_write(
            &path,
            b"data",
            Options::new().permissions(PermissionMode::Inherit),
        )
        .unwrap();
        assert!(path.exists());
    }
}