use std::sync::{Arc, OnceLock};
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
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),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FolderEntry {
pub name: String,
pub len: u64,
pub modified_millis: Option<u64>,
}
pub trait FolderReader: Send {
fn read_chunk(&mut self) -> Result<Option<Vec<u8>>, FolderError>;
}
pub trait FolderWriter: Send {
fn write_chunk(&mut self, bytes: &[u8]) -> Result<(), FolderError>;
fn finish(self: Box<Self>) -> Result<(), FolderError>;
}
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<FolderEntry>, FolderError>;
fn remove(&self, name: &str) -> Result<(), FolderError>;
fn open_read(&self, name: &str) -> Result<Box<dyn FolderReader>, FolderError>;
fn open_write(&self, name: &str) -> Result<Box<dyn FolderWriter>, FolderError>;
fn is_writable(&self) -> bool;
fn handle(&self) -> String;
fn display_name(&self) -> String {
let handle = self.handle();
handle
.trim_end_matches(['/', '\\'])
.rsplit(['/', '\\', ':'])
.find(|segment| !segment.is_empty())
.map(ToString::to_string)
.unwrap_or(handle)
}
fn entry(&self, name: &str) -> Result<FolderEntry, FolderError> {
self.list()?
.into_iter()
.find(|entry| entry.name == name)
.ok_or_else(|| FolderError::NotFound(name.to_string()))
}
}
pub type WritableFolderStoreRef = Arc<dyn WritableFolderStore>;
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 open_writable_folder(handle: &str) -> Option<WritableFolderStoreRef> {
if let Some(factory) = STORE_FACTORY.get() {
return factory(handle);
}
builtin_open(handle)
}
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)]
#[path = "tests/writable_folder_tests.rs"]
mod tests;