use async_trait::async_trait;
use once_cell::sync::OnceCell;
use std::ffi::OsString;
use std::fmt;
use std::io::{self, ErrorKind, Read, Seek, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::SystemTime;
#[cfg(not(target_arch = "wasm32"))]
mod native;
#[cfg(not(target_arch = "wasm32"))]
pub mod remote;
#[cfg(not(target_arch = "wasm32"))]
pub mod sandbox;
#[cfg(target_arch = "wasm32")]
mod wasm;
#[cfg(not(target_arch = "wasm32"))]
pub use native::NativeFsProvider;
#[cfg(not(target_arch = "wasm32"))]
pub use remote::{RemoteFsConfig, RemoteFsProvider};
#[cfg(not(target_arch = "wasm32"))]
pub use sandbox::SandboxFsProvider;
#[cfg(target_arch = "wasm32")]
pub use wasm::PlaceholderFsProvider;
pub mod data_contract;
use data_contract::{
DataChunkUploadRequest, DataChunkUploadTarget, DataManifestDescriptor, DataManifestRequest,
};
pub trait FileHandle: Read + Write + Seek + Send + Sync {}
impl<T> FileHandle for T where T: Read + Write + Seek + Send + Sync + 'static {}
#[derive(Clone, Debug, Default)]
pub struct OpenFlags {
pub read: bool,
pub write: bool,
pub append: bool,
pub truncate: bool,
pub create: bool,
pub create_new: bool,
}
#[derive(Clone, Debug)]
pub struct OpenOptions {
flags: OpenFlags,
}
impl OpenOptions {
pub fn new() -> Self {
Self {
flags: OpenFlags::default(),
}
}
pub fn read(&mut self, value: bool) -> &mut Self {
self.flags.read = value;
self
}
pub fn write(&mut self, value: bool) -> &mut Self {
self.flags.write = value;
self
}
pub fn append(&mut self, value: bool) -> &mut Self {
self.flags.append = value;
self
}
pub fn truncate(&mut self, value: bool) -> &mut Self {
self.flags.truncate = value;
self
}
pub fn create(&mut self, value: bool) -> &mut Self {
self.flags.create = value;
self
}
pub fn create_new(&mut self, value: bool) -> &mut Self {
self.flags.create_new = value;
self
}
pub fn open(&self, path: impl AsRef<Path>) -> io::Result<File> {
let resolved = resolve_path(path.as_ref());
with_provider(|provider| provider.open(&resolved, &self.flags)).map(File::from_handle)
}
pub fn flags(&self) -> &OpenFlags {
&self.flags
}
}
impl Default for OpenOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FsFileType {
Directory,
File,
Symlink,
Other,
Unknown,
}
#[derive(Clone, Debug)]
pub struct FsMetadata {
file_type: FsFileType,
len: u64,
modified: Option<SystemTime>,
readonly: bool,
hash: Option<String>,
}
impl FsMetadata {
pub fn new(
file_type: FsFileType,
len: u64,
modified: Option<SystemTime>,
readonly: bool,
) -> Self {
Self {
file_type,
len,
modified,
readonly,
hash: None,
}
}
pub fn new_with_hash(
file_type: FsFileType,
len: u64,
modified: Option<SystemTime>,
readonly: bool,
hash: Option<String>,
) -> Self {
Self {
file_type,
len,
modified,
readonly,
hash,
}
}
pub fn file_type(&self) -> FsFileType {
self.file_type
}
pub fn is_dir(&self) -> bool {
matches!(self.file_type, FsFileType::Directory)
}
pub fn is_file(&self) -> bool {
matches!(self.file_type, FsFileType::File)
}
pub fn is_symlink(&self) -> bool {
matches!(self.file_type, FsFileType::Symlink)
}
pub fn len(&self) -> u64 {
self.len
}
pub fn hash(&self) -> Option<&str> {
self.hash.as_deref()
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn modified(&self) -> Option<SystemTime> {
self.modified
}
pub fn is_readonly(&self) -> bool {
self.readonly
}
}
#[derive(Clone, Debug)]
pub struct DirEntry {
path: PathBuf,
file_name: OsString,
file_type: FsFileType,
}
#[derive(Clone, Debug)]
pub struct ReadManyEntry {
path: PathBuf,
bytes: Option<Vec<u8>>,
}
impl ReadManyEntry {
pub fn new(path: PathBuf, bytes: Option<Vec<u8>>) -> Self {
Self { path, bytes }
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn bytes(&self) -> Option<&[u8]> {
self.bytes.as_deref()
}
pub fn into_bytes(self) -> Option<Vec<u8>> {
self.bytes
}
}
impl DirEntry {
pub fn new(path: PathBuf, file_name: OsString, file_type: FsFileType) -> Self {
Self {
path,
file_name,
file_type,
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn file_name(&self) -> &OsString {
&self.file_name
}
pub fn file_type(&self) -> FsFileType {
self.file_type
}
pub fn is_dir(&self) -> bool {
matches!(self.file_type, FsFileType::Directory)
}
}
#[async_trait(?Send)]
pub trait FsProvider: Send + Sync + 'static {
fn open(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>>;
async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()>;
async fn remove_file(&self, path: &Path) -> io::Result<()>;
async fn metadata(&self, path: &Path) -> io::Result<FsMetadata>;
async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata>;
async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
async fn create_dir(&self, path: &Path) -> io::Result<()>;
async fn create_dir_all(&self, path: &Path) -> io::Result<()>;
async fn remove_dir(&self, path: &Path) -> io::Result<()>;
async fn remove_dir_all(&self, path: &Path) -> io::Result<()>;
async fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
async fn set_readonly(&self, path: &Path, readonly: bool) -> io::Result<()>;
async fn read_many(&self, paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
let mut entries = Vec::with_capacity(paths.len());
for path in paths {
let bytes = self.read(path).await.ok();
entries.push(ReadManyEntry::new(path.clone(), bytes));
}
Ok(entries)
}
async fn data_manifest_descriptor(
&self,
_request: &DataManifestRequest,
) -> io::Result<DataManifestDescriptor> {
Err(io::Error::new(
ErrorKind::Unsupported,
"data manifest descriptor is unsupported by this provider",
))
}
async fn data_chunk_upload_targets(
&self,
_request: &DataChunkUploadRequest,
) -> io::Result<Vec<DataChunkUploadTarget>> {
Err(io::Error::new(
ErrorKind::Unsupported,
"data chunk upload targets are unsupported by this provider",
))
}
async fn data_upload_chunk(
&self,
_target: &DataChunkUploadTarget,
_data: &[u8],
) -> io::Result<()> {
Err(io::Error::new(
ErrorKind::Unsupported,
"data chunk upload is unsupported by this provider",
))
}
}
pub struct File {
inner: Box<dyn FileHandle>,
}
impl File {
fn from_handle(handle: Box<dyn FileHandle>) -> Self {
Self { inner: handle }
}
pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
let mut opts = OpenOptions::new();
opts.read(true);
opts.open(path)
}
pub fn create(path: impl AsRef<Path>) -> io::Result<Self> {
let mut opts = OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.open(path)
}
}
impl fmt::Debug for File {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("File").finish_non_exhaustive()
}
}
impl Read for File {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl Write for File {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl Seek for File {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.inner.seek(pos)
}
}
static PROVIDER: OnceCell<RwLock<Arc<dyn FsProvider>>> = OnceCell::new();
#[cfg(target_arch = "wasm32")]
static CURRENT_DIR: OnceCell<RwLock<PathBuf>> = OnceCell::new();
fn provider_lock() -> &'static RwLock<Arc<dyn FsProvider>> {
PROVIDER.get_or_init(|| RwLock::new(default_provider()))
}
#[cfg(target_arch = "wasm32")]
fn current_dir_lock() -> &'static RwLock<PathBuf> {
CURRENT_DIR.get_or_init(|| RwLock::new(PathBuf::from("/")))
}
fn with_provider<T>(f: impl FnOnce(&dyn FsProvider) -> T) -> T {
let guard = provider_lock()
.read()
.expect("filesystem provider lock poisoned");
f(&**guard)
}
fn resolve_path(path: &Path) -> PathBuf {
#[cfg(target_arch = "wasm32")]
{
if path.is_absolute() {
return path.to_path_buf();
}
if let Ok(base) = current_dir() {
return base.join(path);
}
return PathBuf::from("/").join(path);
}
#[cfg(not(target_arch = "wasm32"))]
{
path.to_path_buf()
}
}
pub fn set_provider(provider: Arc<dyn FsProvider>) {
let mut guard = provider_lock()
.write()
.expect("filesystem provider lock poisoned");
*guard = provider;
}
pub fn replace_provider(provider: Arc<dyn FsProvider>) -> ProviderGuard {
let mut guard = provider_lock()
.write()
.expect("filesystem provider lock poisoned");
let previous = guard.clone();
*guard = provider;
ProviderGuard { previous }
}
pub fn with_provider_override<R>(provider: Arc<dyn FsProvider>, f: impl FnOnce() -> R) -> R {
let guard = replace_provider(provider);
let result = f();
drop(guard);
result
}
pub fn current_provider() -> Arc<dyn FsProvider> {
provider_lock()
.read()
.expect("filesystem provider lock poisoned")
.clone()
}
pub fn current_dir() -> io::Result<PathBuf> {
#[cfg(target_arch = "wasm32")]
{
return Ok(current_dir_lock()
.read()
.expect("filesystem current dir lock poisoned")
.clone());
}
#[cfg(not(target_arch = "wasm32"))]
{
std::env::current_dir()
}
}
pub fn set_current_dir(path: impl AsRef<Path>) -> io::Result<()> {
#[cfg(target_arch = "wasm32")]
{
let mut target = PathBuf::from(path.as_ref());
if !target.is_absolute() {
let base = current_dir()?;
target = base.join(target);
}
let canonical =
futures::executor::block_on(canonicalize_async(&target)).unwrap_or(target.clone());
let metadata = futures::executor::block_on(metadata_async(&canonical))?;
if !metadata.is_dir() {
return Err(io::Error::new(
ErrorKind::NotFound,
format!("Not a directory: {}", canonical.display()),
));
}
let mut guard = current_dir_lock()
.write()
.expect("filesystem current dir lock poisoned");
*guard = canonical;
return Ok(());
}
#[cfg(not(target_arch = "wasm32"))]
{
std::env::set_current_dir(path)
}
}
pub struct ProviderGuard {
previous: Arc<dyn FsProvider>,
}
impl Drop for ProviderGuard {
fn drop(&mut self) {
set_provider(self.previous.clone());
}
}
pub async fn read_many_async(paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
let resolved = paths
.iter()
.map(|path| resolve_path(path.as_path()))
.collect::<Vec<_>>();
let provider = current_provider();
provider.read_many(&resolved).await
}
pub async fn read_async(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.read(&resolved).await
}
pub async fn read_to_string_async(path: impl AsRef<Path>) -> io::Result<String> {
let bytes = read_async(path).await?;
String::from_utf8(bytes).map_err(|err| io::Error::new(ErrorKind::InvalidData, err.utf8_error()))
}
pub async fn write_async(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> io::Result<()> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.write(&resolved, data.as_ref()).await
}
pub async fn remove_file_async(path: impl AsRef<Path>) -> io::Result<()> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.remove_file(&resolved).await
}
pub async fn metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.metadata(&resolved).await
}
pub async fn symlink_metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.symlink_metadata(&resolved).await
}
pub async fn read_dir_async(path: impl AsRef<Path>) -> io::Result<Vec<DirEntry>> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.read_dir(&resolved).await
}
pub async fn canonicalize_async(path: impl AsRef<Path>) -> io::Result<PathBuf> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.canonicalize(&resolved).await
}
pub async fn create_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.create_dir(&resolved).await
}
pub async fn create_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.create_dir_all(&resolved).await
}
pub async fn remove_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.remove_dir(&resolved).await
}
pub async fn remove_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.remove_dir_all(&resolved).await
}
pub async fn rename_async(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
let resolved_from = resolve_path(from.as_ref());
let resolved_to = resolve_path(to.as_ref());
let provider = current_provider();
provider.rename(&resolved_from, &resolved_to).await
}
pub async fn set_readonly_async(path: impl AsRef<Path>, readonly: bool) -> io::Result<()> {
let resolved = resolve_path(path.as_ref());
let provider = current_provider();
provider.set_readonly(&resolved, readonly).await
}
pub async fn data_manifest_descriptor_async(
request: &DataManifestRequest,
) -> io::Result<DataManifestDescriptor> {
let provider = current_provider();
provider.data_manifest_descriptor(request).await
}
pub async fn data_chunk_upload_targets_async(
request: &DataChunkUploadRequest,
) -> io::Result<Vec<DataChunkUploadTarget>> {
let provider = current_provider();
provider.data_chunk_upload_targets(request).await
}
pub async fn data_upload_chunk_async(
target: &DataChunkUploadTarget,
data: &[u8],
) -> io::Result<()> {
let provider = current_provider();
provider.data_upload_chunk(target, data).await
}
pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> {
let mut reader = OpenOptions::new().read(true).open(from.as_ref())?;
let mut writer = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(to.as_ref())?;
io::copy(&mut reader, &mut writer)
}
fn default_provider() -> Arc<dyn FsProvider> {
#[cfg(not(target_arch = "wasm32"))]
{
Arc::new(NativeFsProvider)
}
#[cfg(target_arch = "wasm32")]
{
Arc::new(PlaceholderFsProvider)
}
}
#[cfg(test)]
mod tests {
use super::*;
use once_cell::sync::Lazy;
use std::io::{Read, Write};
use std::sync::Mutex;
use tempfile::tempdir;
static TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
struct UnsupportedProvider;
#[async_trait(?Send)]
impl FsProvider for UnsupportedProvider {
fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
Err(unsupported())
}
async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
Err(unsupported())
}
async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
Err(unsupported())
}
async fn remove_file(&self, _path: &Path) -> io::Result<()> {
Err(unsupported())
}
async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
Err(unsupported())
}
async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
Err(unsupported())
}
async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
Err(unsupported())
}
async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
Err(unsupported())
}
async fn create_dir(&self, _path: &Path) -> io::Result<()> {
Err(unsupported())
}
async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
Err(unsupported())
}
async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
Err(unsupported())
}
async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
Err(unsupported())
}
async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
Err(unsupported())
}
async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
Err(unsupported())
}
async fn data_manifest_descriptor(
&self,
_request: &DataManifestRequest,
) -> io::Result<DataManifestDescriptor> {
Err(unsupported())
}
async fn data_chunk_upload_targets(
&self,
_request: &DataChunkUploadRequest,
) -> io::Result<Vec<DataChunkUploadTarget>> {
Err(unsupported())
}
async fn data_upload_chunk(
&self,
_target: &DataChunkUploadTarget,
_data: &[u8],
) -> io::Result<()> {
Err(unsupported())
}
}
fn unsupported() -> io::Error {
io::Error::new(ErrorKind::Unsupported, "unsupported in test provider")
}
#[test]
fn copy_file_round_trip() {
let _guard = TEST_LOCK.lock().unwrap();
let dir = tempdir().expect("tempdir");
let src = dir.path().join("src.bin");
let dst = dir.path().join("dst.bin");
{
let mut file = std::fs::File::create(&src).expect("create src");
file.write_all(b"hello filesystem").expect("write src");
}
copy_file(&src, &dst).expect("copy");
let mut dst_file = File::open(&dst).expect("open dst");
let mut contents = Vec::new();
dst_file
.read_to_end(&mut contents)
.expect("read destination");
assert_eq!(contents, b"hello filesystem");
}
#[test]
fn set_readonly_flips_metadata_flag() {
let _guard = TEST_LOCK.lock().unwrap();
let dir = tempdir().expect("tempdir");
let path = dir.path().join("flag.txt");
futures::executor::block_on(write_async(&path, b"flag")).expect("write");
futures::executor::block_on(set_readonly_async(&path, true)).expect("set readonly");
let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
assert!(meta.is_readonly());
futures::executor::block_on(set_readonly_async(&path, false)).expect("unset readonly");
let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
assert!(!meta.is_readonly());
}
#[test]
fn replace_provider_restores_previous() {
let _guard = TEST_LOCK.lock().unwrap();
let original = current_provider();
let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
{
let _guard = replace_provider(custom.clone());
let active = current_provider();
assert!(Arc::ptr_eq(&active, &custom));
}
let final_provider = current_provider();
assert!(Arc::ptr_eq(&final_provider, &original));
}
#[test]
fn with_provider_restores_even_on_panic() {
let _guard = TEST_LOCK.lock().unwrap();
let original = current_provider();
let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
with_provider_override(custom.clone(), || {
let active = current_provider();
assert!(Arc::ptr_eq(&active, &custom));
panic!("boom");
})
}));
assert!(result.is_err());
let final_provider = current_provider();
assert!(Arc::ptr_eq(&final_provider, &original));
}
}