Skip to main content

foundry_common/tempo/
registry.rs

1use eyre::{Result, WrapErr};
2use serde::{Serialize, de::DeserializeOwned};
3use std::{
4    fs,
5    io::{ErrorKind, Write},
6    path::Path,
7};
8
9/// Shared TOML registry helpers for Tempo local state.
10///
11/// We keep the read/parse and atomic write logic here so `keys.toml`,
12/// `sessions.toml`, and any future Tempo registry files all use the same
13/// persistence semantics instead of duplicating the same boilerplate.
14///
15/// Strict readers return `Ok(None)` only when the file is missing.
16/// Corruption and I/O failures bubble up so mutating paths can fail closed.
17pub(crate) fn read_toml_file<T: DeserializeOwned>(path: &Path, label: &str) -> Result<Option<T>> {
18    let contents = match fs::read_to_string(path) {
19        Ok(contents) => contents,
20        Err(e) if e.kind() == ErrorKind::NotFound => {
21            tracing::trace!(?path, "{label} file not found");
22            return Ok(None);
23        }
24        Err(e) => {
25            return Err(e)
26                .wrap_err_with(|| format!("failed to read {label} file {}", path.display()));
27        }
28    };
29
30    let value = toml::from_str(&contents)
31        .wrap_err_with(|| format!("failed to parse {label} file {}", path.display()))?;
32    Ok(Some(value))
33}
34
35/// Write a Tempo registry file atomically via temp file + rename.
36///
37/// This keeps every registry on the same durability path and avoids repeating
38/// the same create-dir / serialize / flush / persist sequence in each caller.
39/// The temp file and parent directory are synced so rename is much closer to a
40/// crash-safe durable write.
41pub(crate) fn write_toml_file_atomic<T: Serialize>(
42    path: &Path,
43    value: &T,
44    header: &str,
45) -> Result<()> {
46    let dir =
47        path.parent().ok_or_else(|| eyre::eyre!("invalid registry path: {}", path.display()))?;
48    fs::create_dir_all(dir)?;
49
50    let body = toml::to_string_pretty(value)?;
51    let contents = if header.trim().is_empty() { body } else { format!("{header}\n\n{body}") };
52
53    let mut tmp = tempfile::NamedTempFile::new_in(dir)?;
54    tmp.write_all(contents.as_bytes())?;
55    tmp.flush()?;
56    tmp.as_file().sync_all()?;
57    tmp.persist(path).map_err(|e| eyre::eyre!("failed to persist {}: {e}", path.display()))?;
58    sync_parent_dir(dir)?;
59
60    Ok(())
61}
62
63#[cfg(unix)]
64fn sync_parent_dir(dir: &Path) -> Result<()> {
65    fs::File::open(dir)?.sync_all()?;
66    Ok(())
67}
68
69#[cfg(not(unix))]
70fn sync_parent_dir(_dir: &Path) -> Result<()> {
71    Ok(())
72}