1use async_trait::async_trait;
2use log::warn;
3use once_cell::sync::OnceCell;
4use std::ffi::OsString;
5use std::fmt;
6use std::io::{self, ErrorKind, Read, Seek, Write};
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, Mutex, MutexGuard, RwLock};
9use std::time::SystemTime;
10
11#[cfg(not(target_arch = "wasm32"))]
12mod native;
13#[cfg(not(target_arch = "wasm32"))]
14pub mod remote;
15#[cfg(not(target_arch = "wasm32"))]
16pub mod sandbox;
17#[cfg(target_arch = "wasm32")]
18mod wasm;
19
20#[cfg(not(target_arch = "wasm32"))]
21pub use native::NativeFsProvider;
22#[cfg(not(target_arch = "wasm32"))]
23pub use remote::{RemoteFsConfig, RemoteFsProvider};
24#[cfg(not(target_arch = "wasm32"))]
25pub use sandbox::SandboxFsProvider;
26#[cfg(target_arch = "wasm32")]
27pub use wasm::PlaceholderFsProvider;
28
29pub mod data_contract;
30
31use data_contract::{
32 DataChunkUploadRequest, DataChunkUploadTarget, DataManifestDescriptor, DataManifestRequest,
33};
34
35#[async_trait(?Send)]
36pub trait FileHandle: Read + Write + Seek + Send + Sync {
37 async fn flush_async(&mut self) -> io::Result<()> {
38 self.flush()
39 }
40
41 async fn sync_all_async(&mut self) -> io::Result<()> {
42 self.flush_async().await
43 }
44}
45
46#[async_trait(?Send)]
47impl FileHandle for std::fs::File {
48 async fn sync_all_async(&mut self) -> io::Result<()> {
49 std::fs::File::sync_all(self)
50 }
51}
52
53#[derive(Clone, Debug, Default)]
54pub struct OpenFlags {
55 pub read: bool,
56 pub write: bool,
57 pub append: bool,
58 pub truncate: bool,
59 pub create: bool,
60 pub create_new: bool,
61}
62
63#[derive(Clone, Debug)]
64pub struct OpenOptions {
65 flags: OpenFlags,
66}
67
68impl OpenOptions {
69 pub fn new() -> Self {
70 Self {
71 flags: OpenFlags::default(),
72 }
73 }
74
75 pub fn read(&mut self, value: bool) -> &mut Self {
76 self.flags.read = value;
77 self
78 }
79
80 pub fn write(&mut self, value: bool) -> &mut Self {
81 self.flags.write = value;
82 self
83 }
84
85 pub fn append(&mut self, value: bool) -> &mut Self {
86 self.flags.append = value;
87 self
88 }
89
90 pub fn truncate(&mut self, value: bool) -> &mut Self {
91 self.flags.truncate = value;
92 self
93 }
94
95 pub fn create(&mut self, value: bool) -> &mut Self {
96 self.flags.create = value;
97 self
98 }
99
100 pub fn create_new(&mut self, value: bool) -> &mut Self {
101 self.flags.create_new = value;
102 self
103 }
104
105 pub fn open(&self, path: impl AsRef<Path>) -> io::Result<File> {
106 let resolved = resolve_path(path.as_ref());
107 with_provider(|provider| provider.open(&resolved, &self.flags)).map(File::from_handle)
108 }
109
110 pub async fn open_async(&self, path: impl AsRef<Path>) -> io::Result<File> {
111 let resolved = resolve_path(path.as_ref());
112 let provider = current_provider();
113 provider
114 .open_async(&resolved, &self.flags)
115 .await
116 .map(File::from_handle)
117 }
118
119 pub fn flags(&self) -> &OpenFlags {
120 &self.flags
121 }
122}
123
124impl Default for OpenOptions {
125 fn default() -> Self {
126 Self::new()
127 }
128}
129
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum FsFileType {
132 Directory,
133 File,
134 Symlink,
135 Other,
136 Unknown,
137}
138
139#[derive(Clone, Debug)]
140pub struct FsMetadata {
141 file_type: FsFileType,
142 len: u64,
143 modified: Option<SystemTime>,
144 readonly: bool,
145 hash: Option<String>,
146}
147
148impl FsMetadata {
149 pub fn new(
150 file_type: FsFileType,
151 len: u64,
152 modified: Option<SystemTime>,
153 readonly: bool,
154 ) -> Self {
155 Self {
156 file_type,
157 len,
158 modified,
159 readonly,
160 hash: None,
161 }
162 }
163
164 pub fn new_with_hash(
165 file_type: FsFileType,
166 len: u64,
167 modified: Option<SystemTime>,
168 readonly: bool,
169 hash: Option<String>,
170 ) -> Self {
171 Self {
172 file_type,
173 len,
174 modified,
175 readonly,
176 hash,
177 }
178 }
179
180 pub fn file_type(&self) -> FsFileType {
181 self.file_type
182 }
183
184 pub fn is_dir(&self) -> bool {
185 matches!(self.file_type, FsFileType::Directory)
186 }
187
188 pub fn is_file(&self) -> bool {
189 matches!(self.file_type, FsFileType::File)
190 }
191
192 pub fn is_symlink(&self) -> bool {
193 matches!(self.file_type, FsFileType::Symlink)
194 }
195
196 pub fn len(&self) -> u64 {
197 self.len
198 }
199
200 pub fn hash(&self) -> Option<&str> {
201 self.hash.as_deref()
202 }
203
204 pub fn is_empty(&self) -> bool {
205 self.len == 0
206 }
207
208 pub fn modified(&self) -> Option<SystemTime> {
209 self.modified
210 }
211
212 pub fn is_readonly(&self) -> bool {
213 self.readonly
214 }
215}
216
217#[derive(Clone, Debug)]
218pub struct DirEntry {
219 path: PathBuf,
220 file_name: OsString,
221 file_type: FsFileType,
222}
223
224#[derive(Clone, Debug)]
225pub struct ReadManyEntry {
226 path: PathBuf,
227 bytes: Option<Vec<u8>>,
228 error: Option<String>,
229}
230
231impl ReadManyEntry {
232 pub fn new(path: PathBuf, bytes: Option<Vec<u8>>) -> Self {
233 Self {
234 path,
235 bytes,
236 error: None,
237 }
238 }
239
240 pub fn with_error(path: PathBuf, error: String) -> Self {
241 Self {
242 path,
243 bytes: None,
244 error: Some(error),
245 }
246 }
247
248 pub fn path(&self) -> &Path {
249 &self.path
250 }
251
252 pub fn bytes(&self) -> Option<&[u8]> {
253 self.bytes.as_deref()
254 }
255
256 pub fn into_bytes(self) -> Option<Vec<u8>> {
257 self.bytes
258 }
259
260 pub fn error(&self) -> Option<&str> {
261 self.error.as_deref()
262 }
263}
264
265impl DirEntry {
266 pub fn new(path: PathBuf, file_name: OsString, file_type: FsFileType) -> Self {
267 Self {
268 path,
269 file_name,
270 file_type,
271 }
272 }
273
274 pub fn path(&self) -> &Path {
275 &self.path
276 }
277
278 pub fn file_name(&self) -> &OsString {
279 &self.file_name
280 }
281
282 pub fn file_type(&self) -> FsFileType {
283 self.file_type
284 }
285
286 pub fn is_dir(&self) -> bool {
287 matches!(self.file_type, FsFileType::Directory)
288 }
289}
290
291#[async_trait(?Send)]
292pub trait FsProvider: Send + Sync + 'static {
293 fn open(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>>;
294 async fn open_async(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
295 self.open(path, flags)
296 }
297 async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
298 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()>;
299 async fn remove_file(&self, path: &Path) -> io::Result<()>;
300 async fn metadata(&self, path: &Path) -> io::Result<FsMetadata>;
301 async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata>;
302 async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
303 async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
304 async fn create_dir(&self, path: &Path) -> io::Result<()>;
305 async fn create_dir_all(&self, path: &Path) -> io::Result<()>;
306 async fn remove_dir(&self, path: &Path) -> io::Result<()>;
307 async fn remove_dir_all(&self, path: &Path) -> io::Result<()>;
308 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
309 async fn set_readonly(&self, path: &Path, readonly: bool) -> io::Result<()>;
310
311 async fn read_many(&self, paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
312 let mut entries = Vec::with_capacity(paths.len());
313 for path in paths {
314 let entry = match self.read(path).await {
315 Ok(payload) => ReadManyEntry::new(path.clone(), Some(payload)),
316 Err(error) => {
317 warn!(
318 "fs.read_many.miss path={} kind={:?} error={}",
319 path.to_string_lossy(),
320 error.kind(),
321 error
322 );
323 ReadManyEntry::with_error(
324 path.clone(),
325 format!("kind={:?}; error={}", error.kind(), error),
326 )
327 }
328 };
329 entries.push(entry);
330 }
331 Ok(entries)
332 }
333
334 async fn data_manifest_descriptor(
335 &self,
336 _request: &DataManifestRequest,
337 ) -> io::Result<DataManifestDescriptor> {
338 Err(io::Error::new(
339 ErrorKind::Unsupported,
340 "data manifest descriptor is unsupported by this provider",
341 ))
342 }
343
344 async fn data_chunk_upload_targets(
345 &self,
346 _request: &DataChunkUploadRequest,
347 ) -> io::Result<Vec<DataChunkUploadTarget>> {
348 Err(io::Error::new(
349 ErrorKind::Unsupported,
350 "data chunk upload targets are unsupported by this provider",
351 ))
352 }
353
354 async fn data_upload_chunk(
355 &self,
356 _target: &DataChunkUploadTarget,
357 _data: &[u8],
358 ) -> io::Result<()> {
359 Err(io::Error::new(
360 ErrorKind::Unsupported,
361 "data chunk upload is unsupported by this provider",
362 ))
363 }
364}
365
366pub struct File {
367 inner: Box<dyn FileHandle>,
368}
369
370impl File {
371 fn from_handle(handle: Box<dyn FileHandle>) -> Self {
372 Self { inner: handle }
373 }
374
375 pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
376 let mut opts = OpenOptions::new();
377 opts.read(true);
378 opts.open(path)
379 }
380
381 pub async fn open_async(path: impl AsRef<Path>) -> io::Result<Self> {
382 let mut opts = OpenOptions::new();
383 opts.read(true);
384 opts.open_async(path).await
385 }
386
387 pub fn create(path: impl AsRef<Path>) -> io::Result<Self> {
388 let mut opts = OpenOptions::new();
389 opts.write(true).create(true).truncate(true);
390 opts.open(path)
391 }
392
393 pub async fn create_async(path: impl AsRef<Path>) -> io::Result<Self> {
394 let mut opts = OpenOptions::new();
395 opts.write(true).create(true).truncate(true);
396 opts.open_async(path).await
397 }
398
399 pub async fn flush_async(&mut self) -> io::Result<()> {
400 self.inner.flush_async().await
401 }
402
403 pub async fn sync_all_async(&mut self) -> io::Result<()> {
404 self.inner.sync_all_async().await
405 }
406}
407
408impl fmt::Debug for File {
409 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410 f.debug_struct("File").finish_non_exhaustive()
411 }
412}
413
414impl Read for File {
415 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
416 self.inner.read(buf)
417 }
418}
419
420impl Write for File {
421 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
422 self.inner.write(buf)
423 }
424
425 fn flush(&mut self) -> io::Result<()> {
426 self.inner.flush()
427 }
428}
429
430impl Seek for File {
431 fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
432 self.inner.seek(pos)
433 }
434}
435
436static PROVIDER: OnceCell<RwLock<Arc<dyn FsProvider>>> = OnceCell::new();
437static PROVIDER_OVERRIDE_LOCK: OnceCell<Mutex<()>> = OnceCell::new();
438#[cfg(target_arch = "wasm32")]
439static CURRENT_DIR: OnceCell<RwLock<PathBuf>> = OnceCell::new();
440
441fn provider_lock() -> &'static RwLock<Arc<dyn FsProvider>> {
442 PROVIDER.get_or_init(|| RwLock::new(default_provider()))
443}
444
445pub fn provider_override_lock() -> MutexGuard<'static, ()> {
448 PROVIDER_OVERRIDE_LOCK
449 .get_or_init(|| Mutex::new(()))
450 .lock()
451 .unwrap_or_else(|poisoned| poisoned.into_inner())
452}
453
454#[cfg(target_arch = "wasm32")]
455fn current_dir_lock() -> &'static RwLock<PathBuf> {
456 CURRENT_DIR.get_or_init(|| RwLock::new(PathBuf::from("/")))
457}
458
459fn with_provider<T>(f: impl FnOnce(&dyn FsProvider) -> T) -> T {
460 let guard = provider_lock()
461 .read()
462 .expect("filesystem provider lock poisoned");
463 f(&**guard)
464}
465
466fn resolve_path(path: &Path) -> PathBuf {
467 #[cfg(target_arch = "wasm32")]
468 {
469 if path.is_absolute() {
470 return path.to_path_buf();
471 }
472 if let Ok(base) = current_dir() {
473 return base.join(path);
474 }
475 PathBuf::from("/").join(path)
476 }
477 #[cfg(not(target_arch = "wasm32"))]
478 {
479 path.to_path_buf()
480 }
481}
482
483pub fn set_provider(provider: Arc<dyn FsProvider>) {
484 let mut guard = provider_lock()
485 .write()
486 .expect("filesystem provider lock poisoned");
487 *guard = provider;
488}
489
490pub fn replace_provider(provider: Arc<dyn FsProvider>) -> ProviderGuard {
494 let mut guard = provider_lock()
495 .write()
496 .expect("filesystem provider lock poisoned");
497 let previous = guard.clone();
498 *guard = provider;
499 ProviderGuard { previous }
500}
501
502pub fn with_provider_override<R>(provider: Arc<dyn FsProvider>, f: impl FnOnce() -> R) -> R {
505 let guard = replace_provider(provider);
506 let result = f();
507 drop(guard);
508 result
509}
510
511pub fn current_provider() -> Arc<dyn FsProvider> {
513 provider_lock()
514 .read()
515 .expect("filesystem provider lock poisoned")
516 .clone()
517}
518
519pub fn current_dir() -> io::Result<PathBuf> {
520 #[cfg(target_arch = "wasm32")]
521 {
522 return Ok(current_dir_lock()
523 .read()
524 .expect("filesystem current dir lock poisoned")
525 .clone());
526 }
527 #[cfg(not(target_arch = "wasm32"))]
528 {
529 std::env::current_dir()
530 }
531}
532
533pub fn set_current_dir(path: impl AsRef<Path>) -> io::Result<()> {
534 #[cfg(target_arch = "wasm32")]
535 {
536 let mut target = PathBuf::from(path.as_ref());
537 if !target.is_absolute() {
538 let base = current_dir()?;
539 target = base.join(target);
540 }
541 let canonical =
542 futures::executor::block_on(canonicalize_async(&target)).unwrap_or(target.clone());
543 let metadata = futures::executor::block_on(metadata_async(&canonical))?;
544 if !metadata.is_dir() {
545 return Err(io::Error::new(
546 ErrorKind::NotFound,
547 format!("Not a directory: {}", canonical.display()),
548 ));
549 }
550 let mut guard = current_dir_lock()
551 .write()
552 .expect("filesystem current dir lock poisoned");
553 *guard = canonical;
554 Ok(())
555 }
556 #[cfg(not(target_arch = "wasm32"))]
557 {
558 std::env::set_current_dir(path)
559 }
560}
561
562pub struct ProviderGuard {
563 previous: Arc<dyn FsProvider>,
564}
565
566impl Drop for ProviderGuard {
567 fn drop(&mut self) {
568 set_provider(self.previous.clone());
569 }
570}
571
572pub async fn read_many_async(paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
573 let resolved = paths
574 .iter()
575 .map(|path| resolve_path(path.as_path()))
576 .collect::<Vec<_>>();
577 let provider = current_provider();
578 provider.read_many(&resolved).await
579}
580
581pub async fn read_async(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
582 let resolved = resolve_path(path.as_ref());
583 let provider = current_provider();
584 provider.read(&resolved).await
585}
586
587pub async fn read_to_string_async(path: impl AsRef<Path>) -> io::Result<String> {
588 let bytes = read_async(path).await?;
589 String::from_utf8(bytes).map_err(|err| io::Error::new(ErrorKind::InvalidData, err.utf8_error()))
590}
591
592pub async fn write_async(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> io::Result<()> {
593 let resolved = resolve_path(path.as_ref());
594 let provider = current_provider();
595 provider.write(&resolved, data.as_ref()).await
596}
597
598pub async fn remove_file_async(path: impl AsRef<Path>) -> io::Result<()> {
599 let resolved = resolve_path(path.as_ref());
600 let provider = current_provider();
601 provider.remove_file(&resolved).await
602}
603
604pub async fn metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
605 let resolved = resolve_path(path.as_ref());
606 let provider = current_provider();
607 provider.metadata(&resolved).await
608}
609
610pub async fn symlink_metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
611 let resolved = resolve_path(path.as_ref());
612 let provider = current_provider();
613 provider.symlink_metadata(&resolved).await
614}
615
616pub async fn read_dir_async(path: impl AsRef<Path>) -> io::Result<Vec<DirEntry>> {
617 let resolved = resolve_path(path.as_ref());
618 let provider = current_provider();
619 provider.read_dir(&resolved).await
620}
621
622pub async fn canonicalize_async(path: impl AsRef<Path>) -> io::Result<PathBuf> {
623 let resolved = resolve_path(path.as_ref());
624 let provider = current_provider();
625 provider.canonicalize(&resolved).await
626}
627
628pub async fn create_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
629 let resolved = resolve_path(path.as_ref());
630 let provider = current_provider();
631 provider.create_dir(&resolved).await
632}
633
634pub async fn create_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
635 let resolved = resolve_path(path.as_ref());
636 let provider = current_provider();
637 provider.create_dir_all(&resolved).await
638}
639
640pub async fn remove_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
641 let resolved = resolve_path(path.as_ref());
642 let provider = current_provider();
643 provider.remove_dir(&resolved).await
644}
645
646pub async fn remove_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
647 let resolved = resolve_path(path.as_ref());
648 let provider = current_provider();
649 provider.remove_dir_all(&resolved).await
650}
651
652pub async fn rename_async(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
653 let resolved_from = resolve_path(from.as_ref());
654 let resolved_to = resolve_path(to.as_ref());
655 let provider = current_provider();
656 provider.rename(&resolved_from, &resolved_to).await
657}
658
659pub async fn set_readonly_async(path: impl AsRef<Path>, readonly: bool) -> io::Result<()> {
660 let resolved = resolve_path(path.as_ref());
661 let provider = current_provider();
662 provider.set_readonly(&resolved, readonly).await
663}
664
665pub async fn data_manifest_descriptor_async(
666 request: &DataManifestRequest,
667) -> io::Result<DataManifestDescriptor> {
668 let provider = current_provider();
669 provider.data_manifest_descriptor(request).await
670}
671
672pub async fn data_chunk_upload_targets_async(
673 request: &DataChunkUploadRequest,
674) -> io::Result<Vec<DataChunkUploadTarget>> {
675 let provider = current_provider();
676 provider.data_chunk_upload_targets(request).await
677}
678
679pub async fn data_upload_chunk_async(
680 target: &DataChunkUploadTarget,
681 data: &[u8],
682) -> io::Result<()> {
683 let provider = current_provider();
684 provider.data_upload_chunk(target, data).await
685}
686
687pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> {
690 let mut reader = OpenOptions::new().read(true).open(from.as_ref())?;
691 let mut writer = OpenOptions::new()
692 .write(true)
693 .create(true)
694 .truncate(true)
695 .open(to.as_ref())?;
696 io::copy(&mut reader, &mut writer)
697}
698
699fn default_provider() -> Arc<dyn FsProvider> {
700 #[cfg(not(target_arch = "wasm32"))]
701 {
702 Arc::new(NativeFsProvider)
703 }
704 #[cfg(target_arch = "wasm32")]
705 {
706 Arc::new(PlaceholderFsProvider)
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use once_cell::sync::Lazy;
714 use std::io::{Read, Seek, SeekFrom, Write};
715 use std::sync::Mutex;
716 use tempfile::tempdir;
717
718 static TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
719
720 struct UnsupportedProvider;
721
722 struct AsyncOpenProvider {
723 opened_async: Arc<Mutex<bool>>,
724 flushed_async: Arc<Mutex<bool>>,
725 }
726
727 struct AsyncTestHandle {
728 cursor: usize,
729 data: Vec<u8>,
730 flushed_async: Arc<Mutex<bool>>,
731 }
732
733 impl Read for AsyncTestHandle {
734 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
735 let remaining = self.data.len().saturating_sub(self.cursor);
736 let to_read = remaining.min(buf.len());
737 buf[..to_read].copy_from_slice(&self.data[self.cursor..self.cursor + to_read]);
738 self.cursor += to_read;
739 Ok(to_read)
740 }
741 }
742
743 impl Write for AsyncTestHandle {
744 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
745 let end = self.cursor + buf.len();
746 if end > self.data.len() {
747 self.data.resize(end, 0);
748 }
749 self.data[self.cursor..end].copy_from_slice(buf);
750 self.cursor = end;
751 Ok(buf.len())
752 }
753
754 fn flush(&mut self) -> io::Result<()> {
755 Ok(())
756 }
757 }
758
759 impl Seek for AsyncTestHandle {
760 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
761 let next = match pos {
762 SeekFrom::Start(offset) => offset as i64,
763 SeekFrom::End(offset) => self.data.len() as i64 + offset,
764 SeekFrom::Current(offset) => self.cursor as i64 + offset,
765 };
766 if next < 0 {
767 return Err(io::Error::new(ErrorKind::InvalidInput, "seek before start"));
768 }
769 self.cursor = next as usize;
770 Ok(self.cursor as u64)
771 }
772 }
773
774 #[async_trait(?Send)]
775 impl FileHandle for AsyncTestHandle {
776 async fn flush_async(&mut self) -> io::Result<()> {
777 *self.flushed_async.lock().unwrap() = true;
778 Ok(())
779 }
780 }
781
782 #[async_trait(?Send)]
783 impl FsProvider for UnsupportedProvider {
784 fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
785 Err(unsupported())
786 }
787
788 async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
789 Err(unsupported())
790 }
791
792 async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
793 Err(unsupported())
794 }
795
796 async fn remove_file(&self, _path: &Path) -> io::Result<()> {
797 Err(unsupported())
798 }
799
800 async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
801 Err(unsupported())
802 }
803
804 async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
805 Err(unsupported())
806 }
807
808 async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
809 Err(unsupported())
810 }
811
812 async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
813 Err(unsupported())
814 }
815
816 async fn create_dir(&self, _path: &Path) -> io::Result<()> {
817 Err(unsupported())
818 }
819
820 async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
821 Err(unsupported())
822 }
823
824 async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
825 Err(unsupported())
826 }
827
828 async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
829 Err(unsupported())
830 }
831
832 async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
833 Err(unsupported())
834 }
835
836 async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
837 Err(unsupported())
838 }
839
840 async fn data_manifest_descriptor(
841 &self,
842 _request: &DataManifestRequest,
843 ) -> io::Result<DataManifestDescriptor> {
844 Err(unsupported())
845 }
846
847 async fn data_chunk_upload_targets(
848 &self,
849 _request: &DataChunkUploadRequest,
850 ) -> io::Result<Vec<DataChunkUploadTarget>> {
851 Err(unsupported())
852 }
853
854 async fn data_upload_chunk(
855 &self,
856 _target: &DataChunkUploadTarget,
857 _data: &[u8],
858 ) -> io::Result<()> {
859 Err(unsupported())
860 }
861 }
862
863 #[async_trait(?Send)]
864 impl FsProvider for AsyncOpenProvider {
865 fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
866 Err(unsupported())
867 }
868
869 async fn open_async(
870 &self,
871 _path: &Path,
872 _flags: &OpenFlags,
873 ) -> io::Result<Box<dyn FileHandle>> {
874 *self.opened_async.lock().unwrap() = true;
875 Ok(Box::new(AsyncTestHandle {
876 cursor: 0,
877 data: b"async contents".to_vec(),
878 flushed_async: self.flushed_async.clone(),
879 }))
880 }
881
882 async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
883 Err(unsupported())
884 }
885
886 async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
887 Err(unsupported())
888 }
889
890 async fn remove_file(&self, _path: &Path) -> io::Result<()> {
891 Err(unsupported())
892 }
893
894 async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
895 Err(unsupported())
896 }
897
898 async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
899 Err(unsupported())
900 }
901
902 async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
903 Err(unsupported())
904 }
905
906 async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
907 Err(unsupported())
908 }
909
910 async fn create_dir(&self, _path: &Path) -> io::Result<()> {
911 Err(unsupported())
912 }
913
914 async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
915 Err(unsupported())
916 }
917
918 async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
919 Err(unsupported())
920 }
921
922 async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
923 Err(unsupported())
924 }
925
926 async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
927 Err(unsupported())
928 }
929
930 async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
931 Err(unsupported())
932 }
933 }
934
935 fn unsupported() -> io::Error {
936 io::Error::new(ErrorKind::Unsupported, "unsupported in test provider")
937 }
938
939 #[test]
940 fn copy_file_round_trip() {
941 let _guard = TEST_LOCK.lock().unwrap();
942 let dir = tempdir().expect("tempdir");
943 let src = dir.path().join("src.bin");
944 let dst = dir.path().join("dst.bin");
945 {
946 let mut file = std::fs::File::create(&src).expect("create src");
947 file.write_all(b"hello filesystem").expect("write src");
948 }
949
950 copy_file(&src, &dst).expect("copy");
951 let mut dst_file = File::open(&dst).expect("open dst");
952 let mut contents = Vec::new();
953 dst_file
954 .read_to_end(&mut contents)
955 .expect("read destination");
956 assert_eq!(contents, b"hello filesystem");
957 }
958
959 #[test]
960 fn set_readonly_flips_metadata_flag() {
961 let _guard = TEST_LOCK.lock().unwrap();
962 let dir = tempdir().expect("tempdir");
963 let path = dir.path().join("flag.txt");
964 futures::executor::block_on(write_async(&path, b"flag")).expect("write");
965
966 futures::executor::block_on(set_readonly_async(&path, true)).expect("set readonly");
967 let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
968 assert!(meta.is_readonly());
969
970 futures::executor::block_on(set_readonly_async(&path, false)).expect("unset readonly");
971 let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
972 assert!(!meta.is_readonly());
973 }
974
975 #[test]
976 fn replace_provider_restores_previous() {
977 let _guard = TEST_LOCK.lock().unwrap();
978 let original = current_provider();
979 let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
980 {
981 let _guard = replace_provider(custom.clone());
982 let active = current_provider();
983 assert!(Arc::ptr_eq(&active, &custom));
984 }
985 let final_provider = current_provider();
986 assert!(Arc::ptr_eq(&final_provider, &original));
987 }
988
989 #[test]
990 fn open_async_and_flush_async_use_provider_async_paths() {
991 let _guard = TEST_LOCK.lock().unwrap();
992 let opened_async = Arc::new(Mutex::new(false));
993 let flushed_async = Arc::new(Mutex::new(false));
994 let provider = Arc::new(AsyncOpenProvider {
995 opened_async: opened_async.clone(),
996 flushed_async: flushed_async.clone(),
997 });
998 let _provider_guard = replace_provider(provider);
999
1000 let mut file =
1001 futures::executor::block_on(OpenOptions::new().read(true).open_async("data.txt"))
1002 .expect("async open");
1003 let mut contents = String::new();
1004 file.read_to_string(&mut contents).expect("read contents");
1005 futures::executor::block_on(file.flush_async()).expect("async flush");
1006
1007 assert_eq!(contents, "async contents");
1008 assert!(*opened_async.lock().unwrap());
1009 assert!(*flushed_async.lock().unwrap());
1010 }
1011
1012 #[test]
1013 fn with_provider_restores_even_on_panic() {
1014 let _guard = TEST_LOCK.lock().unwrap();
1015 let original = current_provider();
1016 let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1017 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1018 with_provider_override(custom.clone(), || {
1019 let active = current_provider();
1020 assert!(Arc::ptr_eq(&active, &custom));
1021 panic!("boom");
1022 })
1023 }));
1024 assert!(result.is_err());
1025 let final_provider = current_provider();
1026 assert!(Arc::ptr_eq(&final_provider, &original));
1027 }
1028}