1use std::fs::{self, File};
2use std::io::Write;
3use std::path::{Path, PathBuf};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use ::log::{debug, error, warn};

Replace path with bytes so that a power cut leaves either the old file or the new one, never an empty or half-written one: the bytes reach the disk before the rename does.

10pub(crate) fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
11    let tmp = path.with_extension("tmp");
12    debug!("atomic write of {} bytes to {}", bytes.len(), path.display());
13    let result = (|| {
14        let mut f = File::create(&tmp)?;
15        f.write_all(bytes)?;
16        f.sync_all()?;
17        drop(f);
18        fs::rename(&tmp, path)
19    })();
20    if let Err(e) = &result {
21        error!("atomic write of {} bytes to {} failed: {e}", bytes.len(), path.display());
22    }
23    result
24}

Move a file that cannot be read out of the way, keeping it for a human to look at, so the app can start clean rather than refuse to start. Returns where it went.

28pub fn set_aside(path: &Path) -> std::io::Result<PathBuf> {
29    let ms = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0);
30    let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
31    let aside = path.with_file_name(format!("{name}.damaged-{ms}"));
32    warn!("setting aside unreadable {} as {}", path.display(), aside.display());
33    fs::rename(path, &aside).inspect_err(|e| error!("could not set {} aside: {e}", path.display()))?;
34    Ok(aside)
35}