1use crate::error::{FsError, Result};
10use std::fs::{self, File, OpenOptions};
11use std::io::Write;
12use std::path::{Path, PathBuf};
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::time::{Duration, SystemTime};
15
16pub const TEMP_MARKER: &str = ".rktmp-";
18static COUNTER: AtomicU64 = AtomicU64::new(0);
19const TEMP_ATTEMPTS: u32 = 32;
20
21#[derive(Clone, Copy, Debug)]
22pub struct AtomicOptions {
23 pub private: bool,
25 pub create_parents: bool,
27 pub durable: bool,
29}
30
31impl Default for AtomicOptions {
32 fn default() -> Self {
33 Self {
34 private: false,
35 create_parents: false,
36 durable: true,
37 }
38 }
39}
40
41impl AtomicOptions {
42 pub fn private() -> Self {
43 Self {
44 private: true,
45 ..Self::default()
46 }
47 }
48}
49
50pub fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
52 atomic_write_with(path, AtomicOptions::default(), |f| f.write_all(bytes))
53}
54
55pub fn atomic_write_private(path: &Path, bytes: &[u8]) -> Result<()> {
57 atomic_write_with(path, AtomicOptions::private(), |f| f.write_all(bytes))
58}
59
60pub fn atomic_write_with(
63 path: &Path,
64 options: AtomicOptions,
65 fill: impl FnOnce(&mut File) -> std::io::Result<()>,
66) -> Result<()> {
67 let mut writer = AtomicWriter::create(path, options)?;
68 fill(writer.file_mut()).map_err(|e| FsError::io(writer.temp_path(), e))?;
69 writer.commit()
70}
71
72#[derive(Debug)]
78pub struct AtomicWriter {
79 file: Option<File>,
80 temp: PathBuf,
81 target: PathBuf,
82 options: AtomicOptions,
83}
84
85impl AtomicWriter {
86 pub fn create(target: &Path, options: AtomicOptions) -> Result<Self> {
87 let parent = parent_of(target);
88 if options.create_parents {
89 if options.private {
90 crate::private::ensure_private_dir(&parent)?;
91 } else {
92 fs::create_dir_all(&parent).map_err(|e| FsError::io(&parent, e))?;
93 }
94 }
95 #[cfg(unix)]
97 let existing_mode = match fs::symlink_metadata(target) {
98 Ok(m) if m.is_file() => {
99 use std::os::unix::fs::PermissionsExt;
100 Some(m.permissions().mode() & 0o7777)
101 }
102 _ => None,
103 };
104 let name = target
105 .file_name()
106 .map(|n| n.to_string_lossy().into_owned())
107 .ok_or_else(|| FsError::BadName(target.display().to_string()))?;
108 let mut last = None;
109 for attempt in 0..TEMP_ATTEMPTS {
110 let temp = parent.join(temp_name(&name, attempt));
111 let mut open = OpenOptions::new();
112 open.write(true).create_new(true);
113 #[cfg(unix)]
114 {
115 use std::os::unix::fs::OpenOptionsExt;
116 let mode = if options.private {
117 0o600
118 } else {
119 existing_mode.unwrap_or(0o666)
120 };
121 open.mode(mode)
122 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
123 }
124 match open.open(&temp) {
125 Ok(file) => {
126 #[cfg(not(unix))]
127 if options.private {
128 if let Err(e) = crate::private::restrict_owner_only(&temp) {
129 let _ = fs::remove_file(&temp);
130 return Err(e);
131 }
132 }
133 #[cfg(unix)]
134 if let (false, Some(mode)) = (options.private, existing_mode) {
135 use std::os::unix::fs::PermissionsExt;
137 let _ = file.set_permissions(fs::Permissions::from_mode(mode));
138 }
139 return Ok(Self {
140 file: Some(file),
141 temp,
142 target: target.to_path_buf(),
143 options,
144 });
145 }
146 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => last = Some(e),
147 Err(e) => return Err(FsError::io(&temp, e)),
148 }
149 }
150 Err(FsError::io(
151 target,
152 last.unwrap_or_else(|| std::io::Error::other("temp name collisions")),
153 ))
154 }
155
156 pub fn temp_path(&self) -> &Path {
157 &self.temp
158 }
159
160 pub fn file_mut(&mut self) -> &mut File {
161 self.file.as_mut().expect("writer is open until commit")
162 }
163
164 pub fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
165 let temp = self.temp.clone();
166 self.file_mut()
167 .write_all(bytes)
168 .map_err(|e| FsError::io(temp, e))
169 }
170
171 pub fn commit(mut self) -> Result<()> {
173 let file = self.file.take().expect("writer is open until commit");
174 let result = (|| -> Result<()> {
175 if self.options.durable {
176 file.sync_all().map_err(|e| FsError::io(&self.temp, e))?;
177 }
178 drop(file);
179 fs::rename(&self.temp, &self.target).map_err(|e| FsError::io(&self.target, e))?;
180 if self.options.durable {
181 sync_dir(&parent_of(&self.target));
182 }
183 Ok(())
184 })();
185 if result.is_err() {
186 let _ = fs::remove_file(&self.temp);
187 }
188 self.temp = PathBuf::new();
190 result
191 }
192}
193
194impl Drop for AtomicWriter {
195 fn drop(&mut self) {
196 if !self.temp.as_os_str().is_empty() {
197 self.file.take();
198 let _ = fs::remove_file(&self.temp);
199 }
200 }
201}
202
203fn parent_of(path: &Path) -> PathBuf {
204 match path.parent() {
205 Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
206 _ => PathBuf::from("."),
207 }
208}
209
210fn temp_name(name: &str, attempt: u32) -> String {
211 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
212 let nanos = SystemTime::now()
213 .duration_since(SystemTime::UNIX_EPOCH)
214 .map(|d| d.subsec_nanos())
215 .unwrap_or(0);
216 format!(
218 ".{name}{TEMP_MARKER}{}-{n}-{nanos:x}-{attempt}",
219 std::process::id()
220 )
221}
222
223pub fn sync_dir(dir: &Path) {
225 #[cfg(unix)]
226 if let Ok(d) = File::open(dir) {
227 let _ = d.sync_all();
228 }
229 #[cfg(not(unix))]
230 let _ = dir;
231}
232
233pub fn sweep_stale_temps(dir: &Path, max_age: Duration) -> Result<usize> {
237 let entries = match fs::read_dir(dir) {
238 Ok(e) => e,
239 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(0),
240 Err(e) => return Err(FsError::io(dir, e)),
241 };
242 let mut removed = 0;
243 for entry in entries.flatten() {
244 let name = entry.file_name().to_string_lossy().into_owned();
245 let Some(pid) = temp_owner_pid(&name) else {
246 continue;
247 };
248 let Ok(meta) = entry.metadata() else { continue };
249 if !meta.is_file() {
250 continue;
251 }
252 let old = meta
253 .modified()
254 .ok()
255 .and_then(|m| m.elapsed().ok())
256 .is_some_and(|age| age >= max_age);
257 if ((pid != std::process::id() && !crate::process_alive(pid)) || old)
258 && fs::remove_file(entry.path()).is_ok()
259 {
260 removed += 1;
261 }
262 }
263 Ok(removed)
264}
265
266fn temp_owner_pid(name: &str) -> Option<u32> {
267 let rest = &name[name.find(TEMP_MARKER)? + TEMP_MARKER.len()..];
268 rest.split('-').next()?.parse().ok()
269}