ccd-cli 1.0.0-beta.2

Bootstrap and validate Continuous Context Development repositories
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicU64, Ordering};

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

pub(crate) fn create_text(path: &Path, contents: &str, mode: Option<u32>) -> io::Result<()> {
    write_text(path, contents.as_bytes(), mode, FinalizeMode::CreateNew)
}

pub(crate) fn replace_text(path: &Path, contents: &str, mode: Option<u32>) -> io::Result<()> {
    write_text(path, contents.as_bytes(), mode, FinalizeMode::Replace)
}

enum FinalizeMode {
    CreateNew,
    Replace,
}

fn write_text(
    path: &Path,
    contents: &[u8],
    mode: Option<u32>,
    finalize: FinalizeMode,
) -> io::Result<()> {
    let parent = path.parent().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("path has no parent: {}", path.display()),
        )
    })?;
    fs::create_dir_all(parent)?;

    let temp_path = unique_temp_path(parent, path);
    let mut temp_file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&temp_path)?;

    if let Err(error) = temp_file.write_all(contents) {
        let _ = fs::remove_file(&temp_path);
        return Err(error);
    }

    if let Err(error) = temp_file.sync_all() {
        let _ = fs::remove_file(&temp_path);
        return Err(error);
    }
    drop(temp_file);

    if let Some(mode) = mode {
        if let Err(error) = fs::set_permissions(&temp_path, fs::Permissions::from_mode(mode)) {
            let _ = fs::remove_file(&temp_path);
            return Err(error);
        }
    }

    let result = match finalize {
        FinalizeMode::CreateNew => finalize_create_new(&temp_path, path),
        FinalizeMode::Replace => fs::rename(&temp_path, path),
    };

    if result.is_err() {
        let _ = fs::remove_file(&temp_path);
    }

    result
}

fn finalize_create_new(temp_path: &Path, path: &Path) -> io::Result<()> {
    fs::hard_link(temp_path, path)?;
    fs::remove_file(temp_path)
}

fn unique_temp_path(parent: &Path, path: &Path) -> PathBuf {
    let file_name = path
        .file_name()
        .map(|value| value.to_string_lossy())
        .unwrap_or_else(|| "ccd".into());
    let counter = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
    let temp_name = format!(".{file_name}.ccd-tmp-{}-{counter}", process::id());
    parent.join(temp_name)
}