Skip to main content

secs_types/
fs.rs

1//! Atomic file replacement shared by every writer (#92 TD-74).
2//!
3//! It uses only `std` and lives in this lowest crate so the storage crate,
4//! the export writers and the service share one implementation without a
5//! new dependency edge (docs/ARCHITECTURE.md section 3).
6
7use std::borrow::Cow;
8use std::fs::{self, File, OpenOptions};
9use std::io::{self, Write};
10use std::path::{Path, PathBuf};
11use std::sync::atomic::{AtomicU64, Ordering};
12
13/// Names tried before giving up when every candidate already exists.
14const ATTEMPTS: usize = 100;
15static COUNTER: AtomicU64 = AtomicU64::new(0);
16
17/// Replaces `path` with the bytes `write` puts into a sibling temporary file.
18///
19/// The temporary file is `.{name}.tmp-{pid}-{counter}` in the destination's
20/// folder, created with `create_new` (another name is tried when one exists).
21/// After `write` returns it is flushed with `sync_all`, closed and renamed over
22/// `path`; std's rename replaces an existing destination on every platform.
23/// On any error the temporary file is removed and `path` keeps its previous
24/// content. The destination's folder must already exist.
25pub fn write_atomic<F>(path: &Path, write: F) -> io::Result<()>
26where
27    F: FnOnce(&mut File) -> io::Result<()>,
28{
29    let parent = path.parent().unwrap_or_else(|| Path::new("."));
30    let name = path
31        .file_name()
32        .map_or(Cow::Borrowed("file"), |name| name.to_string_lossy());
33    let (temporary, mut file) = create_sibling(parent, &name)?;
34    let result = (|| {
35        write(&mut file)?;
36        file.sync_all()?;
37        drop(file);
38        fs::rename(&temporary, path)
39    })();
40    if result.is_err() {
41        let _ = fs::remove_file(&temporary);
42    }
43    result
44}
45
46/// [`write_atomic`] for bytes already in memory.
47pub fn write_atomic_bytes(path: &Path, bytes: &[u8]) -> io::Result<()> {
48    write_atomic(path, |file| file.write_all(bytes))
49}
50
51fn create_sibling(parent: &Path, name: &str) -> io::Result<(PathBuf, File)> {
52    let pid = std::process::id();
53    for _ in 0..ATTEMPTS {
54        let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
55        let candidate = parent.join(format!(".{name}.tmp-{pid}-{counter}"));
56        match OpenOptions::new()
57            .write(true)
58            .create_new(true)
59            .open(&candidate)
60        {
61            Ok(file) => return Ok((candidate, file)),
62            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
63            Err(error) => return Err(error),
64        }
65    }
66    Err(io::Error::new(
67        io::ErrorKind::AlreadyExists,
68        format!("no free temporary name for {name} in {}", parent.display()),
69    ))
70}