Skip to main content

qubit_fs/write/
rejected_writer.rs

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