1use std::path::{Path, PathBuf};
31use tokio::fs::{self, File, OpenOptions};
32use tokio::io::AsyncWriteExt;
33use uuid::Uuid;
34
35pub async fn write_atomic(path: &Path, content: &[u8]) -> std::io::Result<()> {
59 let parent = path.parent().unwrap_or(Path::new("."));
60
61 let temp_path = parent.join(format!(
63 ".nika-tmp-{}-{}",
64 std::process::id(),
65 Uuid::new_v4().simple()
66 ));
67
68 let mut file = File::create(&temp_path).await?;
70 file.write_all(content).await?;
71 file.flush().await?;
72 file.sync_all().await?;
73 drop(file); match fs::rename(&temp_path, path).await {
77 Ok(()) => Ok(()),
78 Err(e) => {
79 let _ = fs::remove_file(&temp_path).await;
81 Err(e)
82 }
83 }
84}
85
86pub async fn write_append(path: &Path, content: &[u8]) -> std::io::Result<()> {
96 let mut file = OpenOptions::new()
97 .create(true)
98 .append(true)
99 .open(path)
100 .await?;
101
102 file.write_all(content).await?;
103 file.flush().await?;
104 file.sync_all().await?;
105 Ok(())
106}
107
108pub async fn write_unique(path: &Path, content: &[u8]) -> std::io::Result<PathBuf> {
128 if !path_exists(path).await {
130 write_atomic(path, content).await?;
131 return Ok(path.to_path_buf());
132 }
133
134 let stem = path
135 .file_stem()
136 .unwrap_or_default()
137 .to_string_lossy()
138 .to_string();
139 let ext = path
140 .extension()
141 .map(|e| format!(".{}", e.to_string_lossy()))
142 .unwrap_or_default();
143 let parent = path.parent().unwrap_or(Path::new("."));
144
145 for i in 1..1000 {
146 let new_path = parent.join(format!("{}-{}{}", stem, i, ext));
147 if !path_exists(&new_path).await {
148 write_atomic(&new_path, content).await?;
149 return Ok(new_path);
150 }
151 }
152
153 Err(std::io::Error::new(
154 std::io::ErrorKind::AlreadyExists,
155 "Could not generate unique filename after 1000 attempts",
156 ))
157}
158
159pub async fn write_fail(path: &Path, content: &[u8]) -> std::io::Result<()> {
173 let mut file = OpenOptions::new()
175 .write(true)
176 .create_new(true) .open(path)
178 .await?;
179
180 file.write_all(content).await?;
181 file.flush().await?;
182 file.sync_all().await?;
183 Ok(())
184}
185
186async fn path_exists(path: &Path) -> bool {
192 fs::metadata(path).await.is_ok()
193}
194
195#[cfg(test)]
200mod tests {
201 use super::*;
202 use tempfile::TempDir;
203
204 #[tokio::test]
209 async fn test_write_atomic_creates_file() {
210 let temp_dir = TempDir::new().unwrap();
211 let path = temp_dir.path().join("test.txt");
212
213 write_atomic(&path, b"Hello, World!").await.unwrap();
214
215 let content = fs::read_to_string(&path).await.unwrap();
216 assert_eq!(content, "Hello, World!");
217 }
218
219 #[tokio::test]
220 async fn test_write_atomic_overwrites_existing() {
221 let temp_dir = TempDir::new().unwrap();
222 let path = temp_dir.path().join("test.txt");
223
224 fs::write(&path, "original").await.unwrap();
226
227 write_atomic(&path, b"updated").await.unwrap();
229
230 let content = fs::read_to_string(&path).await.unwrap();
231 assert_eq!(content, "updated");
232 }
233
234 #[tokio::test]
235 async fn test_write_atomic_no_temp_file_left() {
236 let temp_dir = TempDir::new().unwrap();
237 let path = temp_dir.path().join("test.txt");
238
239 write_atomic(&path, b"content").await.unwrap();
240
241 let entries: Vec<_> = std::fs::read_dir(temp_dir.path())
243 .unwrap()
244 .filter_map(|e| e.ok())
245 .filter(|e| e.file_name().to_string_lossy().starts_with(".nika-tmp-"))
246 .collect();
247
248 assert!(entries.is_empty(), "Temp files should be cleaned up");
249 }
250
251 #[tokio::test]
252 async fn test_write_atomic_binary_content() {
253 let temp_dir = TempDir::new().unwrap();
254 let path = temp_dir.path().join("binary.bin");
255
256 let binary_data: Vec<u8> = (0..=255).collect();
257 write_atomic(&path, &binary_data).await.unwrap();
258
259 let content = fs::read(&path).await.unwrap();
260 assert_eq!(content, binary_data);
261 }
262
263 #[tokio::test]
268 async fn test_write_append_creates_new_file() {
269 let temp_dir = TempDir::new().unwrap();
270 let path = temp_dir.path().join("log.txt");
271
272 write_append(&path, b"line 1\n").await.unwrap();
273
274 let content = fs::read_to_string(&path).await.unwrap();
275 assert_eq!(content, "line 1\n");
276 }
277
278 #[tokio::test]
279 async fn test_write_append_appends_to_existing() {
280 let temp_dir = TempDir::new().unwrap();
281 let path = temp_dir.path().join("log.txt");
282
283 write_append(&path, b"line 1\n").await.unwrap();
284 write_append(&path, b"line 2\n").await.unwrap();
285 write_append(&path, b"line 3\n").await.unwrap();
286
287 let content = fs::read_to_string(&path).await.unwrap();
288 assert_eq!(content, "line 1\nline 2\nline 3\n");
289 }
290
291 #[tokio::test]
296 async fn test_write_unique_uses_original_if_available() {
297 let temp_dir = TempDir::new().unwrap();
298 let path = temp_dir.path().join("data.json");
299
300 let actual = write_unique(&path, b"{}").await.unwrap();
301
302 assert_eq!(actual, path);
303 assert!(path.exists());
304 }
305
306 #[tokio::test]
307 async fn test_write_unique_generates_suffix() {
308 let temp_dir = TempDir::new().unwrap();
309 let path = temp_dir.path().join("data.json");
310
311 fs::write(&path, "original").await.unwrap();
313
314 let actual = write_unique(&path, b"new").await.unwrap();
316
317 assert_eq!(actual, temp_dir.path().join("data-1.json"));
318 assert!(actual.exists());
319
320 let original_content = fs::read_to_string(&path).await.unwrap();
322 assert_eq!(original_content, "original");
323 }
324
325 #[tokio::test]
326 async fn test_write_unique_increments_suffix() {
327 let temp_dir = TempDir::new().unwrap();
328 let path = temp_dir.path().join("file.txt");
329
330 fs::write(&path, "0").await.unwrap();
332 fs::write(temp_dir.path().join("file-1.txt"), "1")
333 .await
334 .unwrap();
335 fs::write(temp_dir.path().join("file-2.txt"), "2")
336 .await
337 .unwrap();
338
339 let actual = write_unique(&path, b"3").await.unwrap();
341
342 assert_eq!(actual, temp_dir.path().join("file-3.txt"));
343 }
344
345 #[tokio::test]
346 async fn test_write_unique_no_extension() {
347 let temp_dir = TempDir::new().unwrap();
348 let path = temp_dir.path().join("README");
349
350 fs::write(&path, "original").await.unwrap();
352
353 let actual = write_unique(&path, b"new").await.unwrap();
355
356 assert_eq!(actual, temp_dir.path().join("README-1"));
357 }
358
359 #[tokio::test]
364 async fn test_write_fail_creates_new_file() {
365 let temp_dir = TempDir::new().unwrap();
366 let path = temp_dir.path().join("new.txt");
367
368 write_fail(&path, b"content").await.unwrap();
369
370 let content = fs::read_to_string(&path).await.unwrap();
371 assert_eq!(content, "content");
372 }
373
374 #[tokio::test]
375 async fn test_write_fail_errors_if_exists() {
376 let temp_dir = TempDir::new().unwrap();
377 let path = temp_dir.path().join("existing.txt");
378
379 fs::write(&path, "existing").await.unwrap();
381
382 let result = write_fail(&path, b"new").await;
384
385 assert!(result.is_err());
386 assert_eq!(
387 result.unwrap_err().kind(),
388 std::io::ErrorKind::AlreadyExists
389 );
390
391 let content = fs::read_to_string(&path).await.unwrap();
393 assert_eq!(content, "existing");
394 }
395
396 #[tokio::test]
397 async fn test_write_fail_atomic_check() {
398 let temp_dir = TempDir::new().unwrap();
401 let path = temp_dir.path().join("race.txt");
402
403 let path1 = path.clone();
405 let path2 = path.clone();
406
407 let (r1, r2) = tokio::join!(
408 write_fail(&path1, b"writer 1"),
409 write_fail(&path2, b"writer 2"),
410 );
411
412 let successes = [r1.is_ok(), r2.is_ok()];
414 assert_eq!(
415 successes.iter().filter(|&&x| x).count(),
416 1,
417 "Exactly one writer should succeed"
418 );
419 }
420
421 #[tokio::test]
426 async fn test_write_atomic_empty_content() {
427 let temp_dir = TempDir::new().unwrap();
428 let path = temp_dir.path().join("empty.txt");
429
430 write_atomic(&path, b"").await.unwrap();
431
432 let content = fs::read(&path).await.unwrap();
433 assert!(content.is_empty());
434 }
435
436 #[tokio::test]
437 async fn test_write_atomic_large_content() {
438 let temp_dir = TempDir::new().unwrap();
439 let path = temp_dir.path().join("large.bin");
440
441 let large_content: Vec<u8> = (0..1024 * 1024).map(|i| (i % 256) as u8).collect();
443 write_atomic(&path, &large_content).await.unwrap();
444
445 let content = fs::read(&path).await.unwrap();
446 assert_eq!(content.len(), 1024 * 1024);
447 assert_eq!(content, large_content);
448 }
449
450 #[tokio::test]
451 async fn test_write_to_nested_existing_dir() {
452 let temp_dir = TempDir::new().unwrap();
453 let nested = temp_dir.path().join("a/b/c");
454 fs::create_dir_all(&nested).await.unwrap();
455
456 let path = nested.join("file.txt");
457 write_atomic(&path, b"nested").await.unwrap();
458
459 assert!(path.exists());
460 }
461}