use crate::file_picker::{FilePickerError, PickerFuture};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, OnceLock};
#[derive(thiserror::Error, Debug, Clone)]
pub enum FolderError {
#[error("writable folder is read-only")]
ReadOnly,
#[error("file not found: {0}")]
NotFound(String),
#[error("writable folders are not supported on this platform")]
Unsupported,
#[error("{0}")]
Io(String),
}
pub trait WritableFolderStore: Send + Sync {
fn write(&self, name: &str, contents: &[u8]) -> Result<(), FolderError>;
fn read(&self, name: &str) -> Result<Vec<u8>, FolderError>;
fn list(&self) -> Result<Vec<String>, FolderError>;
fn remove(&self, name: &str) -> Result<(), FolderError>;
fn is_writable(&self) -> bool;
fn handle(&self) -> String;
}
pub type WritableFolderStoreRef = Arc<dyn WritableFolderStore>;
pub trait WritableFolderPicker {
fn pick(&self) -> PickerFuture<Result<Option<String>, FilePickerError>>;
}
pub type WritableFolderPickerRef = Rc<dyn WritableFolderPicker>;
thread_local! {
static PLATFORM_PICKER: RefCell<Option<WritableFolderPickerRef>> = const { RefCell::new(None) };
}
pub fn set_platform_writable_folder_picker(picker: WritableFolderPickerRef) {
PLATFORM_PICKER.with(|cell| *cell.borrow_mut() = Some(picker));
}
pub fn clear_platform_writable_folder_picker() {
PLATFORM_PICKER.with(|cell| *cell.borrow_mut() = None);
}
fn registered_picker() -> Option<WritableFolderPickerRef> {
PLATFORM_PICKER.with(|cell| cell.borrow().clone())
}
type StoreFactory = Box<dyn Fn(&str) -> Option<WritableFolderStoreRef> + Send + Sync>;
static STORE_FACTORY: OnceLock<StoreFactory> = OnceLock::new();
pub fn set_writable_folder_store_factory(factory: StoreFactory) {
let _ = STORE_FACTORY.set(factory);
}
pub fn pick_writable_folder() -> PickerFuture<Result<Option<String>, FilePickerError>> {
if let Some(picker) = registered_picker() {
return picker.pick();
}
builtin_pick()
}
pub fn open_writable_folder(handle: &str) -> Option<WritableFolderStoreRef> {
if let Some(factory) = STORE_FACTORY.get() {
return factory(handle);
}
builtin_open(handle)
}
fn builtin_pick() -> PickerFuture<Result<Option<String>, FilePickerError>> {
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
not(target_os = "ios"),
feature = "file-picker-native"
))]
{
return desktop::pick();
}
#[allow(unreachable_code)]
Box::pin(async { Err(FilePickerError::UnsupportedPlatform) })
}
fn builtin_open(handle: &str) -> Option<WritableFolderStoreRef> {
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
not(target_os = "ios"),
feature = "file-picker-native"
))]
{
return Some(desktop::open(handle));
}
#[allow(unreachable_code)]
{
let _ = handle;
None
}
}
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
not(target_os = "ios"),
feature = "file-picker-native"
))]
mod desktop;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn folder_error_messages_are_distinct() {
assert_eq!(
FolderError::ReadOnly.to_string(),
"writable folder is read-only"
);
assert!(FolderError::NotFound("a.txt".into())
.to_string()
.contains("a.txt"));
}
#[test]
fn picker_registration_round_trips() {
struct Marker;
impl WritableFolderPicker for Marker {
fn pick(&self) -> PickerFuture<Result<Option<String>, FilePickerError>> {
Box::pin(async { Ok(Some("handle".to_string())) })
}
}
clear_platform_writable_folder_picker();
assert!(registered_picker().is_none());
set_platform_writable_folder_picker(Rc::new(Marker));
assert!(registered_picker().is_some());
let result = pollster::block_on(pick_writable_folder());
assert!(matches!(result, Ok(Some(handle)) if handle == "handle"));
clear_platform_writable_folder_picker();
}
}