1use std::path::{Path, PathBuf};
4
5use std::pin::Pin;
6use std::task::{Context, Poll};
7
8use tokio::io::{AsyncRead, ReadBuf};
9
10use crate::{AsyncFile, FsError, Operation};
11
12pub async fn atomic_write<R>(target: impl AsRef<Path>, mut reader: R) -> Result<(), FsError>
15where
16 R: AsyncRead + Unpin,
17{
18 let target = target.as_ref().to_owned();
19 let parent = target.parent().ok_or_else(|| {
20 FsError::InvalidRequest(format!("target has no parent: {}", target.display()))
21 })?;
22 tokio::fs::create_dir_all(parent)
23 .await
24 .map_err(|error| FsError::io(Operation::Directory, parent, error))?;
25
26 let temporary = temporary_sibling(&target);
27 let result = async {
28 let mut file = AsyncFile::create_new(&temporary).await?;
29 tokio::io::copy(&mut reader, &mut file)
30 .await
31 .map_err(|error| FsError::io(Operation::Write, &temporary, error))?;
32 file.flush().await?;
33 tokio::fs::rename(&temporary, &target)
34 .await
35 .map_err(|error| FsError::io(Operation::Write, &target, error))
36 }
37 .await;
38
39 match result {
40 Ok(()) => Ok(()),
41 Err(primary) => {
42 match tokio::fs::remove_file(&temporary).await {
45 Ok(()) => Err(primary),
46 Err(cleanup) => Err(FsError::Write {
47 path: temporary.display().to_string(),
48 detail: format!("{primary}; temporary-file cleanup failed: {cleanup}"),
49 }),
50 }
51 }
52 }
53}
54
55pub async fn atomic_write_string(target: impl AsRef<Path>, value: &str) -> Result<(), FsError> {
57 atomic_write(target, SliceReader::new(value.as_bytes())).await
58}
59
60pub async fn atomic_copy(
62 source: impl AsRef<Path>,
63 target: impl AsRef<Path>,
64) -> Result<(), FsError> {
65 let source = source.as_ref();
66 let file = AsyncFile::open(source).await?;
67 atomic_write(target, file).await
68}
69
70fn temporary_sibling(target: &Path) -> PathBuf {
71 let mut value = target.as_os_str().to_owned();
72 value.push(format!(".tmp.{}", uuid::Uuid::new_v4()));
73 value.into()
74}
75
76struct SliceReader<'a> {
77 bytes: &'a [u8],
78 position: usize,
79}
80
81impl<'a> SliceReader<'a> {
82 fn new(bytes: &'a [u8]) -> Self {
83 Self { bytes, position: 0 }
84 }
85}
86
87impl AsyncRead for SliceReader<'_> {
88 fn poll_read(
89 mut self: Pin<&mut Self>,
90 _context: &mut Context<'_>,
91 buffer: &mut ReadBuf<'_>,
92 ) -> Poll<std::io::Result<()>> {
93 let remaining = &self.bytes[self.position..];
94 if remaining.is_empty() {
95 return Poll::Ready(Ok(()));
96 }
97 let amount = remaining.len().min(buffer.remaining());
98 buffer.put_slice(&remaining[..amount]);
99 self.position += amount;
100 Poll::Ready(Ok(()))
101 }
102}