super-stt-registry-types/src/fs.rs
Line | Count | Source |
1 | | // SPDX-License-Identifier: GPL-3.0-only |
2 | | //! Small filesystem helpers shared by the daemon and the indexer. |
3 | | |
4 | | use std::io::Write; |
5 | | use std::path::{Path, PathBuf}; |
6 | | |
7 | | /// Atomically write `contents` to `path`: write a sibling temp file, flush and |
8 | | /// `fsync` it, then rename it over `path`. A crash mid-write leaves either the |
9 | | /// old file or the new one, never a truncated mix. The temp file lives in the |
10 | | /// same directory so the rename stays on one filesystem (a cross-device rename |
11 | | /// is not atomic). |
12 | | /// |
13 | | /// Replaces the daemon's cache-write and the indexer's two `index.json` writers |
14 | | /// (which wrote in place, non-atomically, and disagreed on a trailing newline). |
15 | | /// |
16 | | /// # Errors |
17 | | /// Returns any I/O error from creating/writing/syncing the temp file or the |
18 | | /// final rename. On error the temp file is best-effort removed. |
19 | 18 | pub fn write_atomic(path: &Path, contents: &[u8]) -> std::io::Result<()> { |
20 | 18 | let mut tmp = path.as_os_str().to_owned(); |
21 | 18 | tmp.push(".tmp"); |
22 | 18 | let tmp = PathBuf::from(tmp); |
23 | | |
24 | | // Scope the file handle so it is closed before the rename. |
25 | 18 | let write_result = (|| { |
26 | 18 | let mut f = std::fs::File::create(&tmp)?0 ; |
27 | 18 | f.write_all(contents)?0 ; |
28 | 18 | f.flush()?0 ; |
29 | 18 | f.sync_all() |
30 | | })(); |
31 | 18 | if let Err(e0 ) = write_result { |
32 | 0 | let _ = std::fs::remove_file(&tmp); |
33 | 0 | return Err(e); |
34 | 18 | } |
35 | | |
36 | 18 | if let Err(e0 ) = std::fs::rename(&tmp, path) { |
37 | 0 | let _ = std::fs::remove_file(&tmp); |
38 | 0 | return Err(e); |
39 | 18 | } |
40 | 18 | Ok(()) |
41 | 18 | } |
42 | | |
43 | | #[cfg(test)] |
44 | | mod tests { |
45 | | use super::write_atomic; |
46 | | |
47 | | #[test] |
48 | 2 | fn writes_and_overwrites_atomically() { |
49 | 2 | let dir = std::env::temp_dir().join(format!("stt-write-atomic-{}", std::process::id())); |
50 | 2 | std::fs::create_dir_all(&dir).unwrap(); |
51 | 2 | let path = dir.join("index.json"); |
52 | | |
53 | 2 | write_atomic(&path, b"first").unwrap(); |
54 | 2 | assert_eq!(std::fs::read(&path).unwrap(), b"first"); |
55 | | // Overwriting replaces the contents and leaves no `.tmp` sibling. |
56 | 2 | write_atomic(&path, b"second").unwrap(); |
57 | 2 | assert_eq!(std::fs::read(&path).unwrap(), b"second"); |
58 | 2 | let mut tmp = path.clone().into_os_string(); |
59 | 2 | tmp.push(".tmp"); |
60 | 2 | assert!( |
61 | 2 | !std::path::Path::new(&tmp).exists(), |
62 | | "temp file must be gone" |
63 | | ); |
64 | | |
65 | 2 | std::fs::remove_dir_all(&dir).unwrap(); |
66 | 2 | } |
67 | | } |