Skip to main content

qubit_fs/
file_system.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//! Validating synchronous filesystem facade.
9
10use std::sync::Arc;
11
12use crate::copy::CopyAssessment;
13use crate::copy::CopyFailure;
14use crate::copy::CopyOperation;
15use crate::copy::CopyOptions;
16use crate::copy::CopyOutcome;
17use crate::directory::CreateDirectoryOptions;
18use crate::directory::CreateDirectoryOutcome;
19use crate::directory::DeleteOptions;
20use crate::directory::DeleteOutcome;
21use crate::directory::DirectoryOperation;
22use crate::directory::DirectoryStream;
23use crate::directory::ListOptions;
24use crate::directory::ListScope;
25use crate::error::FsError;
26use crate::error::FsErrorKind;
27use crate::error::FsOperation;
28use crate::error::FsResult;
29use crate::error::OpenFailure;
30use crate::error::OpenFailureStage;
31use crate::facade::facade_core::FacadeCore;
32use crate::metadata::FileMetadata;
33use crate::metadata::FileSystemCapability;
34use crate::metadata::FileSystemProperties;
35use crate::path::Path;
36use crate::read::FileReader;
37use crate::read::ReadOperation;
38use crate::read::ReadOptions;
39use crate::rename::RenameFailure;
40use crate::rename::RenameFailureState;
41use crate::rename::RenameOptions;
42use crate::rename::RenameOutcome;
43use crate::rename::validate_rename_outcome;
44use crate::spi::CreateDirectoryRequest;
45use crate::spi::DeleteDirectoryRequest;
46use crate::spi::DeleteFileRequest;
47use crate::spi::FileSystemSpi;
48use crate::spi::OpenReaderRequest;
49use crate::spi::OpenWriterRequest;
50use crate::spi::RenameRequest;
51use crate::spi::ResolvedCreateDirectoryOptions;
52use crate::spi::ResolvedDeleteOptions;
53use crate::spi::ResolvedReadOptions;
54use crate::spi::ResolvedRenameOptions;
55use crate::spi::ResolvedWriteOptions;
56use crate::spi::StatRequest;
57use crate::temp::RejectedTempResource;
58use crate::temp::TempDirectory;
59use crate::temp::TempFile;
60use crate::temp::TempOptions;
61use crate::write::FileWriter;
62use crate::write::RejectedWriter;
63use crate::write::WriteAllFailure;
64use crate::write::WriteOperation;
65use crate::write::WriteOptions;
66
67/// Application-facing synchronous filesystem facade.
68///
69/// # Examples
70///
71/// The example runs against an isolated in-memory fixture. Applications obtain
72/// their configured facade from a provider or registry integration.
73///
74/// ```rust
75/// # mod support { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/common/rustdoc_support.rs")); }
76/// # let filesystem = support::rustdoc_provider::filesystem();
77/// use qubit_fs::Path;
78/// use qubit_fs::read::ReadOptions;
79///
80/// let path = Path::parse("/report")?;
81/// let bytes = filesystem.read_prefix(&path, ReadOptions::default(), 3)?;
82/// assert_eq!(b"rep", bytes.bytes());
83/// # Ok::<(), Box<dyn std::error::Error>>(())
84/// ```
85#[derive(Clone)]
86pub struct FileSystem {
87    /// Provider implementation receiving validated synchronous requests.
88    spi: Arc<dyn FileSystemSpi>,
89    /// Shared immutable state and deterministic preflight policy.
90    core: Arc<FacadeCore>,
91}
92
93impl FileSystem {
94    /// Constructs a facade and caches the provider's single validated property
95    /// snapshot.
96    #[inline]
97    pub fn from_spi<S>(spi: S) -> FsResult<Self>
98    where
99        S: FileSystemSpi + 'static,
100    {
101        Self::from_shared_spi(Arc::new(spi))
102    }
103
104    /// Constructs a facade from a shared provider implementation.
105    #[inline]
106    pub fn from_shared_spi(spi: Arc<dyn FileSystemSpi>) -> FsResult<Self> {
107        let core = FacadeCore::new(spi.properties())?;
108        Ok(Self {
109            spi,
110            core: Arc::new(core),
111        })
112    }
113
114    /// Returns the immutable property snapshot without provider I/O.
115    #[inline]
116    #[must_use]
117    pub fn properties(&self) -> &FileSystemProperties {
118        self.core.properties()
119    }
120
121    /// Validates a write request without opening a provider session.
122    pub fn validate_write(&self, path: &Path, options: &WriteOptions) -> FsResult<()> {
123        self.core.validate_write_request(path, options)
124    }
125
126    /// Assesses copy routes without performing provider I/O.
127    ///
128    /// # Errors
129    /// Returns an invalid-options or requirement error when no provider or
130    /// stream execution route can satisfy the request.
131    pub fn assess_copy(&self, source: &Path, target: &Path, options: &CopyOptions) -> FsResult<CopyAssessment> {
132        self.core.assess_copy(source, target, options)
133    }
134
135    /// Returns shared deterministic facade policy to operation objects.
136    #[inline]
137    pub(crate) fn core(&self) -> &FacadeCore {
138        &self.core
139    }
140
141    /// Returns the synchronous provider implementation to operation objects.
142    #[inline]
143    pub(crate) fn spi(&self) -> &dyn FileSystemSpi {
144        self.spi.as_ref()
145    }
146
147    /// Reads metadata after all local validation succeeds.
148    ///
149    /// # Errors
150    /// Returns the provider or validation error when `path` is invalid, the
151    /// provider cannot stat it, or the provider returns a mismatched path.
152    pub fn stat(&self, path: &Path) -> FsResult<FileMetadata> {
153        self.validate_path(path, FsOperation::Stat)?;
154        let response = self
155            .spi
156            .stat(StatRequest::new(path, ()))
157            .map_err(|error| self.enrich(error, path, FsOperation::Stat))?;
158        if response.path() != path {
159            return Err(self.contract_error(
160                path,
161                FsOperation::ValidateProviderOutcome,
162                "provider returned metadata for a different path",
163            ));
164        }
165        Ok(response.into_metadata())
166    }
167
168    /// Returns `false` only for an explicit not-found response.
169    ///
170    /// # Errors
171    /// Returns the original filesystem error for validation, provider, or
172    /// provider-contract failures other than `NotFound`.
173    pub fn exists(&self, path: &Path) -> FsResult<bool> {
174        match self.stat(path) {
175            Ok(_) => Ok(true),
176            Err(error) if error.kind() == FsErrorKind::NotFound => Ok(false),
177            Err(error) => Err(error.with_operation(FsOperation::Exists)),
178        }
179    }
180
181    /// Copies `source` to `target` through an optional provider fast path and a
182    /// safe stream fallback.
183    ///
184    /// # Errors
185    /// Returns [`CopyFailure`] with a recovery state. A provider failure is
186    /// never retried; only an explicit provider decline can select the
187    /// facade's allowlisted fallback.
188    #[allow(clippy::result_large_err)]
189    pub fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<CopyOutcome, CopyFailure> {
190        CopyOperation::new(self, source, target, options).execute()
191    }
192
193    /// Renames one resource through exactly one provider rename primitive.
194    ///
195    /// # Errors
196    /// Returns [`RenameFailure`] with the provider-confirmed transition state.
197    /// This method never implements rename through copy and delete.
198    pub fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<RenameOutcome, RenameFailure> {
199        if let Err(error) = self.rename_preflight(source, target, &options) {
200            return Err(self.contextualize_rename_failure(
201                RenameFailure::new(error, RenameFailureState::Unchanged),
202                source,
203                target,
204            ));
205        }
206        match self.spi.rename(RenameRequest::new(
207            source,
208            target,
209            ResolvedRenameOptions::new(options.clone()),
210        )) {
211            Ok(outcome) => match validate_rename_outcome(&outcome, &options, source, target) {
212                Some(violation) => Err(RenameFailure::new(
213                    self.contract_error(source, FsOperation::Rename, violation.message)
214                        .with_target(target.clone()),
215                    violation.state,
216                )),
217                None => Ok(outcome),
218            },
219            Err(failure) => {
220                let (error, state) = failure.into_parts();
221                Err(RenameFailure::new(
222                    error.with_operation(FsOperation::Rename).with_missing_context(
223                        source,
224                        Some(target),
225                        self.properties().info().provider_id(),
226                    ),
227                    state,
228                ))
229            }
230        }
231    }
232
233    /// Opens a provider directory stream after local option validation.
234    ///
235    /// # Errors
236    /// Returns an invalid-options, missing-capability, or provider error when
237    /// the scope or options cannot be served.
238    pub fn list(&self, scope: &ListScope, options: ListOptions) -> FsResult<DirectoryStream> {
239        DirectoryOperation::new(self).list(scope, options)
240    }
241
242    /// Opens a provider reader after local option validation.
243    ///
244    /// # Errors
245    /// Returns an invalid-path, unsupported-capability, provider, or
246    /// provider-contract error when the reader cannot be opened safely.
247    pub fn open_reader(&self, path: &Path, options: ReadOptions) -> FsResult<FileReader> {
248        self.open_reader_resolved(path, ResolvedReadOptions::new(options))
249    }
250
251    /// Opens a facade-validated reader with an internal resolved option hint.
252    pub(crate) fn open_reader_resolved(&self, path: &Path, resolved: ResolvedReadOptions) -> FsResult<FileReader> {
253        let options = resolved.options().clone();
254        self.validate_path(path, FsOperation::OpenReader)?;
255        options
256            .validate_against(self.properties().capabilities())
257            .map_err(|error| self.enrich(error, path, FsOperation::OpenReader))?;
258        self.properties()
259            .limits()
260            .validate_read_range(path, options.length())
261            .map_err(|error| self.enrich(error, path, FsOperation::OpenReader))?;
262        self.require(FileSystemCapability::Read, FsOperation::OpenReader, path)?;
263        self.spi
264            .open_reader(OpenReaderRequest::new(path, resolved))
265            .and_then(|opened| {
266                let (info, reader) = opened.into_parts();
267                self.validate_opened_info(&info, path)?;
268                Ok(FileReader::new(info, reader))
269            })
270            .map_err(|error| self.enrich(error, path, FsOperation::OpenReader))
271    }
272
273    /// Opens a provider writer after local option validation.
274    ///
275    /// # Errors
276    /// Returns [`OpenFailure`] identifying preflight, provider-open, or
277    /// provider-contract failure; a retained rejected writer is available when
278    /// provider cleanup is required.
279    pub fn open_writer(&self, path: &Path, options: WriteOptions) -> Result<FileWriter, OpenFailure<RejectedWriter>> {
280        self.core
281            .validate_write_request(path, &options)
282            .map_err(|error| OpenFailure::new(error, OpenFailureStage::Preflight, None))?;
283        let atomicity = options.atomicity();
284        let durability = options.durability();
285        let opened = self
286            .spi
287            .open_writer(OpenWriterRequest::new(path, ResolvedWriteOptions::new(options)))
288            .map_err(|error| {
289                OpenFailure::new(
290                    self.enrich(error, path, FsOperation::OpenWriter),
291                    OpenFailureStage::ProviderOpen,
292                    None,
293                )
294            })?;
295        let (info, session) = opened.into_parts();
296        if let Err(error) = self.validate_opened_info(&info, path) {
297            return Err(OpenFailure::new(
298                error,
299                OpenFailureStage::OutcomeValidation,
300                Some(RejectedWriter::new(
301                    session,
302                    self.properties().info().provider_id(),
303                    Some(path.clone()),
304                )),
305            ));
306        }
307        Ok(FileWriter::new(
308            info,
309            session,
310            atomicity,
311            durability,
312            self.properties().info().provider_id(),
313            self.properties().limits().max_write_bytes().maximum(),
314        ))
315    }
316
317    /// Creates a temporary file and binds its provider session to this facade.
318    ///
319    /// # Errors
320    /// Returns [`OpenFailure`] when preflight, provider creation, or temporary
321    /// identity validation fails; the recovery value may retain cleanup work.
322    pub fn create_temp_file(&self, options: TempOptions) -> Result<TempFile, OpenFailure<RejectedTempResource>> {
323        let parent = options.parent().cloned();
324        self.core
325            .validate_temp_parent(parent.as_ref())
326            .map_err(|error| OpenFailure::new(error, OpenFailureStage::Preflight, None))?;
327        self.core
328            .require(FileSystemCapability::TempFile, FsOperation::CreateTemp, parent.as_ref())
329            .map_err(|error| OpenFailure::new(error, OpenFailureStage::Preflight, None))?;
330        let opened = self
331            .spi
332            .create_temp_file(crate::spi::CreateTempFileRequest::new(options))
333            .map_err(|error| {
334                OpenFailure::new(
335                    self.core.enrich(error, parent.as_ref(), FsOperation::CreateTemp),
336                    OpenFailureStage::ProviderOpen,
337                    None,
338                )
339            })?;
340        let (info, session) = opened.into_parts();
341        if let Err(cause) = self.validate_temp_info(&info, crate::metadata::FileKind::File) {
342            let error = FsError::with_source(
343                FsErrorKind::ProviderContractViolation,
344                FsOperation::ValidateProviderOutcome,
345                "provider returned an invalid temporary identity",
346                cause,
347            )
348            .with_provider(self.properties().info().provider_id());
349            let error = match parent.as_ref() {
350                Some(path) => error.with_path(path.clone()),
351                None => error,
352            };
353            return Err(OpenFailure::new(
354                error,
355                OpenFailureStage::OutcomeValidation,
356                Some(RejectedTempResource::new(
357                    session,
358                    self.properties().info().provider_id(),
359                    parent,
360                )),
361            ));
362        }
363        Ok(TempFile::new(self.clone(), info.path().clone(), session))
364    }
365
366    /// Creates a temporary directory and binds its provider session to this
367    /// facade.
368    ///
369    /// # Errors
370    /// Returns [`OpenFailure`] when preflight, provider creation, or temporary
371    /// identity validation fails; the recovery value may retain cleanup work.
372    pub fn create_temp_directory(
373        &self,
374        options: TempOptions,
375    ) -> Result<TempDirectory, OpenFailure<RejectedTempResource>> {
376        let parent = options.parent().cloned();
377        self.core
378            .validate_temp_parent(parent.as_ref())
379            .map_err(|error| OpenFailure::new(error, OpenFailureStage::Preflight, None))?;
380        self.core
381            .require(
382                FileSystemCapability::TempDirectory,
383                FsOperation::CreateTemp,
384                parent.as_ref(),
385            )
386            .map_err(|error| OpenFailure::new(error, OpenFailureStage::Preflight, None))?;
387        let opened = self
388            .spi
389            .create_temp_directory(crate::spi::CreateTempDirectoryRequest::new(options))
390            .map_err(|error| {
391                OpenFailure::new(
392                    self.core.enrich(error, parent.as_ref(), FsOperation::CreateTemp),
393                    OpenFailureStage::ProviderOpen,
394                    None,
395                )
396            })?;
397        let (info, session) = opened.into_parts();
398        if let Err(cause) = self.validate_temp_info(&info, crate::metadata::FileKind::Directory) {
399            let error = FsError::with_source(
400                FsErrorKind::ProviderContractViolation,
401                FsOperation::ValidateProviderOutcome,
402                "provider returned an invalid temporary identity",
403                cause,
404            )
405            .with_provider(self.properties().info().provider_id());
406            let error = match parent.as_ref() {
407                Some(path) => error.with_path(path.clone()),
408                None => error,
409            };
410            return Err(OpenFailure::new(
411                error,
412                OpenFailureStage::OutcomeValidation,
413                Some(RejectedTempResource::new(
414                    session,
415                    self.properties().info().provider_id(),
416                    parent,
417                )),
418            ));
419        }
420        Ok(TempDirectory::new(self.clone(), info.path().clone(), session))
421    }
422
423    /// Creates a directory after local path validation.
424    ///
425    /// # Errors
426    /// Returns an invalid-path, missing-capability, provider, or provider-
427    /// contract error when the directory cannot be created as requested.
428    pub fn create_directory(&self, path: &Path, options: CreateDirectoryOptions) -> FsResult<CreateDirectoryOutcome> {
429        self.validate_path(path, FsOperation::CreateDir)?;
430        self.require(FileSystemCapability::CreateDirectory, FsOperation::CreateDir, path)?;
431        let exists_ok = options.exists_ok();
432        let outcome = self
433            .spi
434            .create_directory(CreateDirectoryRequest::new(
435                path,
436                ResolvedCreateDirectoryOptions::new(options),
437            ))
438            .map_err(|error| self.enrich(error, path, FsOperation::CreateDir))?;
439        if outcome.already_existed() && !exists_ok {
440            return Err(self.contract_error(
441                path,
442                FsOperation::CreateDir,
443                "provider accepted an existing directory without exists_ok",
444            ));
445        }
446        Ok(outcome)
447    }
448
449    /// Deletes a file after local option validation.
450    ///
451    /// # Errors
452    /// Returns an invalid-options, missing-capability, provider, or
453    /// provider-contract error when deletion cannot be confirmed.
454    #[inline]
455    pub fn delete_file(&self, path: &Path, options: DeleteOptions) -> FsResult<DeleteOutcome> {
456        self.delete(path, options, false)
457    }
458
459    /// Deletes a directory after local option validation.
460    ///
461    /// # Errors
462    /// Returns an invalid-options, missing-capability, provider, or
463    /// provider-contract error when deletion cannot be confirmed.
464    #[inline]
465    pub fn delete_directory(&self, path: &Path, options: DeleteOptions) -> FsResult<DeleteOutcome> {
466        self.delete(path, options, true)
467    }
468
469    /// Validates and dispatches one deletion primitive.
470    fn delete(&self, path: &Path, options: DeleteOptions, directory: bool) -> FsResult<DeleteOutcome> {
471        self.validate_path(path, FsOperation::Delete)?;
472        options
473            .validate_against(self.properties().capabilities())
474            .map_err(|error| self.enrich(error, path, FsOperation::Delete))?;
475        self.require(FileSystemCapability::Delete, FsOperation::Delete, path)?;
476        let missing_ok = options.missing_ok();
477        let outcome = if directory {
478            self.spi
479                .delete_directory(DeleteDirectoryRequest::new(path, ResolvedDeleteOptions::new(options)))
480        } else {
481            self.spi
482                .delete_file(DeleteFileRequest::new(path, ResolvedDeleteOptions::new(options)))
483        }
484        .map_err(|error| self.enrich(error, path, FsOperation::Delete))?;
485        if outcome.already_missing() && !missing_ok {
486            return Err(self.contract_error(
487                path,
488                FsOperation::Delete,
489                "provider accepted a missing target without missing_ok",
490            ));
491        }
492        Ok(outcome)
493    }
494
495    /// Performs validation before the single rename provider call.
496    fn rename_preflight(&self, source: &Path, target: &Path, options: &RenameOptions) -> FsResult<()> {
497        self.validate_path(source, FsOperation::Rename)?;
498        self.validate_path(target, FsOperation::Rename)?;
499        options
500            .validate_against(self.properties().capabilities())
501            .map_err(|error| {
502                self.enrich(error, source, FsOperation::Rename)
503                    .with_target(target.clone())
504            })?;
505        self.require(FileSystemCapability::Rename, FsOperation::Rename, source)?;
506        if source == target {
507            return Err(FsError::new(
508                FsErrorKind::InvalidOptions,
509                FsOperation::Rename,
510                "rename source and target must differ",
511            )
512            .with_path(source.clone())
513            .with_target(target.clone()));
514        }
515        Ok(())
516    }
517
518    /// Adds source, target, and provider facts required by public rename
519    /// failures.
520    fn contextualize_rename_failure(&self, failure: RenameFailure, source: &Path, target: &Path) -> RenameFailure {
521        let (error, state) = failure.into_parts();
522        RenameFailure::new(
523            error.with_missing_context(source, Some(target), self.properties().info().provider_id()),
524            state,
525        )
526    }
527
528    /// Validates a path against the cached provider snapshot before I/O.
529    fn validate_path(&self, path: &Path, operation: FsOperation) -> FsResult<()> {
530        self.core.validate_path(path, operation)
531    }
532
533    /// Requires an advertised capability before I/O.
534    fn require(&self, capability: FileSystemCapability, operation: FsOperation, path: &Path) -> FsResult<()> {
535        self.core.require(capability, operation, Some(path))
536    }
537
538    /// Adds missing public error context to a provider error.
539    fn enrich(&self, error: FsError, path: &Path, operation: FsOperation) -> FsError {
540        self.core.enrich(error, Some(path), operation)
541    }
542
543    /// Builds a provider-contract error for an invalid outcome.
544    fn contract_error(&self, path: &Path, operation: FsOperation, message: &str) -> FsError {
545        self.core.contract_error(path, operation, message)
546    }
547
548    /// Validates the identity the provider attached to an opened file handle.
549    fn validate_opened_info(&self, info: &crate::metadata::OpenedFileInfo, path: &Path) -> FsResult<()> {
550        if info.filesystem_id() != self.properties().info().id() || info.path() != path {
551            return Err(self.contract_error(
552                path,
553                FsOperation::ValidateProviderOutcome,
554                "provider returned an opened handle with a different identity",
555            ));
556        }
557        Ok(())
558    }
559
560    /// Validates the filesystem identity captured for a newly-created temporary
561    /// resource.
562    fn validate_temp_info(
563        &self,
564        info: &crate::metadata::OpenedFileInfo,
565        expected_kind: crate::metadata::FileKind,
566    ) -> FsResult<()> {
567        if info.filesystem_id() != self.properties().info().id() {
568            return Err(self.contract_error(
569                info.path(),
570                FsOperation::ValidateProviderOutcome,
571                "provider returned a temporary handle for a different filesystem",
572            ));
573        }
574        self.validate_path(info.path(), FsOperation::CreateTemp).map_err(|_| {
575            self.contract_error(
576                info.path(),
577                FsOperation::ValidateProviderOutcome,
578                "provider returned a temporary handle with an invalid logical path",
579            )
580        })?;
581        if info.metadata().is_none_or(|metadata| metadata.kind() != &expected_kind) {
582            return Err(self.contract_error(
583                info.path(),
584                FsOperation::ValidateProviderOutcome,
585                "provider returned a temporary handle with an inconsistent resource kind",
586            ));
587        }
588        Ok(())
589    }
590
591    /// Validates a temporary-resource persistence request before provider
592    /// effects.
593    pub(crate) fn preflight_temp_persist(
594        &self,
595        source: &Path,
596        target: &Path,
597        options: &crate::temp::PersistOptions,
598    ) -> FsResult<()> {
599        self.validate_path(source, FsOperation::PersistTemp)?;
600        self.validate_path(target, FsOperation::PersistTemp)?;
601        options
602            .validate_against(self.properties().capabilities())
603            .map_err(|error| {
604                self.enrich(error, source, FsOperation::PersistTemp)
605                    .with_target(target.clone())
606            })?;
607        Ok(())
608    }
609
610    /// Validates a provider-generated target reported by temporary keep.
611    pub(crate) fn validate_temp_keep_target(&self, source: &Path, target: &Path) -> FsResult<()> {
612        self.validate_path(target, FsOperation::KeepTemp).map_err(|_| {
613            self.contract_error(
614                source,
615                FsOperation::ValidateProviderOutcome,
616                "provider returned a temporary keep target with an invalid logical path",
617            )
618            .with_target(target.clone())
619        })
620    }
621
622    /// Reads one file into memory up to `max_bytes` after opening a reader.
623    ///
624    /// The byte cap applies to bytes actually read, even when opened metadata
625    /// overestimates the resource length.
626    ///
627    /// # Errors
628    /// Returns the reader or read error when validation, opening, or bounded
629    /// reading fails.
630    pub fn read_all(&self, path: &Path, options: ReadOptions, max_bytes: usize) -> FsResult<Vec<u8>> {
631        ReadOperation::new(self).read_all(path, options, max_bytes)
632    }
633
634    /// Reads at most max_bytes from a file without requiring a complete read.
635    ///
636    /// # Errors
637    /// Returns the reader or read error when validation, opening, or bounded
638    /// reading fails.
639    pub fn read_prefix(
640        &self,
641        path: &Path,
642        options: ReadOptions,
643        max_bytes: usize,
644    ) -> FsResult<crate::read::PrefixReadOutcome> {
645        ReadOperation::new(self).read_prefix(path, options, max_bytes)
646    }
647
648    /// Writes all bytes and retains the writer if transfer or commit fails.
649    ///
650    /// # Errors
651    /// Returns [`WriteAllFailure`] with the confirmed byte count and retained
652    /// writer when validation, transfer, or publication fails.
653    pub fn write_all(
654        &self,
655        path: &Path,
656        bytes: &[u8],
657        options: WriteOptions,
658    ) -> Result<crate::metadata::WriteOutcome, WriteAllFailure> {
659        WriteOperation::new(self).write_all(path, bytes, options)
660    }
661}