Skip to main content

semora_cli/
cachefile.rs

1//! Small files in the config folder that several commands may write at once.
2
3use std::fs::OpenOptions;
4use std::io::ErrorKind;
5use std::path::Path;
6
7use anyhow::Context;
8
9/// Replaces `dir/name` with `change` of its text (empty when there is no file yet). Writers take
10/// turns on `name.lock`, a file that is never replaced, so no write is lost. Readers don't lock:
11/// the rename swaps in the whole file at once.
12pub fn rewrite(dir: &Path, name: &str, change: impl FnOnce(&str) -> String) -> anyhow::Result<()> {
13    std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
14    let lock_path = dir.join(format!("{name}.lock"));
15    let lock = OpenOptions::new()
16        .write(true)
17        .create(true)
18        .truncate(false)
19        .open(&lock_path)
20        .with_context(|| format!("opening {}", lock_path.display()))?;
21    lock.lock()
22        .with_context(|| format!("locking {}", lock_path.display()))?;
23    let file = dir.join(name);
24    let text = match std::fs::read_to_string(&file) {
25        Ok(t) => t,
26        Err(e) if e.kind() == ErrorKind::NotFound => String::new(),
27        Err(e) => return Err(e).with_context(|| format!("reading {}", file.display())),
28    };
29    let tmp = dir.join(format!("{name}.tmp"));
30    std::fs::write(&tmp, change(&text)).with_context(|| format!("writing {}", tmp.display()))?;
31    std::fs::rename(&tmp, &file).with_context(|| format!("writing {}", file.display()))?;
32    Ok(())
33}