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;