app_json_settings/core/
save.rs1use std::fs::{self, File, OpenOptions};
2use std::io::{self, Write};
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use crate::Result;
8
9#[cfg(windows)]
10use std::os::windows::ffi::OsStrExt;
11
12static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
13
14#[cfg(windows)]
15#[allow(non_snake_case)]
16#[link(name = "kernel32")]
17unsafe extern "system" {
18 fn MoveFileExW(existing_file_name: *const u16, new_file_name: *const u16, flags: u32) -> i32;
19}
20
21#[derive(Debug, Clone, Copy, Eq, PartialEq)]
23pub enum SaveMode {
24 Atomic,
29
30 Direct,
36}
37
38pub fn save_to_path(path: &Path, content: &str, mode: SaveMode) -> Result<()> {
39 if let Some(parent) = non_empty_parent(path) {
40 fs::create_dir_all(parent)?;
41 }
42
43 match mode {
44 SaveMode::Atomic => save_atomic(path, content),
45 SaveMode::Direct => save_direct(path, content),
46 }
47}
48
49fn save_direct(path: &Path, content: &str) -> Result<()> {
50 fs::write(path, content)?;
51 Ok(())
52}
53
54fn save_atomic(path: &Path, content: &str) -> Result<()> {
55 let (temp_path, mut temp_file) = create_temp_file(path)?;
56
57 let result = (|| -> io::Result<()> {
58 temp_file.write_all(content.as_bytes())?;
59 temp_file.sync_all()?;
60 drop(temp_file);
61
62 replace_file(&temp_path, path)?;
63 sync_parent_dir(path);
64 Ok(())
65 })();
66
67 if result.is_err() {
68 let _ = fs::remove_file(&temp_path);
69 }
70
71 result?;
72 Ok(())
73}
74
75fn create_temp_file(target: &Path) -> io::Result<(PathBuf, File)> {
76 let parent = non_empty_parent(target).unwrap_or_else(|| Path::new("."));
77 let target_name = target
78 .file_name()
79 .map(|name| name.to_string_lossy())
80 .unwrap_or_else(|| "settings.json".into());
81
82 for attempt in 0..1000_u16 {
83 let counter = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
84 let nanos = SystemTime::now()
85 .duration_since(UNIX_EPOCH)
86 .map(|duration| duration.as_nanos())
87 .unwrap_or(0);
88 let temp_name = format!(
89 ".{target_name}.tmp.{}.{}.{}.{}",
90 std::process::id(),
91 nanos,
92 counter,
93 attempt
94 );
95 let temp_path = parent.join(temp_name);
96
97 match OpenOptions::new()
98 .write(true)
99 .create_new(true)
100 .open(&temp_path)
101 {
102 Ok(file) => return Ok((temp_path, file)),
103 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
104 Err(error) => return Err(error),
105 }
106 }
107
108 Err(io::Error::new(
109 io::ErrorKind::AlreadyExists,
110 "could not create a unique temporary settings file",
111 ))
112}
113
114#[cfg(unix)]
115fn replace_file(temp_path: &Path, target_path: &Path) -> io::Result<()> {
116 fs::rename(temp_path, target_path)
117}
118
119#[cfg(windows)]
120fn replace_file(temp_path: &Path, target_path: &Path) -> io::Result<()> {
121 const MOVEFILE_REPLACE_EXISTING: u32 = 0x0000_0001;
122 const MOVEFILE_WRITE_THROUGH: u32 = 0x0000_0008;
123
124 let old_path = wide_null_terminated(temp_path);
125 let new_path = wide_null_terminated(target_path);
126
127 let ok = unsafe {
131 MoveFileExW(
132 old_path.as_ptr(),
133 new_path.as_ptr(),
134 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
135 )
136 };
137
138 if ok == 0 {
139 Err(io::Error::last_os_error())
140 } else {
141 Ok(())
142 }
143}
144
145#[cfg(windows)]
146fn wide_null_terminated(path: &Path) -> Vec<u16> {
147 path.as_os_str().encode_wide().chain([0]).collect()
148}
149
150#[cfg(not(any(unix, windows)))]
151fn replace_file(temp_path: &Path, target_path: &Path) -> io::Result<()> {
152 if target_path.exists() {
153 return Err(io::Error::new(
154 io::ErrorKind::Unsupported,
155 "atomic replacement is not implemented for this target; use SaveMode::Direct",
156 ));
157 }
158
159 fs::rename(temp_path, target_path)
160}
161
162fn non_empty_parent(path: &Path) -> Option<&Path> {
163 path.parent()
164 .filter(|parent| !parent.as_os_str().is_empty())
165}
166
167fn sync_parent_dir(path: &Path) {
168 if let Some(parent) = non_empty_parent(path) {
169 if let Ok(dir) = File::open(parent) {
170 let _ = dir.sync_all();
171 }
172 }
173}