dir_structure/
atomic_dir.rs

1//! A module to enable atomic directory writes.
2//!
3//! This module provides the [`AtomicDir`] struct, which allows for safe and atomic
4//! writes to directories.
5//!
6//! It achieves this by writing changes to a temporary directory first, and then
7//! renaming the temporary directory to the target directory once all writes are successful.
8//! This ensures that the target directory is never left in a partially updated state.
9
10#[cfg(feature = "async")]
11use std::future::Future;
12use std::pin::Pin;
13#[cfg(feature = "async")]
14use std::task::Context;
15#[cfg(feature = "async")]
16use std::task::Poll;
17
18#[cfg(feature = "async")]
19use pin_project::pin_project;
20
21#[cfg(feature = "async")]
22use crate::error::Error;
23use crate::error::VfsResult;
24use crate::prelude::*;
25#[cfg(feature = "async")]
26use crate::traits::async_vfs::WriteSupportingVfsAsync;
27use crate::traits::vfs;
28use crate::traits::vfs::WriteSupportingVfs;
29
30/// A wrapper type that enables atomic writes to directories.
31///
32/// When writing, it first writes to a temporary directory and then renames it
33/// to the target directory, ensuring that the target directory is never left
34/// in a partially updated state.
35///
36/// If the write fails, the original directory remains unchanged, and the temporary
37/// directory is cleaned up, before the error is propagated.
38///
39/// The Vfs used for writing must support temporary directories via the
40/// [`VfsSupportsTemporaryDirectories`] trait (or its async counterpart
41/// [`VfsSupportsTemporaryDirectoriesAsync`] when using async writes).
42pub struct AtomicDir<T>(pub T);
43
44impl<'vfs, T, Vfs: vfs::Vfs<'vfs> + 'vfs> ReadFrom<'vfs, Vfs> for AtomicDir<T>
45where
46    T: ReadFrom<'vfs, Vfs>,
47{
48    fn read_from(path: &Vfs::Path, vfs: Pin<&'vfs Vfs>) -> VfsResult<Self, Vfs> {
49        T::read_from(path, vfs).map(Self)
50    }
51}
52
53#[cfg(feature = "async")]
54impl<'vfs, T, Vfs: VfsAsync + 'vfs> ReadFromAsync<'vfs, Vfs> for AtomicDir<T>
55where
56    T: ReadFromAsync<'vfs, Vfs> + Send + 'static,
57{
58    type Future = Pin<Box<dyn Future<Output = VfsResult<Self, Vfs>> + Send + 'vfs>>;
59
60    fn read_from_async(
61        path: <Vfs::Path as PathType>::OwnedPath,
62        vfs: Pin<&'vfs Vfs>,
63    ) -> Self::Future {
64        let fut = T::read_from_async(path, vfs);
65        Box::pin(async move { fut.await.map(Self) })
66    }
67}
68
69/// An API for temporary directories provided by a virtual file system (VFS).
70#[must_use]
71pub trait TempDirApi<'vfs> {
72    /// The associated VFS type.
73    type Vfs: WriteSupportingVfs<'vfs>;
74    /// Returns the path of the temporary directory.
75    fn path(&self) -> &<Self::Vfs as VfsCore>::Path;
76
77    /// Persists the temporary directory at the specified path in the VFS.
78    fn persist_at(
79        self,
80        vfs: Pin<&'vfs Self::Vfs>,
81        path: &<Self::Vfs as VfsCore>::Path,
82    ) -> VfsResult<(), Self::Vfs>;
83
84    /// Deletes the temporary directory.
85    fn delete(self, vfs: Pin<&'vfs Self::Vfs>) -> VfsResult<(), Self::Vfs>;
86}
87
88/// A trait for VFS implementations that support temporary directories.
89pub trait VfsSupportsTemporaryDirectories<'vfs>: WriteSupportingVfs<'vfs> {
90    /// The type representing a temporary directory.
91    type TemporaryDirectory: TempDirApi<'vfs, Vfs = Self>;
92    /// Creates and returns a new temporary directory.
93    fn create_temporary_directory(
94        self: Pin<&'vfs Self>,
95    ) -> VfsResult<Self::TemporaryDirectory, Self>;
96}
97
98impl<'vfs, T, Vfs: vfs::Vfs<'vfs> + vfs::WriteSupportingVfs<'vfs> + 'vfs> WriteTo<'vfs, Vfs>
99    for AtomicDir<T>
100where
101    T: WriteTo<'vfs, Vfs>,
102    Vfs: VfsSupportsTemporaryDirectories<'vfs>,
103{
104    fn write_to(&self, path: &Vfs::Path, vfs: Pin<&'vfs Vfs>) -> VfsResult<(), Vfs> {
105        let temp_dir = vfs.create_temporary_directory()?;
106        match self.0.write_to(temp_dir.path(), vfs) {
107            Ok(()) => temp_dir.persist_at(vfs, path),
108            Err(e) => {
109                let _ = temp_dir.delete(vfs); // try to clean up the temp dir, but ignore errors from that
110                Err(e)
111            }
112        }
113    }
114}
115
116/// An async version of the [`TempDirApi`] trait.
117#[cfg(feature = "async")]
118#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
119pub trait TempDirApiAsync<'vfs> {
120    /// The associated VFS type.
121    type Vfs: VfsAsync + WriteSupportingVfsAsync;
122    /// The future type for the [`persist_at` method](TempDirApiAsync::persist_at).
123    type FuturePersistAt: Future<Output = VfsResult<(), Self::Vfs>> + Send + 'vfs;
124    /// The future type for the [`delete` method](TempDirApiAsync::delete).
125    type FutureDelete: Future<Output = VfsResult<(), Self::Vfs>> + Send + 'vfs;
126
127    /// Returns the path of the temporary directory.
128    fn path(&self) -> &<Self::Vfs as VfsCore>::Path;
129
130    /// Persists the temporary directory at the specified path in the VFS.
131    fn persist_at(
132        self,
133        vfs: Pin<&'vfs Self::Vfs>,
134        path: <<Self::Vfs as VfsCore>::Path as PathType>::OwnedPath,
135    ) -> Self::FuturePersistAt;
136
137    /// Deletes the temporary directory.
138    fn delete(self, vfs: Pin<&'vfs Self::Vfs>) -> Self::FutureDelete;
139}
140
141/// A trait for VFS implementations that support temporary directories in async contexts.
142#[cfg(feature = "async")]
143#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
144pub trait VfsSupportsTemporaryDirectoriesAsync<'vfs>: VfsAsync + WriteSupportingVfsAsync {
145    /// The type representing a temporary directory.
146    type TemporaryDirectory: TempDirApiAsync<'vfs, Vfs = Self> + Send;
147    /// The future type for the [`create_temporary_directory` method](VfsSupportsTemporaryDirectoriesAsync::create_temporary_directory).
148    type TemporaryDirectoryFuture: Future<Output = VfsResult<Self::TemporaryDirectory, Self>>
149        + Send
150        + 'vfs;
151    /// Creates and returns a new temporary directory.
152    fn create_temporary_directory(self: Pin<&'vfs Self>) -> Self::TemporaryDirectoryFuture;
153}
154
155#[cfg(feature = "async")]
156#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
157impl<'vfs, T, Vfs: WriteSupportingVfsAsync + 'vfs> WriteToAsync<'vfs, Vfs> for AtomicDir<T>
158where
159    T: WriteToAsync<'vfs, Vfs> + Send + 'static,
160    Vfs: VfsSupportsTemporaryDirectoriesAsync<'vfs> + VfsAsync + Send,
161{
162    type Future = Pin<Box<dyn Future<Output = VfsResult<(), Vfs>> + Send + 'vfs>>;
163
164    fn write_to_async(
165        self,
166        path: <Vfs::Path as PathType>::OwnedPath,
167        vfs: Pin<&'vfs Vfs>,
168    ) -> Self::Future {
169        Box::pin(async move {
170            let temp_dir = vfs.create_temporary_directory().await?;
171            match self.0.write_to_async(temp_dir.path().owned(), vfs).await {
172                Ok(()) => temp_dir.persist_at(vfs, path).await,
173                Err(e) => {
174                    let fut = temp_dir.delete(vfs); // try to clean up the temp dir, but ignore errors from that
175                    let _ = fut.await;
176                    Err(e)
177                }
178            }
179        })
180    }
181}
182
183#[cfg(feature = "async")]
184#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
185#[pin_project(project_replace = AtomicDirWriteToAsyncRefFutureProj)]
186#[doc(hidden)]
187pub enum AtomicDirWriteToAsyncRefFuture<'r, 'vfs, T, Vfs>
188where
189    Vfs: WriteSupportingVfsAsync + 'vfs,
190    T: WriteToAsyncRef<'vfs, Vfs> + Sync + 'static,
191    <T as WriteToAsyncRef<'vfs, Vfs>>::Future<'r>:
192        Future<Output = VfsResult<(), Vfs>> + Unpin + Send + 'r,
193    Vfs: VfsSupportsTemporaryDirectoriesAsync<'r> + VfsAsync + Send + 'static,
194    'vfs: 'r,
195{
196    CreatingTempDir(
197        Pin<
198            Box<
199                dyn Future<
200                        Output = VfsResult<
201                            <Vfs as VfsSupportsTemporaryDirectoriesAsync<'r>>::TemporaryDirectory,
202                            Vfs,
203                        >,
204                    > + Send
205                    + 'r,
206            >,
207        >,
208        &'r T,
209        <Vfs::Path as PathType>::OwnedPath,
210        Pin<&'r Vfs>,
211    ),
212    WritingToTempDir(
213        <Vfs as VfsSupportsTemporaryDirectoriesAsync<'r>>::TemporaryDirectory,
214        <T as WriteToAsyncRef<'vfs, Vfs>>::Future<'r>,
215        <Vfs::Path as PathType>::OwnedPath,
216        Pin<&'r Vfs>,
217    ),
218    PersistingTempDir(Pin<Box<dyn Future<Output = VfsResult<(), Vfs>> + Send + 'r>>),
219    DeletingTempDir(
220        Pin<Box<dyn Future<Output = VfsResult<(), Vfs>> + Send + 'r>>,
221        Error<<Vfs::Path as PathType>::OwnedPath>,
222    ),
223    Done,
224    Poison,
225}
226
227#[cfg(feature = "async")]
228#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
229impl<'r, 'vfs, T, Vfs> Future for AtomicDirWriteToAsyncRefFuture<'r, 'vfs, T, Vfs>
230where
231    Vfs: WriteSupportingVfsAsync + 'vfs,
232    T: WriteToAsyncRef<'vfs, Vfs> + Sync + 'static,
233    <T as WriteToAsyncRef<'vfs, Vfs>>::Future<'r>:
234        Future<Output = VfsResult<(), Vfs>> + Unpin + Send + 'r,
235    Vfs: VfsSupportsTemporaryDirectoriesAsync<'r> + VfsAsync + Send + 'static,
236    'vfs: 'r,
237{
238    type Output = VfsResult<(), Vfs>;
239
240    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
241        let this = self.as_mut().project_replace(Self::Poison);
242        match this {
243            AtomicDirWriteToAsyncRefFutureProj::CreatingTempDir(mut fut, val, path, vfs) => {
244                match fut.as_mut().poll(cx) {
245                    Poll::Ready(Ok(temp_dir)) => {
246                        let write_fut = val.write_to_async_ref(temp_dir.path().owned(), vfs);
247                        self.as_mut().project_replace(Self::WritingToTempDir(
248                            temp_dir, write_fut, path, vfs,
249                        ));
250                        cx.waker().wake_by_ref();
251                        Poll::Pending
252                    }
253                    Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
254                    Poll::Pending => {
255                        self.as_mut()
256                            .project_replace(Self::CreatingTempDir(fut, val, path, vfs));
257                        Poll::Pending
258                    }
259                }
260            }
261            AtomicDirWriteToAsyncRefFutureProj::WritingToTempDir(temp_dir, mut fut, path, vfs) => {
262                match Pin::new(&mut fut).poll(cx) {
263                    Poll::Ready(Ok(())) => {
264                        let persist_fut = temp_dir.persist_at(vfs.clone(), path.clone());
265                        self.as_mut()
266                            .project_replace(Self::PersistingTempDir(Box::pin(persist_fut)));
267                        cx.waker().wake_by_ref();
268                        Poll::Pending
269                    }
270                    Poll::Ready(Err(e)) => {
271                        let delete_fut = temp_dir.delete(vfs.clone());
272                        self.as_mut()
273                            .project_replace(Self::DeletingTempDir(Box::pin(delete_fut), e));
274                        cx.waker().wake_by_ref();
275                        Poll::Pending
276                    }
277                    Poll::Pending => {
278                        self.as_mut()
279                            .project_replace(Self::WritingToTempDir(temp_dir, fut, path, vfs));
280                        Poll::Pending
281                    }
282                }
283            }
284            AtomicDirWriteToAsyncRefFutureProj::PersistingTempDir(mut fut) => {
285                match fut.as_mut().poll(cx) {
286                    Poll::Ready(result) => {
287                        *self = Self::Done;
288                        Poll::Ready(result)
289                    }
290                    Poll::Pending => {
291                        self.as_mut().project_replace(Self::PersistingTempDir(fut));
292                        Poll::Pending
293                    }
294                }
295            }
296            AtomicDirWriteToAsyncRefFutureProj::DeletingTempDir(mut fut, error) => {
297                match fut.as_mut().poll(cx) {
298                    Poll::Ready(_) => Poll::Ready(Err(error)),
299                    Poll::Pending => {
300                        self.as_mut()
301                            .project_replace(Self::DeletingTempDir(fut, error));
302                        Poll::Pending
303                    }
304                }
305            }
306            AtomicDirWriteToAsyncRefFutureProj::Done => panic!("polled after completion"),
307            AtomicDirWriteToAsyncRefFutureProj::Poison => panic!("future was poisoned"),
308        }
309    }
310}
311
312#[cfg(feature = "async")]
313#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
314impl<'vfs, T, Vfs: WriteSupportingVfsAsync + 'vfs> WriteToAsyncRef<'vfs, Vfs> for AtomicDir<T>
315where
316    T: WriteToAsyncRef<'vfs, Vfs> + Sync + 'static,
317    for<'r> <T as WriteToAsyncRef<'vfs, Vfs>>::Future<'r>:
318        Future<Output = VfsResult<(), Vfs>> + Unpin + Send + 'r,
319    for<'r> Vfs: VfsSupportsTemporaryDirectoriesAsync<'r> + VfsAsync + Send + 'static,
320{
321    type Future<'r>
322        = AtomicDirWriteToAsyncRefFuture<'r, 'vfs, T, Vfs>
323    where
324        'vfs: 'r,
325        Self: 'r;
326
327    fn write_to_async_ref<'r>(
328        &'r self,
329        path: <Vfs::Path as PathType>::OwnedPath,
330        vfs: Pin<&'r Vfs>,
331    ) -> Self::Future<'r>
332    where
333        'vfs: 'r,
334    {
335        AtomicDirWriteToAsyncRefFuture::CreatingTempDir(
336            Box::pin(vfs.create_temporary_directory()),
337            &self.0,
338            path,
339            vfs,
340        )
341    }
342}