1use super::blob_readable_stream::BlobReadableStream;
2use super::writeable_stream::{settle_pending_closes, WriteableStream};
3use crate::browser_app_builder::BrowserPlatformOptions;
4use crate::interop::completion_helper::PromiseError;
5use crate::interop::storage_helper::{self, StorageItemProperties};
6use crate::interop::{ferro_module, non_null, JsObject};
7use ferroui_base::animation::TimeSpan;
8use ferroui_base::input::LocalBoxFuture;
9use ferroui_base::platform::storage::file_io::{DecodeResult, StorageBookmarkHelper, StorageProviderHelpers};
10use ferroui_base::platform::storage::{
11 FilePickerFileType, FilePickerFileTypes, FilePickerOpenOptions, FilePickerSaveOptions, FolderPickerOpenOptions,
12 IStorageBookmarkFile, IStorageBookmarkFolder, IStorageBookmarkItem, IStorageFile, IStorageFolder, IStorageItem,
13 IStorageProvider, OpenFilePickerResult, SaveFilePickerResult, StorageItemProperties as ItemProperties,
14 WellKnownFolder,
15};
16use ferroui_base::reactive::IDisposable;
17use ferroui_base::utilities::{DateTimeOffset, Uri, UriKind};
18use ferroui_base::{FerroLocator, LocatorExtensions};
19use std::any::Any;
20use std::cell::RefCell;
21use std::io::{self, Read, Write};
22use std::rc::{Rc, Weak};
23use wasm_bindgen::JsCast;
24
25pub(crate) const BROWSER_BOOKMARK_KEY: &[u8] = b"browser";
27pub(crate) const PICKER_CANCEL_MESSAGE: &str = "The user aborted a request";
28pub(crate) const NO_PERMISSIONS_MESSAGE: &str = "Permissions denied";
29pub(crate) const FILE_FOLDER_NOT_FOUND_MESSAGE: &str = "A requested file or directory could not be found";
30pub(crate) const TYPE_MISSMATCH_MESSAGE: &str = "The path supplied exists, but was not an entry of requested type";
31
32#[derive(Default)]
38pub struct BrowserStorageProvider;
39
40fn ready<T: 'static>(value: T) -> LocalBoxFuture<T> {
41 Box::pin(std::future::ready(value))
42}
43
44fn is_cancel(error: &PromiseError) -> bool {
45 error.message().contains(PICKER_CANCEL_MESSAGE)
46}
47
48fn denied(error: PromiseError) -> io::Error {
49 if error.message() == NO_PERMISSIONS_MESSAGE {
50 io::Error::new(io::ErrorKind::PermissionDenied, "User denied permissions to open the file")
51 } else {
52 error.into()
53 }
54}
55
56impl BrowserStorageProvider {
57 pub fn new() -> Self {
58 Self
59 }
60
61 fn prefer_polyfill() -> bool {
62 FerroLocator::current()
63 .get_service::<BrowserPlatformOptions>()
64 .is_some_and(|options| options.prefer_file_dialog_polyfill)
65 }
66
67 fn start_in(location: Option<&Rc<dyn IStorageFolder>>) -> Option<JsObject> {
69 location.and_then(|location| JsStorageItem::of(&**location)).and_then(JsStorageItem::file_handle)
70 }
71
72 fn convert_file_types(input: Option<&[Rc<FilePickerFileType>]>) -> (Option<Vec<JsObject>>, bool) {
73 let all = FilePickerFileTypes::all();
74 let types: Option<Vec<JsObject>> = input.map(|input| {
75 input
76 .iter()
77 .filter(|t| t.mime_types().is_some_and(|m| !m.is_empty()) && !Rc::ptr_eq(t, &all))
78 .map(|t| {
79 storage_helper::create_accept_type(
80 t.name(),
81 t.mime_types().map(|m| m.to_vec()).unwrap_or_default(),
82 t.try_get_extensions().map(|e| e.iter().map(|e| format!(".{e}")).collect()),
83 )
84 })
85 .collect()
86 });
87 let types = types.filter(|types| !types.is_empty());
88
89 let include_all = input.is_some_and(|input| input.iter().any(|t| Rc::ptr_eq(t, &all))) || types.is_none();
90
91 (types, !include_all)
92 }
93
94 async fn open_file_picker(options: FilePickerOpenOptions) -> io::Result<Vec<Rc<dyn IStorageFile>>> {
95 ferro_module::import_storage().await?;
96 let start_in = Self::start_in(options.suggested_start_location());
97
98 let (types, exclude_all) = Self::convert_file_types(options.file_type_filter());
99
100 let items = match storage_helper::open_file_dialog(
101 start_in.as_ref(),
102 options.allow_multiple(),
103 types,
104 exclude_all,
105 Self::prefer_polyfill(),
106 )
107 .await
108 {
109 Ok(items) => items,
110 Err(error) if is_cancel(&error) => return Ok(Vec::new()),
111 Err(error) => return Err(error.into()),
112 };
113 let Some(items) = non_null(items) else {
114 return Ok(Vec::new());
115 };
116
117 let items_array = storage_helper::items_array(&items);
118 Ok(items_array.into_iter().map(|item| JsStorageFile::new(item) as Rc<dyn IStorageFile>).collect())
119 }
120
121 async fn save_file_picker(options: FilePickerSaveOptions) -> io::Result<Option<Rc<dyn IStorageFile>>> {
122 ferro_module::import_storage().await?;
123 let start_in = Self::start_in(options.suggested_start_location());
124
125 let (types, exclude_all) = Self::convert_file_types(options.file_type_choices());
126
127 let suggested_name = StorageProviderHelpers::name_with_extension(
128 options.suggested_file_name(),
129 options.default_extension(),
130 None,
131 );
132 match storage_helper::save_file_dialog(
133 start_in.as_ref(),
134 suggested_name,
135 types,
136 exclude_all,
137 Self::prefer_polyfill(),
138 )
139 .await
140 {
141 Ok(item) => Ok(non_null(item).map(|item| JsStorageFile::new(item) as Rc<dyn IStorageFile>)),
142 Err(error) if is_cancel(&error) => Ok(None),
143 Err(error) => Err(error.into()),
144 }
145 }
146
147 async fn open_folder_picker(options: FolderPickerOpenOptions) -> io::Result<Vec<Rc<dyn IStorageFolder>>> {
148 ferro_module::import_storage().await?;
149 let start_in = Self::start_in(options.suggested_start_location());
150
151 match storage_helper::select_folder_dialog(start_in.as_ref(), Self::prefer_polyfill()).await {
152 Ok(item) => Ok(non_null(item)
153 .map(|item| vec![JsStorageFolder::new(item) as Rc<dyn IStorageFolder>])
154 .unwrap_or_default()),
155 Err(error) if is_cancel(&error) => Ok(Vec::new()),
156 Err(error) => Err(error.into()),
157 }
158 }
159
160 async fn decode_bookmark(bookmark: String) -> Option<JsStorageItemKind> {
161 ferro_module::import_storage().await.ok()?;
162 let item = match StorageBookmarkHelper::try_decode_bookmark(BROWSER_BOOKMARK_KEY, Some(&bookmark)) {
163 (DecodeResult::Success, Some(bytes)) => {
164 storage_helper::open_bookmark(&String::from_utf8_lossy(&bytes)).await.ok()
165 }
166 (DecodeResult::InvalidFormat, _) => storage_helper::open_bookmark(&bookmark).await.ok(),
168 _ => None,
169 };
170 JsStorageItemKind::from_item(non_null(item?)?)
171 }
172
173 fn well_known_directory_name(well_known_folder: WellKnownFolder) -> &'static str {
174 match well_known_folder {
175 WellKnownFolder::Desktop => "desktop",
176 WellKnownFolder::Documents => "documents",
177 WellKnownFolder::Downloads => "downloads",
178 WellKnownFolder::Music => "music",
179 WellKnownFolder::Pictures => "pictures",
180 WellKnownFolder::Videos => "videos",
181 }
182 }
183}
184
185impl IStorageProvider for BrowserStorageProvider {
186 fn can_open(&self) -> bool {
187 true
188 }
189
190 fn open_file_picker_async(
191 &self,
192 options: FilePickerOpenOptions,
193 ) -> LocalBoxFuture<io::Result<Vec<Rc<dyn IStorageFile>>>> {
194 Box::pin(Self::open_file_picker(options))
195 }
196
197 fn open_file_picker_with_result_async(
198 &self,
199 options: FilePickerOpenOptions,
200 ) -> LocalBoxFuture<io::Result<OpenFilePickerResult>> {
201 let files = self.open_file_picker_async(options);
202 Box::pin(async move { Ok(OpenFilePickerResult { files: files.await?, selected_file_type: None }) })
203 }
204
205 fn can_save(&self) -> bool {
206 true
207 }
208
209 fn save_file_picker_async(
210 &self,
211 options: FilePickerSaveOptions,
212 ) -> LocalBoxFuture<io::Result<Option<Rc<dyn IStorageFile>>>> {
213 Box::pin(Self::save_file_picker(options))
214 }
215
216 fn save_file_picker_with_result_async(
217 &self,
218 options: FilePickerSaveOptions,
219 ) -> LocalBoxFuture<io::Result<SaveFilePickerResult>> {
220 let file = self.save_file_picker_async(options);
221 Box::pin(async move { Ok(SaveFilePickerResult { file: file.await?, selected_file_type: None }) })
222 }
223
224 fn can_pick_folder(&self) -> bool {
225 true
226 }
227
228 fn open_folder_picker_async(
229 &self,
230 options: FolderPickerOpenOptions,
231 ) -> LocalBoxFuture<io::Result<Vec<Rc<dyn IStorageFolder>>>> {
232 Box::pin(Self::open_folder_picker(options))
233 }
234
235 fn open_file_bookmark_async(&self, bookmark: &str) -> LocalBoxFuture<Option<Rc<dyn IStorageBookmarkFile>>> {
236 let bookmark = bookmark.to_string();
237 Box::pin(async move {
238 match Self::decode_bookmark(bookmark).await? {
239 JsStorageItemKind::File(file) => Some(file as Rc<dyn IStorageBookmarkFile>),
240 JsStorageItemKind::Folder(_) => None,
241 }
242 })
243 }
244
245 fn open_folder_bookmark_async(&self, bookmark: &str) -> LocalBoxFuture<Option<Rc<dyn IStorageBookmarkFolder>>> {
246 let bookmark = bookmark.to_string();
247 Box::pin(async move {
248 match Self::decode_bookmark(bookmark).await? {
249 JsStorageItemKind::Folder(folder) => Some(folder as Rc<dyn IStorageBookmarkFolder>),
250 JsStorageItemKind::File(_) => None,
251 }
252 })
253 }
254
255 fn try_get_file_from_path_async(&self, _file_path: &Uri) -> LocalBoxFuture<Option<Rc<dyn IStorageFile>>> {
256 ready(None)
257 }
258
259 fn try_get_folder_from_path_async(&self, _folder_path: &Uri) -> LocalBoxFuture<Option<Rc<dyn IStorageFolder>>> {
260 ready(None)
261 }
262
263 fn try_get_well_known_folder_async(
264 &self,
265 well_known_folder: WellKnownFolder,
266 ) -> LocalBoxFuture<Option<Rc<dyn IStorageFolder>>> {
267 Box::pin(async move {
268 ferro_module::import_storage().await.ok()?;
269 let directory =
270 storage_helper::create_well_known_directory(Self::well_known_directory_name(well_known_folder));
271 Some(JsStorageFolder::new(directory) as Rc<dyn IStorageFolder>)
272 })
273 }
274}
275
276enum JsStorageItemKind {
278 File(Rc<JsStorageFile>),
279 Folder(Rc<JsStorageFolder>),
280}
281
282impl JsStorageItemKind {
283 fn from_item(item: JsObject) -> Option<Self> {
284 match storage_helper::kind_of(&item).as_deref() {
285 Some("directory") => Some(Self::Folder(JsStorageFolder::new(item))),
286 Some("file") => Some(Self::File(JsStorageFile::new(item))),
287 _ => None,
288 }
289 }
290
291 fn from_handle(handle: JsObject) -> Option<Self> {
293 let item = storage_helper::storage_item_from_handle(&handle)?;
294 Self::from_item(item)
295 }
296
297 fn into_item(self) -> Rc<dyn IStorageItem> {
298 match self {
299 Self::File(file) => file,
300 Self::Folder(folder) => folder,
301 }
302 }
303}
304
305pub(crate) struct JsStorageItem {
307 file_handle: RefCell<Option<JsObject>>,
308 this: RefCell<Option<Weak<dyn IStorageItem>>>,
309}
310
311impl JsStorageItem {
312 fn new(file_handle: JsObject) -> Self {
313 Self { file_handle: RefCell::new(Some(file_handle)), this: RefCell::new(None) }
314 }
315
316 fn of(item: &dyn IStorageItem) -> Option<&JsStorageItem> {
318 let item = item.as_any()?;
319 if let Some(file) = item.downcast_ref::<JsStorageFile>() {
320 return Some(&file.base);
321 }
322 item.downcast_ref::<JsStorageFolder>().map(|folder| &folder.base)
323 }
324
325 pub(crate) fn file_handle(&self) -> Option<JsObject> {
327 self.file_handle.borrow().clone()
328 }
329
330 fn require_file_handle(&self) -> io::Result<JsObject> {
331 self.file_handle()
332 .ok_or_else(|| io::Error::other("Cannot access a disposed object. Object name: 'JSStorageItem'."))
333 }
334
335 fn name(&self) -> String {
336 self.file_handle().and_then(|handle| storage_helper::name_of(&handle)).unwrap_or_default()
337 }
338
339 fn path(&self) -> Uri {
340 let name = self.name();
341 Uri::new(&name, UriKind::Relative)
343 .or_else(|_| Uri::new(&format!("./{name}"), UriKind::Relative))
344 .expect("a dot segment makes any name a relative reference")
345 }
346
347 fn get_basic_properties_async(&self) -> LocalBoxFuture<ItemProperties> {
348 let handle = self.file_handle();
349 Box::pin(async move {
350 let properties = match handle {
352 Some(handle) if ferro_module::import_storage().await.is_ok() => {
353 let _ = settle_pending_closes(&handle, false).await;
355 storage_helper::get_properties(&handle).await.ok().and_then(non_null)
356 }
357 _ => None,
358 };
359 let properties = properties.map(JsCast::unchecked_into::<StorageItemProperties>);
360 let size = properties.as_ref().and_then(|p| p.size()).map(|size| size as i64);
361 let last_modified = properties.as_ref().and_then(|p| p.last_modified()).map(|ms| ms as i64);
362
363 ItemProperties::new(
364 size.map(|size| size as u64),
365 None,
366 last_modified.filter(|ms| *ms > 0).map(from_unix_time_milliseconds),
367 )
368 })
369 }
370
371 fn can_bookmark(&self) -> bool {
372 storage_helper::has_native_file_picker()
373 }
374
375 fn save_bookmark_async(&self) -> LocalBoxFuture<Option<String>> {
376 if !self.can_bookmark() {
377 return ready(None);
378 }
379 let handle = self.file_handle();
380 Box::pin(async move {
381 ferro_module::import_storage().await.ok()?;
382 let native_bookmark = storage_helper::save_bookmark(&handle?).await.ok()?.as_string()?;
383 StorageBookmarkHelper::encode_bookmark(BROWSER_BOOKMARK_KEY, Some(&native_bookmark))
384 })
385 }
386
387 fn get_parent_async(&self) -> LocalBoxFuture<Option<Rc<dyn IStorageFolder>>> {
388 ready(None)
389 }
390
391 fn delete_async(&self) -> LocalBoxFuture<io::Result<()>> {
392 let handle = self.require_file_handle();
393 Box::pin(async move {
394 ferro_module::import_storage().await?;
395 let handle = handle?;
396 let _ = settle_pending_closes(&handle, false).await;
398 storage_helper::delete_async(&handle).await?;
399 Ok(())
400 })
401 }
402
403 fn move_async(
404 &self,
405 destination: Rc<dyn IStorageFolder>,
406 ) -> LocalBoxFuture<io::Result<Option<Rc<dyn IStorageItem>>>> {
407 let Some(folder) = destination.as_any().and_then(|item| item.downcast_ref::<JsStorageFolder>()) else {
408 return ready(Err(io::Error::new(
409 io::ErrorKind::InvalidInput,
410 "Destination folder must be initialized the StorageProvider API.",
411 )));
412 };
413 let handle = self.require_file_handle();
414 let destination_handle = folder.base.require_file_handle();
415 let this = self.this.borrow().clone();
416 Box::pin(async move {
417 ferro_module::import_storage().await?;
418 let handle = handle?;
419 let _ = settle_pending_closes(&handle, false).await;
422 let item_handle = storage_helper::move_async(&handle, &destination_handle?).await?;
423 let Some(item_handle) = non_null(item_handle) else {
424 return Ok(None);
425 };
426
427 Ok(match JsStorageItemKind::from_handle(item_handle) {
428 Some(item) => Some(item.into_item()),
429 None => this.and_then(|this| this.upgrade()),
430 })
431 })
432 }
433
434 fn release_bookmark_async(&self) -> LocalBoxFuture<()> {
435 if !self.can_bookmark() {
436 return ready(());
437 }
438 let handle = self.file_handle();
439 Box::pin(async move {
440 if let Some(handle) = handle {
441 if ferro_module::import_storage().await.is_err() {
442 return;
443 }
444 let _ = storage_helper::delete_bookmark(&handle).await;
445 }
446 })
447 }
448
449 fn dispose(&self) {
450 self.file_handle.borrow_mut().take();
451 }
452}
453
454fn from_unix_time_milliseconds(milliseconds: i64) -> DateTimeOffset {
455 DateTimeOffset::UNIX_EPOCH.add(TimeSpan::from_ticks(milliseconds * 10_000))
456}
457
458macro_rules! impl_js_storage_item {
459 ($type:ty, $kind:ident, $trait:ident) => {
460 impl IDisposable for $type {
461 fn dispose(&self) {
462 self.base.dispose();
463 }
464 }
465
466 impl IStorageItem for $type {
467 fn name(&self) -> String {
468 self.base.name()
469 }
470
471 fn path(&self) -> Uri {
472 self.base.path()
473 }
474
475 fn get_basic_properties_async(&self) -> LocalBoxFuture<ItemProperties> {
476 self.base.get_basic_properties_async()
477 }
478
479 fn can_bookmark(&self) -> bool {
480 self.base.can_bookmark()
481 }
482
483 fn save_bookmark_async(&self) -> LocalBoxFuture<Option<String>> {
484 self.base.save_bookmark_async()
485 }
486
487 fn get_parent_async(&self) -> LocalBoxFuture<Option<Rc<dyn IStorageFolder>>> {
488 self.base.get_parent_async()
489 }
490
491 fn delete_async(&self) -> LocalBoxFuture<io::Result<()>> {
492 self.base.delete_async()
493 }
494
495 fn move_async(
496 &self,
497 destination: Rc<dyn IStorageFolder>,
498 ) -> LocalBoxFuture<io::Result<Option<Rc<dyn IStorageItem>>>> {
499 self.base.move_async(destination)
500 }
501
502 fn $kind(self: Rc<Self>) -> Option<Rc<dyn $trait>> {
503 Some(self)
504 }
505
506 fn as_any(&self) -> Option<&dyn Any> {
507 Some(self)
508 }
509 }
510
511 impl IStorageBookmarkItem for $type {
512 fn release_bookmark_async(&self) -> LocalBoxFuture<()> {
513 self.base.release_bookmark_async()
514 }
515 }
516 };
517}
518
519pub(crate) struct JsStorageFile {
521 base: JsStorageItem,
522}
523
524impl JsStorageFile {
525 pub(crate) fn new(file_handle: JsObject) -> Rc<Self> {
526 let this = Rc::new(Self { base: JsStorageItem::new(file_handle) });
527 let weak: Weak<dyn IStorageItem> = Rc::downgrade(&this) as Weak<dyn IStorageItem>;
528 *this.base.this.borrow_mut() = Some(weak);
529 this
530 }
531
532 pub async fn open_read_stream_async(&self) -> io::Result<BlobReadableStream> {
535 let handle = self.base.require_file_handle()?;
536 ferro_module::import_storage().await?;
539 settle_pending_closes(&handle, true).await?;
540 let blob = storage_helper::open_read(&handle).await.map_err(denied)?;
541 let mut stream = BlobReadableStream::new(blob);
542 stream.load_async().await?;
543 Ok(stream)
544 }
545
546 pub async fn open_write_stream_async(&self) -> io::Result<WriteableStream> {
548 let handle = self.base.require_file_handle()?;
549 ferro_module::import_storage().await?;
550 settle_pending_closes(&handle, true).await?;
551 let stream_writer = storage_helper::open_write(&handle).await.map_err(denied)?;
552
553 Ok(WriteableStream::new(stream_writer, Some(handle), 0))
556 }
557}
558
559impl_js_storage_item!(JsStorageFile, as_storage_file, IStorageFile);
560
561impl IStorageFile for JsStorageFile {
562 fn open_read_async(&self) -> LocalBoxFuture<io::Result<Box<dyn Read>>> {
563 let handle = self.base.file_handle();
564 Box::pin(async move {
565 let file = JsStorageFile::detached(handle)?;
566 Ok(Box::new(file.open_read_stream_async().await?) as Box<dyn Read>)
567 })
568 }
569
570 fn open_write_async(&self) -> LocalBoxFuture<io::Result<Box<dyn Write>>> {
571 let handle = self.base.file_handle();
572 Box::pin(async move {
573 let file = JsStorageFile::detached(handle)?;
574 Ok(Box::new(file.open_write_stream_async().await?) as Box<dyn Write>)
575 })
576 }
577}
578
579impl JsStorageFile {
580 fn detached(handle: Option<JsObject>) -> io::Result<JsStorageFile> {
583 let base = JsStorageItem { file_handle: RefCell::new(handle), this: RefCell::new(None) };
584 base.require_file_handle()?;
585 Ok(JsStorageFile { base })
586 }
587}
588
589impl IStorageBookmarkFile for JsStorageFile {}
590
591pub(crate) struct JsStorageFolder {
593 base: JsStorageItem,
594}
595
596impl JsStorageFolder {
597 pub(crate) fn new(file_handle: JsObject) -> Rc<Self> {
598 let this = Rc::new(Self { base: JsStorageItem::new(file_handle) });
599 let weak: Weak<dyn IStorageItem> = Rc::downgrade(&this) as Weak<dyn IStorageItem>;
600 *this.base.this.borrow_mut() = Some(weak);
601 this
602 }
603
604 fn should_supress_error_on_file_access(error: &PromiseError) -> bool {
605 error.message() == NO_PERMISSIONS_MESSAGE
606 || error.message().contains(TYPE_MISSMATCH_MESSAGE)
607 || error.message().contains(FILE_FOLDER_NOT_FOUND_MESSAGE)
608 }
609}
610
611impl_js_storage_item!(JsStorageFolder, as_storage_folder, IStorageFolder);
612
613impl IStorageFolder for JsStorageFolder {
614 fn get_items_async(&self) -> LocalBoxFuture<io::Result<Vec<Rc<dyn IStorageItem>>>> {
615 let handle = self.base.require_file_handle();
616 Box::pin(async move {
617 let mut items: Vec<Rc<dyn IStorageItem>> = Vec::new();
618 let Some(items_iterator) = storage_helper::get_items_iterator(&handle?) else {
619 return Ok(items);
620 };
621
622 loop {
623 let Some(next_result) = non_null(items_iterator.next().await?) else {
624 break;
625 };
626 let next_result = next_result.unchecked_into::<storage_helper::ItemsIteratorResult>();
627
628 if next_result.done().unwrap_or(false) {
629 break;
630 }
631
632 let Some(storage_item) = next_result.item() else {
634 break;
635 };
636
637 if let Some(item) = JsStorageItemKind::from_handle(storage_item) {
638 items.push(item.into_item());
639 }
640 }
641
642 Ok(items)
643 })
644 }
645
646 fn get_folder_async(&self, name: &str) -> LocalBoxFuture<Option<Rc<dyn IStorageFolder>>> {
647 let handle = self.base.file_handle();
648 let name = name.to_string();
649 Box::pin(async move {
650 match storage_helper::get_folder(&handle?, &name).await {
651 Ok(folder_handle) => {
652 let storage_folder = storage_helper::storage_item_from_handle(&non_null(folder_handle)?)?;
653 Some(JsStorageFolder::new(storage_folder) as Rc<dyn IStorageFolder>)
654 }
655 Err(error) if Self::should_supress_error_on_file_access(&error) => None,
656 Err(_) => None,
658 }
659 })
660 }
661
662 fn get_file_async(&self, name: &str) -> LocalBoxFuture<Option<Rc<dyn IStorageFile>>> {
663 let handle = self.base.file_handle();
664 let name = name.to_string();
665 Box::pin(async move {
666 match storage_helper::get_file(&handle?, &name).await {
667 Ok(file_handle) => {
668 let storage_file = storage_helper::storage_item_from_handle(&non_null(file_handle)?)?;
669 Some(JsStorageFile::new(storage_file) as Rc<dyn IStorageFile>)
670 }
671 Err(error) if Self::should_supress_error_on_file_access(&error) => None,
672 Err(_) => None,
674 }
675 })
676 }
677
678 fn create_file_async(&self, name: &str) -> LocalBoxFuture<io::Result<Option<Rc<dyn IStorageFile>>>> {
679 let handle = self.base.require_file_handle();
680 let name = name.to_string();
681 Box::pin(async move {
682 let file_handle = storage_helper::create_file(&handle?, &name).await.map_err(denied)?;
683 let Some(file_handle) = non_null(file_handle) else {
684 return Ok(None);
685 };
686
687 Ok(storage_helper::storage_item_from_handle(&file_handle)
688 .map(|storage_file| JsStorageFile::new(storage_file) as Rc<dyn IStorageFile>))
689 })
690 }
691
692 fn create_folder_async(&self, name: &str) -> LocalBoxFuture<io::Result<Option<Rc<dyn IStorageFolder>>>> {
693 let handle = self.base.require_file_handle();
694 let name = name.to_string();
695 Box::pin(async move {
696 let folder_handle = storage_helper::create_folder(&handle?, &name).await.map_err(denied)?;
697 let Some(folder_handle) = non_null(folder_handle) else {
698 return Ok(None);
699 };
700
701 Ok(storage_helper::storage_item_from_handle(&folder_handle)
702 .map(|storage_folder| JsStorageFolder::new(storage_folder) as Rc<dyn IStorageFolder>))
703 })
704 }
705}
706
707impl IStorageBookmarkFolder for JsStorageFolder {}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
714
715 #[test]
716 fn convert_file_types_without_filter_includes_all() {
717 let (types, exclude_all) = BrowserStorageProvider::convert_file_types(None);
718 assert!(types.is_none());
719 assert!(!exclude_all);
720 }
721
722 #[test]
723 fn convert_file_types_with_only_all_includes_all() {
724 let all = [FilePickerFileTypes::all()];
725 let (types, exclude_all) = BrowserStorageProvider::convert_file_types(Some(&all));
726 assert!(types.is_none());
727 assert!(!exclude_all);
728 }
729
730 #[test]
731 fn convert_file_types_without_mime_types_includes_all() {
732 let patterns_only = [FilePickerFileType::new(Some("Text")).with_patterns(&["*.txt"])];
733 let (types, exclude_all) = BrowserStorageProvider::convert_file_types(Some(&patterns_only));
734 assert!(types.is_none());
735 assert!(!exclude_all);
736 }
737
738 #[test]
739 fn picker_cancel_and_permission_messages_are_recognised() {
740 assert!(is_cancel(&PromiseError::new("AbortError", "The user aborted a request.")));
741 assert!(!is_cancel(&PromiseError::new("", "Permissions denied")));
742 assert_eq!(denied(PromiseError::new("", "Permissions denied")).kind(), io::ErrorKind::PermissionDenied);
743 assert_eq!(denied(PromiseError::new("", "other")).kind(), io::ErrorKind::Other);
744 assert!(JsStorageFolder::should_supress_error_on_file_access(&PromiseError::new("",
745 "A requested file or directory could not be found at the time an operation was processed."
746 )));
747 assert!(!JsStorageFolder::should_supress_error_on_file_access(&PromiseError::new("", "other")));
748 }
749
750 #[test]
751 fn last_modified_converts_from_unix_milliseconds() {
752 let date = from_unix_time_milliseconds(86_400_000);
753 assert_eq!(date, DateTimeOffset::UNIX_EPOCH.add_days(1.0));
754 }
755
756 #[test]
757 fn well_known_folders_map_to_the_picker_names() {
758 assert_eq!(BrowserStorageProvider::well_known_directory_name(WellKnownFolder::Desktop), "desktop");
759 assert_eq!(BrowserStorageProvider::well_known_directory_name(WellKnownFolder::Videos), "videos");
760 }
761}