1pub mod error;
2pub mod paths;
3
4pub use error::RlobKitError;
5
6use bytes::Bytes;
7use std::path::{Path, PathBuf};
8use std::sync::OnceLock;
9
10#[cfg(all(feature = "tokio-runtime", not(target_arch = "wasm32")))]
11use tokio::io::AsyncReadExt;
12
13pub type AndroidReadBytes = fn(&str) -> Result<Bytes, RlobKitError>;
14pub type AndroidWriteBytes = fn(&str, &[u8]) -> Result<(), RlobKitError>;
15
16static ANDROID_READ: OnceLock<AndroidReadBytes> = OnceLock::new();
17static ANDROID_WRITE: OnceLock<AndroidWriteBytes> = OnceLock::new();
18
19pub fn set_android_io(read: AndroidReadBytes, write: AndroidWriteBytes) {
20 let _ = ANDROID_READ.set(read);
21 let _ = ANDROID_WRITE.set(write);
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum FileSource {
26 Path(PathBuf),
27 #[cfg(target_os = "android")]
28 Uri(String),
29 Bytes(Bytes),
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct PlatformFile {
34 name: String,
35 source: FileSource,
36 size: Option<u64>,
37 mime_type: Option<String>,
38}
39
40impl PlatformFile {
41 pub fn from_path(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
42 Self {
43 name: name.into(),
44 source: FileSource::Path(path.into()),
45 size: None,
46 mime_type: None,
47 }
48 }
49
50 #[cfg(target_os = "android")]
51 pub fn from_uri(
52 name: impl Into<String>,
53 uri: impl Into<String>,
54 size: Option<u64>,
55 mime_type: Option<String>,
56 ) -> Self {
57 Self {
58 name: name.into(),
59 source: FileSource::Uri(uri.into()),
60 size,
61 mime_type,
62 }
63 }
64
65 #[cfg(target_arch = "wasm32")]
66 pub fn from_blob(name: impl Into<String>, data: Bytes, mime_type: Option<String>) -> Self {
67 let size = Some(data.len() as u64);
68 Self {
69 name: name.into(),
70 source: FileSource::Bytes(data),
71 size,
72 mime_type,
73 }
74 }
75
76 pub fn name(&self) -> &str {
77 &self.name
78 }
79
80 pub fn extension(&self) -> Option<&str> {
81 std::path::Path::new(&self.name)
82 .extension()
83 .and_then(|e| e.to_str())
84 }
85
86 pub fn mime_type(&self) -> Option<String> {
87 if let Some(mime) = &self.mime_type {
88 return Some(mime.clone());
89 }
90 let ext = self.extension()?;
91 Some(
92 mime_guess::from_ext(ext)
93 .first_or_octet_stream()
94 .to_string(),
95 )
96 }
97
98 pub fn source(&self) -> &FileSource {
99 &self.source
100 }
101
102 pub fn path(&self) -> Option<&Path> {
103 match &self.source {
104 FileSource::Path(p) => Some(p),
105 _ => None,
106 }
107 }
108
109 pub fn uri(&self) -> Option<&str> {
110 #[cfg(target_os = "android")]
111 {
112 if let FileSource::Uri(u) = &self.source {
113 return Some(u);
114 }
115 }
116 None
117 }
118
119 pub fn data(&self) -> Option<&Bytes> {
120 match &self.source {
121 FileSource::Bytes(b) => Some(b),
122 _ => None,
123 }
124 }
125
126 pub fn size(&self) -> Option<u64> {
127 self.size
128 }
129
130 pub fn read_bytes(&self) -> Result<Bytes, RlobKitError> {
131 match &self.source {
132 FileSource::Path(p) => Ok(Bytes::from(std::fs::read(p)?)),
133 FileSource::Bytes(b) => Ok(b.clone()),
134 #[cfg(target_os = "android")]
135 FileSource::Uri(u) => {
136 let reader = ANDROID_READ.get().ok_or_else(|| {
137 RlobKitError::UnsupportedOperation(
138 "Android I/O not initialized; call rlobkit_dialogs::init()".into(),
139 )
140 })?;
141 reader(u)
142 }
143 }
144 }
145
146 #[cfg(all(feature = "tokio-runtime", not(target_arch = "wasm32")))]
147 pub async fn read_bytes_async(&self) -> Result<Bytes, RlobKitError> {
148 match &self.source {
149 FileSource::Path(p) => {
150 let mut file = tokio::fs::File::open(p).await?;
151 let mut buffer = Vec::new();
152 file.read_to_end(&mut buffer).await?;
153 Ok(Bytes::from(buffer))
154 }
155 FileSource::Bytes(b) => Ok(b.clone()),
156 #[cfg(target_os = "android")]
157 FileSource::Uri(u) => {
158 let uri = u.clone();
159 let reader = ANDROID_READ.get().ok_or_else(|| {
160 RlobKitError::UnsupportedOperation(
161 "Android I/O not initialized; call rlobkit_dialogs::init()".into(),
162 )
163 })?;
164 tokio::task::spawn_blocking(move || reader(&uri))
165 .await
166 .map_err(|e| {
167 RlobKitError::UnsupportedOperation(format!("join error: {e}"))
168 })?
169 }
170 }
171 }
172
173 #[cfg(target_arch = "wasm32")]
174 pub async fn read_bytes_async(&self) -> Result<Bytes, RlobKitError> {
175 self.read_bytes()
176 }
177
178 pub fn write_bytes(&self, data: &[u8]) -> Result<(), RlobKitError> {
179 match &self.source {
180 FileSource::Path(p) => {
181 std::fs::write(p, data)?;
182 Ok(())
183 }
184 FileSource::Bytes(_) => Err(RlobKitError::UnsupportedOperation(
185 "Writing to an in-memory blob is not supported".into(),
186 )),
187 #[cfg(target_os = "android")]
188 FileSource::Uri(u) => {
189 let writer = ANDROID_WRITE.get().ok_or_else(|| {
190 RlobKitError::UnsupportedOperation(
191 "Android I/O not initialized; call rlobkit_dialogs::init()".into(),
192 )
193 })?;
194 writer(u, data)
195 }
196 }
197 }
198
199 pub fn write_string(&self, s: &str) -> Result<(), RlobKitError> {
200 self.write_bytes(s.as_bytes())
201 }
202
203 pub fn builder(name: impl Into<String>) -> PlatformFileBuilder {
204 PlatformFileBuilder {
205 name: name.into(),
206 source: None,
207 size: None,
208 mime_type: None,
209 }
210 }
211}
212
213#[derive(Debug, Clone, Default)]
214pub struct PlatformFileBuilder {
215 name: String,
216 source: Option<FileSource>,
217 size: Option<u64>,
218 mime_type: Option<String>,
219}
220
221impl PlatformFileBuilder {
222 pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
223 self.source = Some(FileSource::Path(path.into()));
224 self
225 }
226
227 #[cfg(target_os = "android")]
228 pub fn uri(mut self, uri: impl Into<String>) -> Self {
229 self.source = Some(FileSource::Uri(uri.into()));
230 self
231 }
232
233 pub fn data(mut self, data: Bytes) -> Self {
234 let size = Some(data.len() as u64);
235 self.source = Some(FileSource::Bytes(data));
236 self.size = size;
237 self
238 }
239
240 pub fn size(mut self, size: u64) -> Self {
241 self.size = Some(size);
242 self
243 }
244
245 pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
246 self.mime_type = Some(mime.into());
247 self
248 }
249
250 pub fn build(self) -> Result<PlatformFile, RlobKitError> {
251 let source = self.source.ok_or_else(|| {
252 RlobKitError::UnsupportedOperation(
253 "PlatformFile must have a source (path, uri, or data)".into(),
254 )
255 })?;
256 Ok(PlatformFile {
257 name: self.name,
258 source,
259 size: self.size,
260 mime_type: self.mime_type,
261 })
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub enum DirectorySource {
267 Path(PathBuf),
268 #[cfg(target_os = "android")]
269 Uri(String),
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
273pub struct PlatformDirectory {
274 source: DirectorySource,
275}
276
277impl PlatformDirectory {
278 pub fn new(path: impl Into<PathBuf>) -> Self {
279 Self {
280 source: DirectorySource::Path(path.into()),
281 }
282 }
283
284 #[cfg(target_os = "android")]
285 pub fn from_uri(uri: impl Into<String>) -> Self {
286 Self {
287 source: DirectorySource::Uri(uri.into()),
288 }
289 }
290
291 pub fn source(&self) -> &DirectorySource {
292 &self.source
293 }
294
295 pub fn path(&self) -> Option<&Path> {
296 match &self.source {
297 DirectorySource::Path(p) => Some(p),
298 #[cfg(target_os = "android")]
299 _ => None,
300 }
301 }
302
303 pub fn uri(&self) -> Option<&str> {
304 #[cfg(target_os = "android")]
305 {
306 if let DirectorySource::Uri(u) = &self.source {
307 return Some(u);
308 }
309 }
310 None
311 }
312
313 pub fn name(&self) -> Option<String> {
314 match &self.source {
315 DirectorySource::Path(p) => p.file_name()?.to_str().map(String::from),
316 #[cfg(target_os = "android")]
317 DirectorySource::Uri(u) => {
318 let trimmed = u.trim_end_matches('/');
319 trimmed.rsplit('/').next().map(|s| s.to_string())
320 }
321 }
322 }
323
324 pub fn file(&self, name: &str) -> PlatformFile {
325 match &self.source {
326 DirectorySource::Path(p) => PlatformFile::from_path(name, p.join(name)),
327 #[cfg(target_os = "android")]
328 DirectorySource::Uri(u) => {
329 let base = u.trim_end_matches('/');
330 let uri = format!("{}/{}", base, name);
331 PlatformFile::from_uri(name, uri, None, None)
332 }
333 }
334 }
335
336 #[cfg(not(target_arch = "wasm32"))]
337 pub fn list_files(&self) -> Result<Vec<PlatformFile>, RlobKitError> {
338 match &self.source {
339 DirectorySource::Path(p) => {
340 let mut files = Vec::new();
341 for entry in std::fs::read_dir(p)? {
342 let entry = entry?;
343 if entry.file_type()?.is_file() {
344 let path = entry.path();
345 let name = path
346 .file_name()
347 .and_then(|n| n.to_str())
348 .unwrap_or("")
349 .to_string();
350 files.push(PlatformFile::from_path(name, path));
351 }
352 }
353 Ok(files)
354 }
355 #[cfg(target_os = "android")]
356 DirectorySource::Uri(_) => Err(RlobKitError::UnsupportedOperation(
357 "Listing directory contents via SAF URI is not yet supported".into(),
358 )),
359 }
360 }
361}
362
363impl std::ops::Div<&str> for &PlatformDirectory {
364 type Output = PlatformFile;
365 fn div(self, rhs: &str) -> PlatformFile {
366 self.file(rhs)
367 }
368}
369
370pub fn mime_to_extension(mime: &str) -> Option<&'static str> {
371 if let Some(extensions) = mime_guess::get_mime_extensions_str(mime)
372 && let Some(ext) = extensions.first()
373 {
374 return Some(ext);
375 }
376 match mime {
377 "application/x-clap" => Some("clap"),
378 _ => None,
379 }
380}