Skip to main content

scv_client/
fs.rs

1//! Private instance files: SCV's state, records, and credentials are
2//! written whole, readable only by the user, and survive a crash.
3
4use std::ffi::OsString;
5use std::io::{self, Write as _};
6use std::path::Path;
7
8/// Replace `path` with `bytes` atomically, as a file only the user can read.
9///
10/// The bytes go to a temporary file in the same directory, which is synced
11/// and renamed over `path`; the directory is then synced so the rename
12/// itself survives a crash. A reader sees the old file or the new one, never
13/// a partial write. The directory must exist; callers create it with the
14/// privacy their data needs. The temporary file is named for its target
15/// ([`temporary_prefix`]), so one a crash leaves shows whose it was.
16pub fn replace_private(path: &Path, bytes: &[u8]) -> io::Result<()> {
17    replace(path, bytes, true)
18}
19
20/// [`replace_private`] without syncing: still atomic and private, but a
21/// machine crash may lose the change. For state that is rewritten often and
22/// only says what is happening now, written where a sync would hold up a
23/// busy task.
24pub fn replace_private_unsynced(path: &Path, bytes: &[u8]) -> io::Result<()> {
25    replace(path, bytes, false)
26}
27
28fn replace(path: &Path, bytes: &[u8], sync: bool) -> io::Result<()> {
29    let parent = path
30        .parent()
31        .ok_or_else(|| io::Error::other(format!("{} has no parent", path.display())))?;
32    // Named temporary files are created with mode 0600.
33    let mut temporary = tempfile::Builder::new()
34        .prefix(&temporary_prefix(path))
35        .suffix(".tmp")
36        .tempfile_in(parent)?;
37    temporary.write_all(bytes)?;
38    if sync {
39        temporary.as_file().sync_all()?;
40    }
41    temporary.persist(path).map_err(|error| error.error)?;
42    if sync {
43        sync_directory(parent)?;
44    }
45    Ok(())
46}
47
48/// The longest file name a temporary file is named for; a longer one would
49/// make the temporary file's name too long for some file systems.
50const MAX_NAMED: usize = 200;
51
52/// How [`replace_private`] starts the name of the temporary file it writes
53/// `path` through: `.<file name>.`, followed by random characters and
54/// `.tmp`. A file name over 200 bytes gets `.tmp.` instead.
55pub fn temporary_prefix(path: &Path) -> OsString {
56    let mut prefix = OsString::from(".");
57    match path.file_name() {
58        Some(name) if name.len() <= MAX_NAMED => prefix.push(name),
59        _ => prefix.push("tmp"),
60    }
61    prefix.push(".");
62    prefix
63}
64
65/// Flush a directory's entries, so files created, renamed, or removed in it
66/// persist. A no-op where directories cannot be opened (not Unix).
67pub fn sync_directory(directory: &Path) -> io::Result<()> {
68    #[cfg(unix)]
69    std::fs::File::open(directory)?.sync_all()?;
70    #[cfg(not(unix))]
71    let _ = directory;
72    Ok(())
73}
74
75#[cfg(test)]
76mod tests;