Skip to main content

rlobkit_core/
lib.rs

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}