Skip to content

Commit 6c2aeff

Browse files
committed
feat: an atomic dir util tool
1 parent 10ef2f0 commit 6c2aeff

7 files changed

Lines changed: 496 additions & 3 deletions

File tree

.github/workflows/test-dir-structure.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,12 @@ jobs:
143143
- name: Run feature-combo tests (--features assert_eq,include_dir)
144144
uses: ./.github/actions/test-dir-structure
145145
with: { feature-combos: "--features assert_eq,include_dir" }
146-
146+
- name: Run feature-combo tests (--no-default-features --features tools-atomic-dir)
147+
uses: ./.github/actions/test-dir-structure
148+
with: { feature-combos: "--no-default-features --features tools-atomic-dir" }
149+
- name: Run feature-combo tests (--no-default-features --features tools-atomic-dir,async)
150+
uses: ./.github/actions/test-dir-structure
151+
with: { feature-combos: "--no-default-features --features tools-atomic-dir,async" }
152+
- name: Run feature-combo tests (--no-default-features --features tools-atomic-dir,async,tokio)
153+
uses: ./.github/actions/test-dir-structure
154+
with: { feature-combos: "--no-default-features --features tools-atomic-dir,async,tokio" }

src/dev/dir-structure/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ rustdoc-args = ["--cfg", "docsrs"]
1313
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1414

1515
[features]
16-
default = ["derive", "tools"]
16+
default = ["derive", "tools", "_all"]
1717
json = ["dep:serde", "dep:serde_json", "tools-data-formats"]
1818
toml = ["dep:serde", "dep:toml", "tools-data-formats"]
1919
yaml = ["dep:serde", "dep:serde_yaml", "tools-data-formats"]
@@ -61,6 +61,7 @@ all-image-formats = [
6161
"image-format-qoi",
6262
]
6363

64+
tools-atomic-dir = []
6465
tools-clean-dir = []
6566
tools-data-formats = []
6667
tools-deferred-read = []
@@ -72,6 +73,7 @@ tools-try-parse = []
7273
tools-versioned = []
7374
tools-versioned-hash = []
7475
tools = [
76+
"tools-atomic-dir",
7577
"tools-clean-dir",
7678
"tools-deferred-read",
7779
"tools-deferred-read-or-own",
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
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")]
11+
use std::future::Future;
12+
use std::pin::Pin;
13+
#[cfg(feature = "async")]
14+
use std::task::Context;
15+
#[cfg(feature = "async")]
16+
use std::task::Poll;
17+
18+
#[cfg(feature = "async")]
19+
use pin_project::pin_project;
20+
21+
#[cfg(feature = "async")]
22+
use crate::error::Error;
23+
use crate::error::VfsResult;
24+
use crate::prelude::*;
25+
#[cfg(feature = "async")]
26+
use crate::traits::async_vfs::WriteSupportingVfsAsync;
27+
use crate::traits::vfs;
28+
use 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).
42+
pub struct AtomicDir<T>(pub T);
43+
44+
impl<'vfs, T, Vfs: vfs::Vfs<'vfs> + 'vfs> ReadFrom<'vfs, Vfs> for AtomicDir<T>
45+
where
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")]
54+
impl<'vfs, T, Vfs: VfsAsync + 'vfs> ReadFromAsync<'vfs, Vfs> for AtomicDir<T>
55+
where
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]
71+
pub 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.
89+
pub 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+
98+
impl<'vfs, T, Vfs: vfs::Vfs<'vfs> + vfs::WriteSupportingVfs<'vfs> + 'vfs> WriteTo<'vfs, Vfs>
99+
for AtomicDir<T>
100+
where
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")))]
119+
pub 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")))]
144+
pub 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")))]
157+
impl<'vfs, T, Vfs: WriteSupportingVfsAsync + 'vfs> WriteToAsync<'vfs, Vfs> for AtomicDir<T>
158+
where
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)]
187+
pub enum AtomicDirWriteToAsyncRefFuture<'r, 'vfs, T, Vfs>
188+
where
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")))]
229+
impl<'r, 'vfs, T, Vfs> Future for AtomicDirWriteToAsyncRefFuture<'r, 'vfs, T, Vfs>
230+
where
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+
return Poll::Ready(result);
289+
}
290+
Poll::Pending => return Poll::Pending,
291+
}
292+
}
293+
AtomicDirWriteToAsyncRefFutureProj::DeletingTempDir(mut fut, error) => {
294+
match fut.as_mut().poll(cx) {
295+
Poll::Ready(_) => Poll::Ready(Err(error)),
296+
Poll::Pending => {
297+
self.as_mut()
298+
.project_replace(Self::DeletingTempDir(fut, error));
299+
Poll::Pending
300+
}
301+
}
302+
}
303+
AtomicDirWriteToAsyncRefFutureProj::Done => panic!("polled after completion"),
304+
AtomicDirWriteToAsyncRefFutureProj::Poison => panic!("future was poisoned"),
305+
}
306+
}
307+
}
308+
309+
#[cfg(feature = "async")]
310+
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
311+
impl<'vfs, T, Vfs: WriteSupportingVfsAsync + 'vfs> WriteToAsyncRef<'vfs, Vfs> for AtomicDir<T>
312+
where
313+
T: WriteToAsyncRef<'vfs, Vfs> + Sync + 'static,
314+
for<'r> <T as WriteToAsyncRef<'vfs, Vfs>>::Future<'r>:
315+
Future<Output = VfsResult<(), Vfs>> + Unpin + Send + 'r,
316+
for<'r> Vfs: VfsSupportsTemporaryDirectoriesAsync<'r> + VfsAsync + Send + 'static,
317+
{
318+
type Future<'r>
319+
= AtomicDirWriteToAsyncRefFuture<'r, 'vfs, T, Vfs>
320+
where
321+
'vfs: 'r,
322+
Self: 'r;
323+
324+
fn write_to_async_ref<'r>(
325+
&'r self,
326+
path: <Vfs::Path as PathType>::OwnedPath,
327+
vfs: Pin<&'r Vfs>,
328+
) -> Self::Future<'r>
329+
where
330+
'vfs: 'r,
331+
{
332+
AtomicDirWriteToAsyncRefFuture::CreatingTempDir(
333+
Box::pin(vfs.create_temporary_directory()),
334+
&self.0,
335+
path,
336+
vfs,
337+
)
338+
}
339+
}

src/dev/dir-structure/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ pub mod prelude {
156156
pub use crate::traits::vfs::WriteSupportingVfsExt;
157157
}
158158

159+
#[cfg(feature = "tools-atomic-dir")]
160+
pub mod atomic_dir;
159161
#[cfg(feature = "tools-clean-dir")]
160162
pub mod clean_dir;
161163
#[cfg(feature = "tools-data-formats")]

src/dev/dir-structure/src/traits/asy.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::traits::vfs::VfsCore;
1515
/// `async` version of [`ReadFrom`](crate::traits::sync::ReadFrom).
1616
#[cfg(feature = "async")]
1717
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
18-
pub trait ReadFromAsync<'a, Vfs: VfsAsync + ?Sized + 'a>: Sized {
18+
pub trait ReadFromAsync<'a, Vfs: VfsAsync + ?Sized + 'a>: Sized + 'a {
1919
/// The future type returned by the async read function.
2020
type Future: Future<Output = VfsResult<Self, Vfs>> + Send + Unpin + 'a
2121
where

0 commit comments

Comments
 (0)