use std::sync::Arc;
use crate::copy::CopyAssessment;
use crate::copy::CopyFailure;
use crate::copy::CopyOperation;
use crate::copy::CopyOptions;
use crate::copy::CopyOutcome;
use crate::directory::CreateDirectoryOptions;
use crate::directory::CreateDirectoryOutcome;
use crate::directory::DeleteOptions;
use crate::directory::DeleteOutcome;
use crate::directory::DirectoryOperation;
use crate::directory::DirectoryStream;
use crate::directory::ListOptions;
use crate::directory::ListScope;
use crate::error::FsError;
use crate::error::FsErrorKind;
use crate::error::FsOperation;
use crate::error::FsResult;
use crate::error::OpenFailure;
use crate::error::OpenFailureStage;
use crate::facade::facade_core::FacadeCore;
use crate::metadata::FileMetadata;
use crate::metadata::FileSystemCapability;
use crate::metadata::FileSystemProperties;
use crate::path::Path;
use crate::read::FileReader;
use crate::read::ReadOperation;
use crate::read::ReadOptions;
use crate::rename::RenameFailure;
use crate::rename::RenameFailureState;
use crate::rename::RenameOptions;
use crate::rename::RenameOutcome;
use crate::rename::validate_rename_outcome;
use crate::spi::CreateDirectoryRequest;
use crate::spi::DeleteDirectoryRequest;
use crate::spi::DeleteFileRequest;
use crate::spi::FileSystemSpi;
use crate::spi::OpenReaderRequest;
use crate::spi::OpenWriterRequest;
use crate::spi::RenameRequest;
use crate::spi::ResolvedCreateDirectoryOptions;
use crate::spi::ResolvedDeleteOptions;
use crate::spi::ResolvedReadOptions;
use crate::spi::ResolvedRenameOptions;
use crate::spi::ResolvedWriteOptions;
use crate::spi::StatRequest;
use crate::temp::RejectedTempResource;
use crate::temp::TempDirectory;
use crate::temp::TempFile;
use crate::temp::TempOptions;
use crate::write::FileWriter;
use crate::write::RejectedWriter;
use crate::write::WriteAllFailure;
use crate::write::WriteOperation;
use crate::write::WriteOptions;
#[derive(Clone)]
pub struct FileSystem {
spi: Arc<dyn FileSystemSpi>,
core: Arc<FacadeCore>,
}
impl FileSystem {
#[inline]
pub fn from_spi<S>(spi: S) -> FsResult<Self>
where
S: FileSystemSpi + 'static,
{
Self::from_shared_spi(Arc::new(spi))
}
#[inline]
pub fn from_shared_spi(spi: Arc<dyn FileSystemSpi>) -> FsResult<Self> {
let core = FacadeCore::new(spi.properties())?;
Ok(Self {
spi,
core: Arc::new(core),
})
}
#[inline]
#[must_use]
pub fn properties(&self) -> &FileSystemProperties {
self.core.properties()
}
pub fn validate_write(&self, path: &Path, options: &WriteOptions) -> FsResult<()> {
self.core.validate_write_request(path, options)
}
pub fn assess_copy(&self, source: &Path, target: &Path, options: &CopyOptions) -> FsResult<CopyAssessment> {
self.core.assess_copy(source, target, options)
}
#[inline]
pub(crate) fn core(&self) -> &FacadeCore {
&self.core
}
#[inline]
pub(crate) fn spi(&self) -> &dyn FileSystemSpi {
self.spi.as_ref()
}
pub fn stat(&self, path: &Path) -> FsResult<FileMetadata> {
self.validate_path(path, FsOperation::Stat)?;
let response = self
.spi
.stat(StatRequest::new(path, ()))
.map_err(|error| self.enrich(error, path, FsOperation::Stat))?;
if response.path() != path {
return Err(self.contract_error(
path,
FsOperation::ValidateProviderOutcome,
"provider returned metadata for a different path",
));
}
Ok(response.into_metadata())
}
pub fn exists(&self, path: &Path) -> FsResult<bool> {
match self.stat(path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == FsErrorKind::NotFound => Ok(false),
Err(error) => Err(error.with_operation(FsOperation::Exists)),
}
}
#[allow(clippy::result_large_err)]
pub fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<CopyOutcome, CopyFailure> {
CopyOperation::new(self, source, target, options).execute()
}
pub fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<RenameOutcome, RenameFailure> {
if let Err(error) = self.rename_preflight(source, target, &options) {
return Err(self.contextualize_rename_failure(
RenameFailure::new(error, RenameFailureState::Unchanged),
source,
target,
));
}
match self.spi.rename(RenameRequest::new(
source,
target,
ResolvedRenameOptions::new(options.clone()),
)) {
Ok(outcome) => match validate_rename_outcome(&outcome, &options, source, target) {
Some(violation) => Err(RenameFailure::new(
self.contract_error(source, FsOperation::Rename, violation.message)
.with_target(target.clone()),
violation.state,
)),
None => Ok(outcome),
},
Err(failure) => {
let (error, state) = failure.into_parts();
Err(RenameFailure::new(
error.with_operation(FsOperation::Rename).with_missing_context(
source,
Some(target),
self.properties().info().provider_id(),
),
state,
))
}
}
}
pub fn list(&self, scope: &ListScope, options: ListOptions) -> FsResult<DirectoryStream> {
DirectoryOperation::new(self).list(scope, options)
}
pub fn open_reader(&self, path: &Path, options: ReadOptions) -> FsResult<FileReader> {
self.open_reader_resolved(path, ResolvedReadOptions::new(options))
}
pub(crate) fn open_reader_resolved(&self, path: &Path, resolved: ResolvedReadOptions) -> FsResult<FileReader> {
let options = resolved.options().clone();
self.validate_path(path, FsOperation::OpenReader)?;
options
.validate_against(self.properties().capabilities())
.map_err(|error| self.enrich(error, path, FsOperation::OpenReader))?;
self.properties()
.limits()
.validate_read_range(path, options.length())
.map_err(|error| self.enrich(error, path, FsOperation::OpenReader))?;
self.require(FileSystemCapability::Read, FsOperation::OpenReader, path)?;
self.spi
.open_reader(OpenReaderRequest::new(path, resolved))
.and_then(|opened| {
let (info, reader) = opened.into_parts();
self.validate_opened_info(&info, path)?;
Ok(FileReader::new(info, reader))
})
.map_err(|error| self.enrich(error, path, FsOperation::OpenReader))
}
pub fn open_writer(&self, path: &Path, options: WriteOptions) -> Result<FileWriter, OpenFailure<RejectedWriter>> {
self.core
.validate_write_request(path, &options)
.map_err(|error| OpenFailure::new(error, OpenFailureStage::Preflight, None))?;
let atomicity = options.atomicity();
let durability = options.durability();
let opened = self
.spi
.open_writer(OpenWriterRequest::new(path, ResolvedWriteOptions::new(options)))
.map_err(|error| {
OpenFailure::new(
self.enrich(error, path, FsOperation::OpenWriter),
OpenFailureStage::ProviderOpen,
None,
)
})?;
let (info, session) = opened.into_parts();
if let Err(error) = self.validate_opened_info(&info, path) {
return Err(OpenFailure::new(
error,
OpenFailureStage::OutcomeValidation,
Some(RejectedWriter::new(
session,
self.properties().info().provider_id(),
Some(path.clone()),
)),
));
}
Ok(FileWriter::new(
info,
session,
atomicity,
durability,
self.properties().info().provider_id(),
self.properties().limits().max_write_bytes().maximum(),
))
}
pub fn create_temp_file(&self, options: TempOptions) -> Result<TempFile, OpenFailure<RejectedTempResource>> {
let parent = options.parent().cloned();
self.core
.validate_temp_parent(parent.as_ref())
.map_err(|error| OpenFailure::new(error, OpenFailureStage::Preflight, None))?;
self.core
.require(FileSystemCapability::TempFile, FsOperation::CreateTemp, parent.as_ref())
.map_err(|error| OpenFailure::new(error, OpenFailureStage::Preflight, None))?;
let opened = self
.spi
.create_temp_file(crate::spi::CreateTempFileRequest::new(options))
.map_err(|error| {
OpenFailure::new(
self.core.enrich(error, parent.as_ref(), FsOperation::CreateTemp),
OpenFailureStage::ProviderOpen,
None,
)
})?;
let (info, session) = opened.into_parts();
if let Err(cause) = self.validate_temp_info(&info, crate::metadata::FileKind::File) {
let error = FsError::with_source(
FsErrorKind::ProviderContractViolation,
FsOperation::ValidateProviderOutcome,
"provider returned an invalid temporary identity",
cause,
)
.with_provider(self.properties().info().provider_id());
let error = match parent.as_ref() {
Some(path) => error.with_path(path.clone()),
None => error,
};
return Err(OpenFailure::new(
error,
OpenFailureStage::OutcomeValidation,
Some(RejectedTempResource::new(
session,
self.properties().info().provider_id(),
parent,
)),
));
}
Ok(TempFile::new(self.clone(), info.path().clone(), session))
}
pub fn create_temp_directory(
&self,
options: TempOptions,
) -> Result<TempDirectory, OpenFailure<RejectedTempResource>> {
let parent = options.parent().cloned();
self.core
.validate_temp_parent(parent.as_ref())
.map_err(|error| OpenFailure::new(error, OpenFailureStage::Preflight, None))?;
self.core
.require(
FileSystemCapability::TempDirectory,
FsOperation::CreateTemp,
parent.as_ref(),
)
.map_err(|error| OpenFailure::new(error, OpenFailureStage::Preflight, None))?;
let opened = self
.spi
.create_temp_directory(crate::spi::CreateTempDirectoryRequest::new(options))
.map_err(|error| {
OpenFailure::new(
self.core.enrich(error, parent.as_ref(), FsOperation::CreateTemp),
OpenFailureStage::ProviderOpen,
None,
)
})?;
let (info, session) = opened.into_parts();
if let Err(cause) = self.validate_temp_info(&info, crate::metadata::FileKind::Directory) {
let error = FsError::with_source(
FsErrorKind::ProviderContractViolation,
FsOperation::ValidateProviderOutcome,
"provider returned an invalid temporary identity",
cause,
)
.with_provider(self.properties().info().provider_id());
let error = match parent.as_ref() {
Some(path) => error.with_path(path.clone()),
None => error,
};
return Err(OpenFailure::new(
error,
OpenFailureStage::OutcomeValidation,
Some(RejectedTempResource::new(
session,
self.properties().info().provider_id(),
parent,
)),
));
}
Ok(TempDirectory::new(self.clone(), info.path().clone(), session))
}
pub fn create_directory(&self, path: &Path, options: CreateDirectoryOptions) -> FsResult<CreateDirectoryOutcome> {
self.validate_path(path, FsOperation::CreateDir)?;
self.require(FileSystemCapability::CreateDirectory, FsOperation::CreateDir, path)?;
let exists_ok = options.exists_ok();
let outcome = self
.spi
.create_directory(CreateDirectoryRequest::new(
path,
ResolvedCreateDirectoryOptions::new(options),
))
.map_err(|error| self.enrich(error, path, FsOperation::CreateDir))?;
if outcome.already_existed() && !exists_ok {
return Err(self.contract_error(
path,
FsOperation::CreateDir,
"provider accepted an existing directory without exists_ok",
));
}
Ok(outcome)
}
#[inline]
pub fn delete_file(&self, path: &Path, options: DeleteOptions) -> FsResult<DeleteOutcome> {
self.delete(path, options, false)
}
#[inline]
pub fn delete_directory(&self, path: &Path, options: DeleteOptions) -> FsResult<DeleteOutcome> {
self.delete(path, options, true)
}
fn delete(&self, path: &Path, options: DeleteOptions, directory: bool) -> FsResult<DeleteOutcome> {
self.validate_path(path, FsOperation::Delete)?;
options
.validate_against(self.properties().capabilities())
.map_err(|error| self.enrich(error, path, FsOperation::Delete))?;
self.require(FileSystemCapability::Delete, FsOperation::Delete, path)?;
let missing_ok = options.missing_ok();
let outcome = if directory {
self.spi
.delete_directory(DeleteDirectoryRequest::new(path, ResolvedDeleteOptions::new(options)))
} else {
self.spi
.delete_file(DeleteFileRequest::new(path, ResolvedDeleteOptions::new(options)))
}
.map_err(|error| self.enrich(error, path, FsOperation::Delete))?;
if outcome.already_missing() && !missing_ok {
return Err(self.contract_error(
path,
FsOperation::Delete,
"provider accepted a missing target without missing_ok",
));
}
Ok(outcome)
}
fn rename_preflight(&self, source: &Path, target: &Path, options: &RenameOptions) -> FsResult<()> {
self.validate_path(source, FsOperation::Rename)?;
self.validate_path(target, FsOperation::Rename)?;
options
.validate_against(self.properties().capabilities())
.map_err(|error| {
self.enrich(error, source, FsOperation::Rename)
.with_target(target.clone())
})?;
self.require(FileSystemCapability::Rename, FsOperation::Rename, source)?;
if source == target {
return Err(FsError::new(
FsErrorKind::InvalidOptions,
FsOperation::Rename,
"rename source and target must differ",
)
.with_path(source.clone())
.with_target(target.clone()));
}
Ok(())
}
fn contextualize_rename_failure(&self, failure: RenameFailure, source: &Path, target: &Path) -> RenameFailure {
let (error, state) = failure.into_parts();
RenameFailure::new(
error.with_missing_context(source, Some(target), self.properties().info().provider_id()),
state,
)
}
fn validate_path(&self, path: &Path, operation: FsOperation) -> FsResult<()> {
self.core.validate_path(path, operation)
}
fn require(&self, capability: FileSystemCapability, operation: FsOperation, path: &Path) -> FsResult<()> {
self.core.require(capability, operation, Some(path))
}
fn enrich(&self, error: FsError, path: &Path, operation: FsOperation) -> FsError {
self.core.enrich(error, Some(path), operation)
}
fn contract_error(&self, path: &Path, operation: FsOperation, message: &str) -> FsError {
self.core.contract_error(path, operation, message)
}
fn validate_opened_info(&self, info: &crate::metadata::OpenedFileInfo, path: &Path) -> FsResult<()> {
if info.filesystem_id() != self.properties().info().id() || info.path() != path {
return Err(self.contract_error(
path,
FsOperation::ValidateProviderOutcome,
"provider returned an opened handle with a different identity",
));
}
Ok(())
}
fn validate_temp_info(
&self,
info: &crate::metadata::OpenedFileInfo,
expected_kind: crate::metadata::FileKind,
) -> FsResult<()> {
if info.filesystem_id() != self.properties().info().id() {
return Err(self.contract_error(
info.path(),
FsOperation::ValidateProviderOutcome,
"provider returned a temporary handle for a different filesystem",
));
}
self.validate_path(info.path(), FsOperation::CreateTemp).map_err(|_| {
self.contract_error(
info.path(),
FsOperation::ValidateProviderOutcome,
"provider returned a temporary handle with an invalid logical path",
)
})?;
if info.metadata().is_none_or(|metadata| metadata.kind() != &expected_kind) {
return Err(self.contract_error(
info.path(),
FsOperation::ValidateProviderOutcome,
"provider returned a temporary handle with an inconsistent resource kind",
));
}
Ok(())
}
pub(crate) fn preflight_temp_persist(
&self,
source: &Path,
target: &Path,
options: &crate::temp::PersistOptions,
) -> FsResult<()> {
self.validate_path(source, FsOperation::PersistTemp)?;
self.validate_path(target, FsOperation::PersistTemp)?;
options
.validate_against(self.properties().capabilities())
.map_err(|error| {
self.enrich(error, source, FsOperation::PersistTemp)
.with_target(target.clone())
})?;
Ok(())
}
pub(crate) fn validate_temp_keep_target(&self, source: &Path, target: &Path) -> FsResult<()> {
self.validate_path(target, FsOperation::KeepTemp).map_err(|_| {
self.contract_error(
source,
FsOperation::ValidateProviderOutcome,
"provider returned a temporary keep target with an invalid logical path",
)
.with_target(target.clone())
})
}
pub fn read_all(&self, path: &Path, options: ReadOptions, max_bytes: usize) -> FsResult<Vec<u8>> {
ReadOperation::new(self).read_all(path, options, max_bytes)
}
pub fn read_prefix(
&self,
path: &Path,
options: ReadOptions,
max_bytes: usize,
) -> FsResult<crate::read::PrefixReadOutcome> {
ReadOperation::new(self).read_prefix(path, options, max_bytes)
}
pub fn write_all(
&self,
path: &Path,
bytes: &[u8],
options: WriteOptions,
) -> Result<crate::metadata::WriteOutcome, WriteAllFailure> {
WriteOperation::new(self).write_all(path, bytes, options)
}
}