wecom-cli 0.1.5

The official CLI for WeCom — 企业微信命令行工具,让人类和 AI Agent 都能在终端中操作企业微信
use std::fs;
use std::io::Write;
use std::path::Path;

use anyhow::Result;

/// Atomically write `data` (bytes) to `path`, overwriting if the file already exists.
///
/// Uses `tempfile::NamedTempFile` to create a temporary file in the same directory,
/// then atomically renames it to the target path via `persist`.
pub fn atomic_write(path: &Path, data: &[u8], mode: Option<u32>) -> Result<()> {
    let Some(parent) = path.parent() else {
        anyhow::bail!("无效文件路径:{}", path.display());
    };

    fs::create_dir_all(parent)?;

    // Create a temp file in the same directory (same filesystem → atomic rename).
    let mut tmp = tempfile::NamedTempFile::new_in(parent)?;

    // Set permissions before writing content (avoid permission window).
    #[cfg(unix)]
    if let Some(m) = mode {
        use std::os::unix::fs::PermissionsExt;
        tmp.as_file()
            .set_permissions(fs::Permissions::from_mode(m))?;
    }

    tmp.write_all(data)?;
    tmp.as_file().flush()?;
    tmp.as_file().sync_all()?;

    // Atomically rename to the target path.
    tmp.persist(path)?;

    Ok(())
}

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

    fn tmp() -> TempDir {
        tempfile::tempdir().unwrap()
    }

    // -----------------------------------------------------------------------
    // atomic_write
    // -----------------------------------------------------------------------

    #[test]
    fn write_creates_file_with_correct_content() {
        let dir = tmp();
        let path = dir.path().join("test.bin");
        let data = b"hello world";

        atomic_write(&path, data, None).unwrap();
        assert_eq!(fs::read(&path).unwrap(), data);
    }

    #[test]
    fn write_creates_parent_directories() {
        let dir = tmp();
        let path = dir.path().join("a").join("b").join("deep.bin");

        atomic_write(&path, b"nested", None).unwrap();
        assert_eq!(fs::read(&path).unwrap(), b"nested");
    }

    #[test]
    fn overwrite_replaces_existing_file() {
        let dir = tmp();
        let path = dir.path().join("overwrite.bin");

        atomic_write(&path, b"first", None).unwrap();
        atomic_write(&path, b"second", None).unwrap();
        assert_eq!(fs::read(&path).unwrap(), b"second");
    }

    #[test]
    fn no_temp_file_left_after_success() {
        let dir = tmp();
        let path = dir.path().join("clean.bin");

        atomic_write(&path, b"ok", None).unwrap();

        let entries: Vec<_> = fs::read_dir(dir.path())
            .unwrap()
            .filter_map(|e| e.ok())
            .collect();
        // Only the target file should remain; no `.tmp.*` leftovers.
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].file_name(), "clean.bin");
    }

    #[test]
    fn write_empty_data() {
        let dir = tmp();
        let path = dir.path().join("empty.bin");

        atomic_write(&path, b"", None).unwrap();
        assert_eq!(fs::read(&path).unwrap(), b"");
    }

    // -----------------------------------------------------------------------
    // Unix-specific: file permissions
    // -----------------------------------------------------------------------

    #[cfg(unix)]
    mod unix {
        use super::*;
        use std::os::unix::fs::PermissionsExt;

        #[test]
        fn write_sets_file_permissions() {
            let dir = tmp();
            let path = dir.path().join("secret.bin");

            atomic_write(&path, b"secret", Some(0o600)).unwrap();

            let perms = fs::metadata(&path).unwrap().permissions();
            assert_eq!(perms.mode() & 0o777, 0o600);
        }

        #[test]
        fn write_sets_readonly_permissions() {
            let dir = tmp();
            let path = dir.path().join("readonly.bin");

            atomic_write(&path, b"ro", Some(0o400)).unwrap();

            let perms = fs::metadata(&path).unwrap().permissions();
            assert_eq!(perms.mode() & 0o777, 0o400);
        }
    }
}