Skip to main content

qubit_fs/temp/
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//! Provider-backed temporary directory lifecycle handle.
9
10use std::fmt::Debug;
11use std::fmt::Formatter;
12use std::fmt::Result as FmtResult;
13
14use crate::FileSystem;
15use crate::error::FsError;
16use crate::error::FsErrorKind;
17use crate::error::FsOperation;
18use crate::error::FsResult;
19use crate::metadata::AchievedAtomicity;
20use crate::metadata::AtomicityRequirement;
21use crate::path::Path;
22use crate::path::PathComponent;
23use crate::path::RelativePath;
24use crate::spi::PersistRequest;
25use crate::spi::SpiPersistFailure;
26use crate::spi::TempResourceSpi;
27use crate::temp::PersistFailure;
28use crate::temp::PersistFailureState;
29use crate::temp::PersistOptions;
30use crate::temp::PersistOutcome;
31use crate::temp::TempResourceState;
32use crate::temp::internal::TempLifecycle;
33
34/// Temporary directory retaining the provider session until lifecycle
35/// completion.
36///
37/// # Examples
38///
39/// ```rust
40/// # mod support { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/common/rustdoc_support.rs")); }
41/// # let filesystem = support::rustdoc_provider::filesystem();
42/// use qubit_fs::temp::{TempOptions, TempResourceState};
43///
44/// let mut temporary = filesystem.create_temp_directory(TempOptions::default())?;
45/// assert_eq!(TempResourceState::Owned, temporary.state());
46/// # Ok::<(), Box<dyn std::error::Error>>(())
47/// ```
48pub struct TempDirectory {
49    /// Facade that owns validation and persistence policy.
50    filesystem: FileSystem,
51    /// Provider-local temporary directory path.
52    path: Path,
53    /// Provider lifecycle session.
54    session: Box<dyn TempResourceSpi>,
55    /// Current cleanup and publication lifecycle state.
56    lifecycle: TempLifecycle,
57}
58impl TempDirectory {
59    /// Creates the facade handle from validated provider parts.
60    pub(crate) fn new(filesystem: FileSystem, path: Path, session: Box<dyn TempResourceSpi>) -> Self {
61        Self {
62            filesystem,
63            path,
64            session,
65            lifecycle: TempLifecycle::new(),
66        }
67    }
68    /// Returns the logical temporary directory path.
69    #[inline]
70    #[must_use]
71    pub const fn path(&self) -> &Path {
72        &self.path
73    }
74    /// Returns the resource lifecycle state.
75    #[inline]
76    #[must_use]
77    pub const fn state(&self) -> TempResourceState {
78        self.lifecycle.state()
79    }
80    /// Returns one lexically safe child path.
81    #[inline]
82    #[must_use]
83    pub fn child(&self, component: &PathComponent) -> Path {
84        self.path.child(component)
85    }
86    /// Returns one lexically safe descendant path.
87    #[inline]
88    #[must_use]
89    pub fn descendant(&self, relative: &RelativePath) -> Path {
90        self.path.join(relative)
91    }
92    /// Persists this directory.
93    ///
94    /// # Parameters
95    /// - `target`: Destination path validated by the owning filesystem.
96    /// - `options`: Atomicity and publication requirements.
97    ///
98    /// # Returns
99    /// The provider-confirmed publication outcome.
100    ///
101    /// # Errors
102    /// Returns a typed failure for invalid lifecycle state, failed preflight,
103    /// provider failure, or a provider contract violation.
104    #[allow(clippy::result_large_err)]
105    pub fn persist(&mut self, target: &Path, options: PersistOptions) -> Result<PersistOutcome, PersistFailure> {
106        if self.lifecycle.state() != TempResourceState::Owned {
107            return Err(PersistFailure::new(
108                self.invalid_state(FsOperation::PersistTemp),
109                self.lifecycle.failure_state(),
110            )
111            .with_publication_target(self.lifecycle.publication_target()));
112        }
113        if let Err(error) = self.filesystem.preflight_temp_persist(&self.path, target, &options) {
114            return Err(PersistFailure::new(error, PersistFailureState::NotPublished));
115        }
116        match self.session.persist(PersistRequest::new(target, options.clone())) {
117            Ok(outcome) => {
118                if outcome.target() != target {
119                    self.lifecycle
120                        .record_failure(PersistFailureState::Indeterminate, Some(target.clone()), false);
121                    return Err(PersistFailure::new(
122                        FsError::new(
123                            FsErrorKind::ProviderContractViolation,
124                            FsOperation::PersistTemp,
125                            "provider reported a persistence target different from the request",
126                        )
127                        .with_path(self.path.clone())
128                        .with_target(target.clone()),
129                        PersistFailureState::Indeterminate,
130                    ));
131                }
132                if options.atomicity() == AtomicityRequirement::Required
133                    && outcome.atomicity() != AchievedAtomicity::Atomic
134                {
135                    self.lifecycle.record_failure(
136                        PersistFailureState::PublishedSourceRetained,
137                        Some(target.clone()),
138                        false,
139                    );
140                    return Err(PersistFailure::new(
141                        FsError::new(
142                            FsErrorKind::ProviderContractViolation,
143                            FsOperation::PersistTemp,
144                            "provider reported non-atomic success for atomic-required persist",
145                        )
146                        .with_path(self.path.clone())
147                        .with_target(target.clone()),
148                        PersistFailureState::PublishedSourceRetained,
149                    )
150                    .with_publication_target(self.lifecycle.publication_target()));
151                }
152                self.lifecycle.record_success(false, outcome.target().clone());
153                Ok(outcome)
154            }
155            Err(failure) => Err(self.record_persist_failure(failure, target, FsOperation::PersistTemp)),
156        }
157    }
158    /// Publishes this temporary directory to the provider-generated target.
159    ///
160    /// # Returns
161    /// The provider-confirmed publication outcome and generated target.
162    ///
163    /// # Errors
164    /// Returns a typed failure when ownership is unavailable, provider
165    /// publication fails, or the provider returns an invalid target.
166    #[allow(clippy::result_large_err)]
167    pub fn keep(&mut self) -> Result<PersistOutcome, PersistFailure> {
168        if let Err(error) = self.ensure_owned(FsOperation::KeepTemp) {
169            return Err(PersistFailure::new(error, self.lifecycle.failure_state())
170                .with_publication_target(self.lifecycle.publication_target()));
171        }
172        match self.session.keep() {
173            Ok(outcome) => {
174                if let Err(error) = self.filesystem.validate_temp_keep_target(&self.path, outcome.target()) {
175                    self.lifecycle.record_failure(
176                        PersistFailureState::Indeterminate,
177                        Some(outcome.target().clone()),
178                        true,
179                    );
180                    return Err(PersistFailure::new(error, PersistFailureState::Indeterminate));
181                }
182                self.path = outcome.target().clone();
183                self.lifecycle.record_success(true, outcome.target().clone());
184                Ok(outcome)
185            }
186            Err(failure) => Err(self.record_persist_failure(failure, &self.path.clone(), FsOperation::KeepTemp)),
187        }
188    }
189    /// Cleans the temporary directory.
190    ///
191    /// # Errors
192    /// Returns an invalid-state error when cleanup is no longer legal, or the
193    /// provider cleanup error when cleanup cannot be confirmed.
194    pub fn cleanup(&mut self) -> FsResult<()> {
195        if !matches!(
196            self.lifecycle.state(),
197            TempResourceState::Owned | TempResourceState::CleanupRequired
198        ) {
199            return Err(self.invalid_state(FsOperation::CleanupTemp));
200        }
201        self.session
202            .cleanup()
203            .map(|()| self.lifecycle.record_cleanup_success())
204            .map_err(|error| self.record_lifecycle_error(error, FsOperation::CleanupTemp))
205    }
206    /// Records provider partial persistence facts.
207    fn record_persist_failure(
208        &mut self,
209        failure: SpiPersistFailure,
210        target: &Path,
211        operation: FsOperation,
212    ) -> PersistFailure {
213        let (error, state) = failure.into_parts();
214        let publication_target = if operation == FsOperation::KeepTemp {
215            error.target().cloned()
216        } else {
217            Some(target.clone())
218        };
219        self.lifecycle
220            .record_failure(state, publication_target, operation == FsOperation::KeepTemp);
221        PersistFailure::new(
222            error.with_operation(operation).with_missing_context(
223                &self.path,
224                Some(target),
225                self.filesystem.properties().info().provider_id(),
226            ),
227            state,
228        )
229        .with_publication_target(self.lifecycle.publication_target())
230    }
231    /// Requires ownership of an unpublished source.
232    fn ensure_owned(&self, operation: FsOperation) -> FsResult<()> {
233        if self.lifecycle.state() == TempResourceState::Owned {
234            Ok(())
235        } else {
236            Err(self.invalid_state(operation))
237        }
238    }
239    /// Records lifecycle error state with resource context.
240    fn record_lifecycle_error(&mut self, error: FsError, operation: FsOperation) -> FsError {
241        self.lifecycle.record_cleanup_error(&error);
242        error.with_operation(operation).with_missing_context(
243            &self.path,
244            None,
245            self.filesystem.properties().info().provider_id(),
246        )
247    }
248    /// Builds a contextual invalid-state error.
249    fn invalid_state(&self, operation: FsOperation) -> FsError {
250        FsError::new(
251            FsErrorKind::InvalidState,
252            operation,
253            "temporary directory cannot perform this lifecycle operation",
254        )
255        .with_path(self.path.clone())
256    }
257}
258impl Debug for TempDirectory {
259    #[inline]
260    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
261        f.debug_struct("TempDirectory")
262            .field("path", &self.path)
263            .field("state", &self.lifecycle.state())
264            .finish_non_exhaustive()
265    }
266}
267impl Drop for TempDirectory {
268    fn drop(&mut self) {
269        if matches!(
270            self.lifecycle.state(),
271            TempResourceState::Owned | TempResourceState::CleanupRequired
272        ) {
273            let _ = self.session.cleanup();
274        }
275    }
276}