qubit_fs/temp/
temp_directory.rs1use 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
34pub struct TempDirectory {
49 filesystem: FileSystem,
51 path: Path,
53 session: Box<dyn TempResourceSpi>,
55 lifecycle: TempLifecycle,
57}
58impl TempDirectory {
59 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 #[inline]
70 #[must_use]
71 pub const fn path(&self) -> &Path {
72 &self.path
73 }
74 #[inline]
76 #[must_use]
77 pub const fn state(&self) -> TempResourceState {
78 self.lifecycle.state()
79 }
80 #[inline]
82 #[must_use]
83 pub fn child(&self, component: &PathComponent) -> Path {
84 self.path.child(component)
85 }
86 #[inline]
88 #[must_use]
89 pub fn descendant(&self, relative: &RelativePath) -> Path {
90 self.path.join(relative)
91 }
92 #[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 #[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 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 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 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 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 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}