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