Skip to main content

qubit_fs/temp/
rejected_async_temp_resource.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//! Explicit cleanup authority for a rejected temp session.
9
10use std::fmt::Debug;
11use std::fmt::Formatter;
12use std::fmt::Result as FmtResult;
13use std::pin::Pin;
14
15use crate::error::FsError;
16use crate::error::FsErrorKind;
17use crate::error::FsOperation;
18use crate::error::FsResult;
19use crate::error::RecoveryCleanupState;
20use crate::facade::internal::RecoveryCleanupGuard;
21use crate::path::Path;
22use crate::spi::AsyncTempResourceSpi;
23use crate::spi::SpiFuture;
24
25/// Isolated cleanup ownership after a provider returned an invalid identity.
26///
27/// This handle cannot write or publish. Cleanup acts only on resources owned by
28/// its session, never on a diagnostic path. Drop performs no explicit cleanup
29/// or cancellation hook; retain this handle until cleanup is confirmed.
30///
31/// ```compile_fail
32/// use qubit_fs::temp::RejectedAsyncTempResource;
33/// fn cannot_publish(mut recovery: RejectedAsyncTempResource) {
34///     recovery.keep();
35/// }
36/// ```
37///
38/// # Examples
39///
40/// ```rust
41/// use qubit_fs::error::RecoveryCleanupState;
42/// use qubit_fs::temp::RejectedAsyncTempResource;
43///
44/// assert!(std::any::type_name::<RejectedAsyncTempResource>().contains("RejectedAsyncTempResource"));
45/// assert_eq!(RecoveryCleanupState::Pending, RecoveryCleanupState::Pending);
46/// ```
47#[must_use = "explicitly clean or retain the isolated recovery session"]
48pub struct RejectedAsyncTempResource {
49    /// The actual provider session, retained independently of cleanup futures.
50    session: Pin<Box<dyn AsyncTempResourceSpi>>,
51    /// Last observed cleanup state, never a publication snapshot.
52    state: RecoveryCleanupState,
53    /// Trusted configured provider identifier.
54    provider: Box<str>,
55    /// Requested path, never the unvalidated provider identity.
56    path: Option<Path>,
57}
58impl RejectedAsyncTempResource {
59    /// Takes ownership of a rejected session and trusted request context.
60    pub(crate) fn new(session: Box<dyn AsyncTempResourceSpi>, provider: &str, path: Option<Path>) -> Self {
61        Self {
62            session: Box::into_pin(session),
63            state: RecoveryCleanupState::Pending,
64            provider: provider.into(),
65            path,
66        }
67    }
68    /// Returns cleanup progress; errors and cancellation do not release
69    /// ownership.
70    pub const fn cleanup_state(&self) -> RecoveryCleanupState {
71        self.state
72    }
73
74    /// Explicitly cleans resources actually owned by this isolated session.
75    ///
76    /// A failed attempt retains the session for explicit retry or
77    /// reconciliation. No work occurs until polled. Dropping a polled,
78    /// unfinished future records Indeterminate; dropping an unpolled future
79    /// leaves the state unchanged.
80    ///
81    /// # Errors
82    /// Returns InvalidState after confirmed cleanup, or the contextual provider
83    /// error without replacing the original opening failure.
84    pub fn cleanup_async(&mut self) -> SpiFuture<'_, FsResult<()>> {
85        Box::pin(async move {
86            if self.state == RecoveryCleanupState::Completed {
87                return Err(self.contextual_error(FsError::new(
88                    FsErrorKind::InvalidState,
89                    FsOperation::CleanupTemp,
90                    "isolated session cleanup already completed",
91                )));
92            }
93            let mut guard = RecoveryCleanupGuard::start(&mut self.state);
94            let result = self.session.as_mut().cleanup().await;
95            guard.finish(result.is_ok());
96            drop(guard);
97            result.map_err(|error| self.contextual_error(error))
98        })
99    }
100    /// Adds only trusted request context to a cleanup error.
101    fn contextual_error(&self, error: FsError) -> FsError {
102        error.with_trusted_cleanup_context(FsOperation::CleanupTemp, self.path.as_ref(), &self.provider)
103    }
104}
105impl Debug for RejectedAsyncTempResource {
106    /// Omits the session and unvalidated provider identity.
107    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
108        f.debug_struct("RejectedAsyncTempResource")
109            .field("cleanup_state", &self.state)
110            .finish_non_exhaustive()
111    }
112}