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