Skip to main content

qubit_fs/temp/
async_temp_directory.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8// facade.
9//! Runtime-neutral asynchronous temporary-directory facade handle.
10
11use crate::AsyncFileSystem;
12use crate::error::FsResult;
13use crate::path::Path;
14use crate::path::PathComponent;
15use crate::path::RelativePath;
16use crate::spi::AsyncTempResourceSpi;
17use crate::spi::SpiFuture;
18use crate::temp::AsyncTempFile;
19use crate::temp::PersistFailure;
20use crate::temp::PersistOptions;
21use crate::temp::PersistOutcome;
22use crate::temp::TempResourceState;
23
24/// A facade-owned asynchronous temporary directory.
25///
26/// # Examples
27///
28/// ```rust
29/// # mod support { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/common/rustdoc_support.rs")); }
30/// # use support::*;
31/// # let (filesystem, _) = async_recording_spi::async_recording_file_system(Default::default());
32/// # poll_support::ready(async {
33/// use qubit_fs::temp::{TempOptions, TempResourceState};
34///
35/// let mut temporary = filesystem.create_temp_directory(TempOptions::default()).await?;
36/// temporary.cleanup().await?;
37/// assert_eq!(TempResourceState::Cleaned, temporary.state());
38/// # Ok::<(), Box<dyn std::error::Error>>(())
39/// # }).unwrap();
40/// ```
41pub struct AsyncTempDirectory(
42    /// Shared asynchronous temporary-resource lifecycle implementation.
43    AsyncTempFile,
44);
45
46impl AsyncTempDirectory {
47    /// Binds a validated provider temporary-directory session to its facade.
48    ///
49    /// # Parameters
50    /// - `file_system`: Facade that owns validation and persistence policy.
51    /// - `path`: Validated provider-local temporary path.
52    /// - `session`: Provider lifecycle session.
53    ///
54    /// # Returns
55    /// An owned asynchronous temporary-directory handle.
56    #[inline]
57    pub(crate) fn new(file_system: AsyncFileSystem, path: Path, session: Box<dyn AsyncTempResourceSpi>) -> Self {
58        Self(AsyncTempFile::new(file_system, path, session, "temporary directory"))
59    }
60
61    /// Returns the provider-local temporary path.
62    ///
63    /// # Returns
64    /// The validated path supplied by the provider.
65    #[inline]
66    #[must_use]
67    pub const fn path(&self) -> &Path {
68        self.0.path()
69    }
70
71    /// Returns the current ownership lifecycle state.
72    ///
73    /// # Returns
74    /// The handle's current cleanup and publication state.
75    #[inline]
76    #[must_use]
77    pub const fn state(&self) -> TempResourceState {
78        self.0.state()
79    }
80
81    /// Returns one lexically safe child path.
82    #[inline]
83    #[must_use]
84    pub fn child(&self, component: &PathComponent) -> Path {
85        self.0.child(component)
86    }
87
88    /// Returns one lexically safe descendant path.
89    #[inline]
90    #[must_use]
91    pub fn descendant(&self, relative: &RelativePath) -> Path {
92        self.0.descendant(relative)
93    }
94
95    /// Asynchronously confirms cleanup of this temporary directory.
96    ///
97    /// # Returns
98    /// A future resolving after provider cleanup is confirmed.
99    ///
100    /// # Errors
101    /// Resolves to an invalid-state error when cleanup is no longer legal, or
102    /// to the provider cleanup failure.
103    #[inline]
104    pub fn cleanup(&mut self) -> SpiFuture<'_, FsResult<()>> {
105        self.0.cleanup()
106    }
107
108    /// Asynchronously publishes this directory to a generated target.
109    ///
110    /// # Returns
111    /// A future resolving to the confirmed publication outcome.
112    ///
113    /// # Errors
114    /// Resolves to an invalid-state error when the directory is no longer
115    /// owned, or to the provider ownership-transfer failure.
116    #[inline]
117    pub fn keep(&mut self) -> SpiFuture<'_, Result<PersistOutcome, PersistFailure>> {
118        self.0.keep()
119    }
120
121    /// Asynchronously persists this directory to a validated destination.
122    ///
123    /// # Parameters
124    /// - `target`: Validated destination path.
125    /// - `options`: Persistence atomicity and publication requirements.
126    ///
127    /// # Returns
128    /// A future resolving to the confirmed persistence outcome.
129    ///
130    /// # Errors
131    /// Resolves to a typed failure for invalid lifecycle state, failed local
132    /// preflight, provider failure, or provider contract violation.
133    #[inline]
134    pub fn persist<'a>(
135        &'a mut self,
136        target: &'a Path,
137        options: PersistOptions,
138    ) -> SpiFuture<'a, Result<PersistOutcome, PersistFailure>> {
139        self.0.persist(target, options)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use std::pin::Pin;
146
147    use super::AsyncTempDirectory;
148    use crate::AsyncFileSystem;
149    use crate::error::FsResult;
150    use crate::metadata::FileKind;
151    use crate::metadata::FileSystemCapabilities;
152    use crate::metadata::FileSystemCapability;
153    use crate::metadata::FileSystemId;
154    use crate::metadata::FileSystemInfo;
155    use crate::metadata::FileSystemLimits;
156    use crate::metadata::SymlinkPolicy;
157    use crate::path::Path;
158    use crate::path::PathComponent;
159    use crate::path::PathConstraints;
160    use crate::path::PathSemantics;
161    use crate::path::RelativePath;
162    use crate::spi::AsyncFileSystemSpi;
163    use crate::spi::AsyncTempResourceSpi;
164    use crate::spi::PersistRequest;
165    use crate::spi::ProviderProperties;
166    use crate::spi::SpiFuture;
167    use crate::spi::SpiPersistFailure;
168    use crate::spi::StatRequest;
169    use crate::spi::StatResponse;
170    use crate::temp::PersistOptions;
171    use crate::temp::PersistOutcome;
172    use crate::temp::TempResourceState;
173
174    struct Session;
175
176    impl AsyncTempResourceSpi for Session {
177        fn cleanup<'a>(self: Pin<&'a mut Self>) -> SpiFuture<'a, FsResult<()>> {
178            Box::pin(async { panic!("test future is not polled") })
179        }
180
181        fn keep<'a>(self: Pin<&'a mut Self>) -> SpiFuture<'a, Result<PersistOutcome, SpiPersistFailure>> {
182            Box::pin(async { panic!("test future is not polled") })
183        }
184
185        fn persist<'a>(
186            self: Pin<&'a mut Self>,
187            _: PersistRequest<'a>,
188        ) -> SpiFuture<'a, Result<PersistOutcome, SpiPersistFailure>> {
189            Box::pin(async { panic!("test future is not polled") })
190        }
191    }
192
193    struct Provider {
194        properties: ProviderProperties,
195    }
196
197    impl AsyncFileSystemSpi for Provider {
198        fn properties(&self) -> ProviderProperties {
199            self.properties.clone()
200        }
201
202        fn stat<'a>(&'a self, _: StatRequest<'a>) -> SpiFuture<'a, FsResult<StatResponse>> {
203            Box::pin(async {
204                Ok(StatResponse::new(
205                    Path::parse("/tmp/recording").expect("valid path"),
206                    crate::metadata::FileMetadata::new(FileKind::Directory),
207                ))
208            })
209        }
210    }
211
212    fn filesystem() -> AsyncFileSystem {
213        let properties = ProviderProperties::new(
214            FileSystemInfo::new(
215                FileSystemId::new("async-temp-directory-test").expect("valid id"),
216                "test",
217                PathSemantics::Hierarchical,
218            ),
219            crate::spi::ProviderOperations::new().with(crate::spi::ProviderOperation::CreateTempDirectory),
220            FileSystemCapabilities::new().with_guaranteed(FileSystemCapability::TempDirectory),
221            FileSystemLimits::unknown(),
222            PathConstraints::absolute(),
223            SymlinkPolicy::Reject,
224        )
225        .expect("valid properties");
226        AsyncFileSystem::from_spi(Provider { properties }).expect("valid filesystem")
227    }
228
229    #[test]
230    fn forwarding_methods_are_executed_at_runtime() {
231        let file_system = filesystem();
232        let mut directory = AsyncTempDirectory::new(
233            file_system,
234            Path::parse("/tmp/recording").expect("valid path"),
235            Box::new(Session),
236        );
237        let component = PathComponent::parse("child").expect("valid component");
238        let relative = RelativePath::parse("nested/item").expect("valid relative path");
239
240        assert_eq!(directory.path().as_str(), "/tmp/recording");
241        assert_eq!(directory.state(), TempResourceState::Owned);
242        assert_eq!(directory.child(&component).as_str(), "/tmp/recording/child");
243        assert_eq!(directory.descendant(&relative).as_str(), "/tmp/recording/nested/item");
244        drop(directory.cleanup());
245        drop(directory.keep());
246        drop(directory.persist(&Path::parse("/target").expect("valid path"), PersistOptions::default()));
247    }
248}