semora-cli 1.1.0

Semora CLI: publish Markdown and HTML, and hear back from the page
Documentation
//! Small files in the config folder that several commands may write at once.

use std::fs::OpenOptions;
use std::io::ErrorKind;
use std::path::Path;

use anyhow::Context;

/// Replaces `dir/name` with `change` of its text (empty when there is no file yet). Writers take
/// turns on `name.lock`, a file that is never replaced, so no write is lost. Readers don't lock:
/// the rename swaps in the whole file at once.
pub fn rewrite(dir: &Path, name: &str, change: impl FnOnce(&str) -> String) -> anyhow::Result<()> {
    std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
    let lock_path = dir.join(format!("{name}.lock"));
    let lock = OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(false)
        .open(&lock_path)
        .with_context(|| format!("opening {}", lock_path.display()))?;
    lock.lock()
        .with_context(|| format!("locking {}", lock_path.display()))?;
    let file = dir.join(name);
    let text = match std::fs::read_to_string(&file) {
        Ok(t) => t,
        Err(e) if e.kind() == ErrorKind::NotFound => String::new(),
        Err(e) => return Err(e).with_context(|| format!("reading {}", file.display())),
    };
    let tmp = dir.join(format!("{name}.tmp"));
    std::fs::write(&tmp, change(&text)).with_context(|| format!("writing {}", tmp.display()))?;
    std::fs::rename(&tmp, &file).with_context(|| format!("writing {}", file.display()))?;
    Ok(())
}