cranpose_services/
writable_folder.rs1use crate::file_picker::{FilePickerError, PickerFuture};
25use std::cell::RefCell;
26use std::rc::Rc;
27use std::sync::{Arc, OnceLock};
28
29#[derive(thiserror::Error, Debug, Clone)]
31pub enum FolderError {
32 #[error("writable folder is read-only")]
34 ReadOnly,
35 #[error("file not found: {0}")]
37 NotFound(String),
38 #[error("writable folders are not supported on this platform")]
40 Unsupported,
41 #[error("{0}")]
43 Io(String),
44}
45
46pub trait WritableFolderStore: Send + Sync {
52 fn write(&self, name: &str, contents: &[u8]) -> Result<(), FolderError>;
54
55 fn read(&self, name: &str) -> Result<Vec<u8>, FolderError>;
57
58 fn list(&self) -> Result<Vec<String>, FolderError>;
60
61 fn remove(&self, name: &str) -> Result<(), FolderError>;
63
64 fn is_writable(&self) -> bool;
67
68 fn handle(&self) -> String;
71}
72
73pub type WritableFolderStoreRef = Arc<dyn WritableFolderStore>;
75
76pub trait WritableFolderPicker {
79 fn pick(&self) -> PickerFuture<Result<Option<String>, FilePickerError>>;
81}
82
83pub type WritableFolderPickerRef = Rc<dyn WritableFolderPicker>;
85
86thread_local! {
87 static PLATFORM_PICKER: RefCell<Option<WritableFolderPickerRef>> = const { RefCell::new(None) };
88}
89
90pub fn set_platform_writable_folder_picker(picker: WritableFolderPickerRef) {
94 PLATFORM_PICKER.with(|cell| *cell.borrow_mut() = Some(picker));
95}
96
97pub fn clear_platform_writable_folder_picker() {
99 PLATFORM_PICKER.with(|cell| *cell.borrow_mut() = None);
100}
101
102fn registered_picker() -> Option<WritableFolderPickerRef> {
103 PLATFORM_PICKER.with(|cell| cell.borrow().clone())
104}
105
106type StoreFactory = Box<dyn Fn(&str) -> Option<WritableFolderStoreRef> + Send + Sync>;
109static STORE_FACTORY: OnceLock<StoreFactory> = OnceLock::new();
110
111pub fn set_writable_folder_store_factory(factory: StoreFactory) {
114 let _ = STORE_FACTORY.set(factory);
115}
116
117pub fn pick_writable_folder() -> PickerFuture<Result<Option<String>, FilePickerError>> {
120 if let Some(picker) = registered_picker() {
121 return picker.pick();
122 }
123 builtin_pick()
124}
125
126pub fn open_writable_folder(handle: &str) -> Option<WritableFolderStoreRef> {
129 if let Some(factory) = STORE_FACTORY.get() {
130 return factory(handle);
131 }
132 builtin_open(handle)
133}
134
135fn builtin_pick() -> PickerFuture<Result<Option<String>, FilePickerError>> {
136 #[cfg(all(
137 not(target_arch = "wasm32"),
138 not(target_os = "android"),
139 not(target_os = "ios"),
140 feature = "file-picker-native"
141 ))]
142 {
143 return desktop::pick();
144 }
145 #[allow(unreachable_code)]
146 Box::pin(async { Err(FilePickerError::UnsupportedPlatform) })
147}
148
149fn builtin_open(handle: &str) -> Option<WritableFolderStoreRef> {
150 #[cfg(all(
151 not(target_arch = "wasm32"),
152 not(target_os = "android"),
153 not(target_os = "ios"),
154 feature = "file-picker-native"
155 ))]
156 {
157 return Some(desktop::open(handle));
158 }
159 #[allow(unreachable_code)]
160 {
161 let _ = handle;
162 None
163 }
164}
165
166#[cfg(all(
167 not(target_arch = "wasm32"),
168 not(target_os = "android"),
169 not(target_os = "ios"),
170 feature = "file-picker-native"
171))]
172mod desktop;
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn folder_error_messages_are_distinct() {
180 assert_eq!(
181 FolderError::ReadOnly.to_string(),
182 "writable folder is read-only"
183 );
184 assert!(FolderError::NotFound("a.txt".into())
185 .to_string()
186 .contains("a.txt"));
187 }
188
189 #[test]
190 fn picker_registration_round_trips() {
191 struct Marker;
192 impl WritableFolderPicker for Marker {
193 fn pick(&self) -> PickerFuture<Result<Option<String>, FilePickerError>> {
194 Box::pin(async { Ok(Some("handle".to_string())) })
195 }
196 }
197 clear_platform_writable_folder_picker();
198 assert!(registered_picker().is_none());
199 set_platform_writable_folder_picker(Rc::new(Marker));
200 assert!(registered_picker().is_some());
201 let result = pollster::block_on(pick_writable_folder());
202 assert!(matches!(result, Ok(Some(handle)) if handle == "handle"));
203 clear_platform_writable_folder_picker();
204 }
205}