1pub mod error;
4pub mod paths;
5
6pub use error::RlobKitError;
7
8use bytes::Bytes;
9use std::path::{Path, PathBuf};
10use std::sync::OnceLock;
11
12#[cfg(all(feature = "tokio-runtime", not(target_arch = "wasm32")))]
13use tokio::io::AsyncReadExt;
14
15pub type AndroidReadBytes = fn(&str) -> Result<Bytes, RlobKitError>;
21pub type AndroidWriteBytes = fn(&str, &[u8]) -> Result<(), RlobKitError>;
22
23static ANDROID_READ: OnceLock<AndroidReadBytes> = OnceLock::new();
24static ANDROID_WRITE: OnceLock<AndroidWriteBytes> = OnceLock::new();
25
26pub fn set_android_io(read: AndroidReadBytes, write: AndroidWriteBytes) {
30 let _ = ANDROID_READ.set(read);
31 let _ = ANDROID_WRITE.set(write);
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct PlatformFile {
36 name: String,
37 path: Option<PathBuf>,
38 uri: Option<String>,
39 data: Option<Bytes>,
40 size: Option<u64>,
41 mime_type: Option<String>,
45}
46
47impl PlatformFile {
48 pub fn from_path(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
49 Self {
50 name: name.into(),
51 path: Some(path.into()),
52 uri: None,
53 data: None,
54 size: None,
55 mime_type: None,
56 }
57 }
58
59 #[cfg(target_os = "android")]
60 pub fn from_uri(
61 name: impl Into<String>,
62 uri: impl Into<String>,
63 size: Option<u64>,
64 mime_type: Option<String>,
65 ) -> Self {
66 Self {
67 name: name.into(),
68 path: None,
69 uri: Some(uri.into()),
70 data: None,
71 size,
72 mime_type,
73 }
74 }
75
76 #[cfg(target_arch = "wasm32")]
77 pub fn from_blob(name: impl Into<String>, data: Bytes, mime_type: Option<String>) -> Self {
78 let size = Some(data.len() as u64);
79 Self {
80 name: name.into(),
81 path: None,
82 uri: None,
83 data: Some(data),
84 size,
85 mime_type,
86 }
87 }
88
89 pub fn name(&self) -> &str {
91 &self.name
92 }
93
94 pub fn extension(&self) -> Option<&str> {
96 std::path::Path::new(&self.name)
97 .extension()
98 .and_then(|e| e.to_str())
99 }
100
101 pub fn mime_type(&self) -> Option<String> {
105 if let Some(mime) = &self.mime_type {
106 return Some(mime.clone());
107 }
108 let ext = self.extension()?;
109 Some(
110 mime_guess::from_ext(ext)
111 .first_or_octet_stream()
112 .to_string(),
113 )
114 }
115
116 pub fn path(&self) -> Option<&Path> {
117 self.path.as_deref()
118 }
119
120 pub fn uri(&self) -> Option<&str> {
123 self.uri.as_deref()
124 }
125
126 pub fn data(&self) -> Option<&Bytes> {
129 self.data.as_ref()
130 }
131
132 pub fn size(&self) -> Option<u64> {
135 self.size
136 }
137
138 pub fn read_bytes(&self) -> Result<Bytes, RlobKitError> {
139 if let Some(p) = &self.path {
140 return Ok(Bytes::from(std::fs::read(p)?));
141 }
142 if let Some(u) = &self.uri {
143 let reader = ANDROID_READ.get().ok_or_else(|| {
144 RlobKitError::UnsupportedOperation(
145 "Android I/O not initialized; call rlobkit_dialogs::init()".into(),
146 )
147 })?;
148 return reader(u);
149 }
150 if let Some(d) = &self.data {
151 return Ok(d.clone());
152 }
153 Err(RlobKitError::UnsupportedOperation(
154 "PlatformFile has no readable source".into(),
155 ))
156 }
157
158 #[cfg(all(feature = "tokio-runtime", not(target_arch = "wasm32")))]
159 pub async fn read_bytes_async(&self) -> Result<Bytes, RlobKitError> {
160 if let Some(p) = &self.path {
161 let mut file = tokio::fs::File::open(p).await?;
162 let mut buffer = Vec::new();
163 file.read_to_end(&mut buffer).await?;
164 return Ok(Bytes::from(buffer));
165 }
166 if let Some(u) = &self.uri {
167 let reader = ANDROID_READ.get().ok_or_else(|| {
168 RlobKitError::UnsupportedOperation(
169 "Android I/O not initialized; call rlobkit_dialogs::init()".into(),
170 )
171 })?;
172 return reader(u);
173 }
174 Err(RlobKitError::UnsupportedOperation(
175 "PlatformFile has no readable source".into(),
176 ))
177 }
178
179 #[cfg(target_arch = "wasm32")]
180 pub async fn read_bytes_async(&self) -> Result<Bytes, RlobKitError> {
181 self.read_bytes()
182 }
183
184 pub fn write_bytes(&self, data: &[u8]) -> Result<(), RlobKitError> {
185 if let Some(p) = &self.path {
186 std::fs::write(p, data)?;
187 return Ok(());
188 }
189 if let Some(u) = &self.uri {
190 let writer = ANDROID_WRITE.get().ok_or_else(|| {
191 RlobKitError::UnsupportedOperation(
192 "Android I/O not initialized; call rlobkit_dialogs::init()".into(),
193 )
194 })?;
195 return writer(u, data);
196 }
197 if self.data.is_some() {
198 return Err(RlobKitError::UnsupportedOperation(
199 "Writing to an in-memory blob is not supported".into(),
200 ));
201 }
202 Err(RlobKitError::UnsupportedOperation(
203 "PlatformFile has no writable destination".into(),
204 ))
205 }
206
207 pub fn write_string(&self, s: &str) -> Result<(), RlobKitError> {
208 self.write_bytes(s.as_bytes())
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct PlatformDirectory {
214 path: PathBuf,
215}
216
217impl PlatformDirectory {
218 pub fn new(path: impl Into<PathBuf>) -> Self {
219 Self { path: path.into() }
220 }
221
222 pub fn path(&self) -> &Path {
223 &self.path
224 }
225
226 pub fn name(&self) -> Option<String> {
227 self.path.file_name()?.to_str().map(String::from)
228 }
229
230 pub fn file(&self, name: &str) -> PlatformFile {
231 PlatformFile::from_path(name, self.path.join(name))
232 }
233
234 #[cfg(not(target_arch = "wasm32"))]
235 pub fn list_files(&self) -> Result<Vec<PlatformFile>, RlobKitError> {
236 let mut files = Vec::new();
237 for entry in std::fs::read_dir(&self.path)? {
238 let entry = entry?;
239 if entry.file_type()?.is_file() {
240 let path = entry.path();
241 let name = path
242 .file_name()
243 .and_then(|n| n.to_str())
244 .unwrap_or("")
245 .to_string();
246 files.push(PlatformFile::from_path(name, path));
247 }
248 }
249 Ok(files)
250 }
251}
252
253impl std::ops::Div<&str> for &PlatformDirectory {
254 type Output = PlatformFile;
255 fn div(self, rhs: &str) -> PlatformFile {
256 self.file(rhs)
257 }
258}
259
260pub fn mime_to_extension(mime: &str) -> Option<&'static str> {
262 if let Some(extensions) = mime_guess::get_mime_extensions_str(mime) {
263 if let Some(ext) = extensions.first() {
264 return Some(ext);
265 }
266 }
267 match mime {
268 "application/x-clap" => Some("clap"),
269 _ => None,
270 }
271}