1use crate::utils::asynchronous::BoxPlatformSendFuture;
7#[cfg(target_arch = "wasm32")]
8use crate::utils::asynchronous::ThreadToken;
9#[cfg(target_arch = "wasm32")]
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use std::sync::Arc;
13
14#[cfg(feature = "json")]
15use serde::{Deserialize, Serialize};
16
17#[cfg(target_arch = "wasm32")]
19#[derive(Clone)]
20struct WebFileHandle {
21 id: u64,
22 rfd_handle: rfd::FileHandle,
23}
24
25#[cfg(target_arch = "wasm32")]
26impl From<rfd::FileHandle> for WebFileHandle {
27 fn from(handle: rfd::FileHandle) -> Self {
28 static NEXT_ID: AtomicU64 = AtomicU64::new(0);
29 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
30 WebFileHandle {
31 id,
32 rfd_handle: handle,
33 }
34 }
35}
36
37#[cfg(target_arch = "wasm32")]
38impl PartialEq for WebFileHandle {
39 fn eq(&self, other: &Self) -> bool {
40 self.id == other.id
41 }
42}
43
44#[derive(Clone)]
45struct PersistedAttachmentHandle {
46 reader: Arc<
47 dyn Fn(&str) -> BoxPlatformSendFuture<'static, std::io::Result<Arc<[u8]>>> + Send + Sync,
48 >,
49 key: String,
50}
51
52impl std::fmt::Debug for PersistedAttachmentHandle {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("PersistedAttachmentHandle")
55 .field("key", &self.key)
56 .field("reader", &format_args!("{:p}", Arc::as_ptr(&self.reader)))
57 .finish()
58 }
59}
60
61#[derive(Debug, Clone)]
65enum AttachmentContentHandle {
66 InMemory(Arc<[u8]>),
67 #[cfg(not(target_arch = "wasm32"))]
68 FilePick(std::path::PathBuf),
69 #[cfg(target_arch = "wasm32")]
70 FilePick(ThreadToken<WebFileHandle>),
71 ErasedPersisted(String),
72 Persisted(PersistedAttachmentHandle),
73}
74
75#[cfg(feature = "json")]
76impl serde::Serialize for AttachmentContentHandle {
77 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
78 where
79 S: serde::Serializer,
80 {
81 match self {
82 AttachmentContentHandle::ErasedPersisted(key) => serializer.serialize_str(key),
83 AttachmentContentHandle::Persisted(persisted) => {
84 serializer.serialize_str(&persisted.key)
85 }
86 _ => serializer.serialize_none(),
87 }
88 }
89}
90
91#[cfg(feature = "json")]
92impl<'de> serde::Deserialize<'de> for AttachmentContentHandle {
93 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
94 where
95 D: serde::Deserializer<'de>,
96 {
97 let opt_key: Option<String> = Option::deserialize(deserializer)?;
98 match opt_key {
99 Some(key) => Ok(AttachmentContentHandle::ErasedPersisted(key)),
100 None => Err(serde::de::Error::custom(
101 "AttachmentContentHandle cannot be deserialized from null",
102 )),
103 }
104 }
105}
106
107impl PartialEq for AttachmentContentHandle {
108 fn eq(&self, other: &Self) -> bool {
109 match (self, other) {
110 (AttachmentContentHandle::InMemory(a), AttachmentContentHandle::InMemory(b)) => {
111 Arc::ptr_eq(a, b)
112 }
113 #[cfg(not(target_arch = "wasm32"))]
114 (AttachmentContentHandle::FilePick(a), AttachmentContentHandle::FilePick(b)) => a == b,
115 #[cfg(target_arch = "wasm32")]
116 (AttachmentContentHandle::FilePick(a), AttachmentContentHandle::FilePick(b)) => {
117 let a_id = a.peek(|handle| handle.id);
118 let b_id = b.peek(|handle| handle.id);
119 a_id == b_id
120 }
121 (
122 AttachmentContentHandle::ErasedPersisted(a),
123 AttachmentContentHandle::ErasedPersisted(b),
124 ) => a == b,
125 (AttachmentContentHandle::Persisted(a), AttachmentContentHandle::Persisted(b)) => {
126 a.key == b.key
127 }
128
129 _ => false,
130 }
131 }
132}
133
134impl Eq for AttachmentContentHandle {}
135
136impl std::hash::Hash for AttachmentContentHandle {
137 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
138 match self {
139 AttachmentContentHandle::InMemory(content) => {
140 Arc::as_ptr(content).hash(state);
141 }
142 #[cfg(not(target_arch = "wasm32"))]
143 AttachmentContentHandle::FilePick(path) => path.hash(state),
144 #[cfg(target_arch = "wasm32")]
145 AttachmentContentHandle::FilePick(handle) => handle.peek(|h| h.id).hash(state),
146 AttachmentContentHandle::ErasedPersisted(key) => key.hash(state),
147 AttachmentContentHandle::Persisted(persisted) => persisted.key.hash(state),
148 }
149 }
150}
151
152impl From<&[u8]> for AttachmentContentHandle {
153 fn from(bytes: &[u8]) -> Self {
154 AttachmentContentHandle::InMemory(Arc::from(bytes))
155 }
156}
157
158#[cfg(not(target_arch = "wasm32"))]
159impl From<std::path::PathBuf> for AttachmentContentHandle {
160 fn from(path: std::path::PathBuf) -> Self {
161 AttachmentContentHandle::FilePick(path)
162 }
163}
164
165#[cfg(target_arch = "wasm32")]
166impl From<rfd::FileHandle> for AttachmentContentHandle {
167 fn from(handle: rfd::FileHandle) -> Self {
168 AttachmentContentHandle::FilePick(ThreadToken::new(WebFileHandle::from(handle)))
169 }
170}
171
172impl AttachmentContentHandle {
173 async fn read(&self) -> std::io::Result<Arc<[u8]>> {
174 match self {
175 AttachmentContentHandle::InMemory(content) => Ok(content.clone()),
176 #[cfg(not(target_arch = "wasm32"))]
177 AttachmentContentHandle::FilePick(path) => {
178 let content = tokio::fs::read(path).await?;
179 Ok(Arc::from(content))
180 }
181 #[cfg(target_arch = "wasm32")]
182 AttachmentContentHandle::FilePick(handle) => {
183 let handle = handle.clone_inner();
184 let content = handle.rfd_handle.read().await;
185 Ok(Arc::from(content))
186 }
187 AttachmentContentHandle::ErasedPersisted(_) => Err(std::io::Error::new(
188 std::io::ErrorKind::Other,
189 "Cannot read erased persisted attachment. Please restore the reader with `set_persistence_reader` first.",
190 )),
191 AttachmentContentHandle::Persisted(persisted) => {
192 (persisted.reader)(persisted.key.as_str()).await
193 }
194 }
195 }
196}
197
198#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
238#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
239pub struct Attachment {
240 pub name: String,
242 pub content_type: Option<String>,
244 content: Option<AttachmentContentHandle>,
245}
246
247const SUPPORTED_IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"];
249const SUPPORTED_DOCUMENT_EXTENSIONS: &[&str] = &["pdf", "txt", "md", "html", "htm"];
250const SUPPORTED_ALL_EXTENSIONS: &[&str] = &[
251 "png", "jpg", "jpeg", "gif", "webp", "bmp", "svg", "pdf", "txt", "md", "html", "htm", "xml", "json", "yaml", "yml", "csv", "log", "ini", "cfg",
254 "conf", "js", "ts", "tsx", "jsx", "py", "rs", "go", "java", "c", "cpp", "h", "hpp", "cs", "rb",
256 "php", "swift", "kt", "scala", "r", "m", "sql", "sh", "bash", "zsh", "fish", "ps1", "bat", "cmd",
258 "css", "scss", "sass", "less", "toml", "env", ];
260
261impl Attachment {
262 pub(crate) fn pick_multiple(cb: impl FnOnce(Result<Vec<Attachment>, ()>) + 'static) {
270 cfg_if::cfg_if! {
271 if #[cfg(target_arch = "wasm32")] {
272 crate::utils::asynchronous::spawn(async move {
273 let Some(handles) = rfd::AsyncFileDialog::new()
274 .add_filter("Supported Files", SUPPORTED_ALL_EXTENSIONS)
275 .add_filter("Images", SUPPORTED_IMAGE_EXTENSIONS)
276 .add_filter("Documents", SUPPORTED_DOCUMENT_EXTENSIONS)
277 .add_filter("All Files", &["*"])
278 .pick_files()
279 .await
280 else {
281 cb(Err(()));
282 return;
283 };
284
285 let mut attachments = Vec::with_capacity(handles.len());
286 for handle in handles {
287 let name = handle.file_name();
288 let content_type = mime_guess::from_path(&name)
289 .first()
290 .map(|m| m.to_string());
291 attachments.push(Attachment {
292 name,
293 content_type,
294 content: Some(handle.into()),
295 });
296 }
297
298 cb(Ok(attachments));
299 });
300 } else if #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] {
301 let Some(paths) = rfd::FileDialog::new()
302 .add_filter("Supported Files", SUPPORTED_ALL_EXTENSIONS)
303 .add_filter("Images", SUPPORTED_IMAGE_EXTENSIONS)
304 .add_filter("Documents", SUPPORTED_DOCUMENT_EXTENSIONS)
305 .add_filter("All Files", &["*"])
306 .pick_files()
307 else {
308 cb(Err(()));
309 return;
310 };
311
312 let mut attachments = Vec::with_capacity(paths.len());
313 for path in paths {
314 let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
315 let content_type = mime_guess::from_path(&name)
316 .first()
317 .map(|m| m.to_string());
318
319 attachments.push(Attachment {
320 name,
321 content_type,
322 content: Some(path.into()),
323 });
324 }
325 cb(Ok(attachments));
326 } else {
327 ::log::warn!("Attachment picking is not supported on this platform");
328 cb(Err(()));
329 }
330 }
331 }
332
333 pub fn from_bytes(name: String, content_type: Option<String>, content: &[u8]) -> Self {
335 Attachment {
336 name,
337 content_type,
338 content: Some(content.into()),
339 }
340 }
341
342 pub fn from_base64(
344 name: String,
345 content_type: Option<String>,
346 base64_content: &str,
347 ) -> std::io::Result<Self> {
348 use base64::Engine;
349 let content = base64::engine::general_purpose::STANDARD
350 .decode(base64_content)
351 .map_err(|_| {
352 std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid base64 content")
353 })?;
354
355 Ok(Attachment::from_bytes(name, content_type, &content))
356 }
357
358 pub fn is_available(&self) -> bool {
359 self.content.is_some()
360 }
361
362 pub fn is_image(&self) -> bool {
363 if let Some(content_type) = &self.content_type {
364 content_type.starts_with("image/")
365 } else {
366 false
367 }
368 }
369
370 pub fn is_pdf(&self) -> bool {
371 self.content_type.as_deref() == Some("application/pdf")
372 }
373
374 pub async fn read(&self) -> std::io::Result<Arc<[u8]>> {
375 if let Some(content) = &self.content {
376 content.read().await
377 } else {
378 Err(std::io::Error::new(
379 std::io::ErrorKind::NotFound,
380 "Attachment content not available",
381 ))
382 }
383 }
384
385 pub async fn read_base64(&self) -> std::io::Result<String> {
386 use base64::Engine;
387 let content = self.read().await?;
388 Ok(base64::engine::general_purpose::STANDARD.encode(content))
389 }
390
391 pub(crate) fn save(&self) {
393 ::log::info!("Downloading attachment: {}", self.name);
394
395 if self.content.is_none() {
396 ::log::warn!("Attachment content not available for saving: {}", self.name);
397 return;
398 }
399
400 self.save_impl();
401 }
402
403 #[cfg(target_arch = "wasm32")]
404 fn save_impl(&self) {
405 let self_clone = self.clone();
406 crate::utils::asynchronous::spawn(async move {
407 let Ok(content) = self_clone.content.as_ref().unwrap().read().await else {
408 ::log::warn!(
409 "Failed to read attachment content for saving: {}",
410 self_clone.name
411 );
412 return;
413 };
414
415 use crate::utils::platform::{create_scoped_blob_url, trigger_download};
416 create_scoped_blob_url(&content, self_clone.content_type.as_deref(), |url| {
417 trigger_download(url, &self_clone.name);
418 });
419 });
420 }
421
422 #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
423 fn save_impl(&self) {
424 let content_handle = self.content.as_ref().unwrap();
425
426 let content = match futures::executor::block_on(content_handle.read()) {
430 Ok(content) => content,
431 Err(err) => {
432 ::log::warn!(
433 "Failed to read attachment content for saving {}: {}",
434 self.name,
435 err
436 );
437 return;
438 }
439 };
440
441 crate::utils::platform::trigger_save_as(&content, Some(self.name.as_str()));
442 }
443
444 #[cfg(not(any(
445 target_arch = "wasm32",
446 target_os = "windows",
447 target_os = "macos",
448 target_os = "linux"
449 )))]
450 fn save_impl(&self) {
451 ::log::warn!("Attachment saving is not supported on this platform");
452 }
453
454 pub fn content_type_or_octet_stream(&self) -> &str {
456 self.content_type
457 .as_deref()
458 .unwrap_or("application/octet-stream")
459 }
460
461 pub fn get_persistence_key(&self) -> Option<&str> {
463 match &self.content {
464 Some(AttachmentContentHandle::Persisted(persisted)) => Some(&persisted.key),
465 Some(AttachmentContentHandle::ErasedPersisted(key)) => Some(key),
466 _ => None,
467 }
468 }
469
470 pub fn has_persistence_key(&self) -> bool {
472 self.get_persistence_key().is_some()
473 }
474
475 pub fn has_persistence_reader(&self) -> bool {
477 matches!(&self.content, Some(AttachmentContentHandle::Persisted(_)))
478 }
479
480 pub fn set_persistence_key(&mut self, key: String) {
490 match &self.content {
491 Some(AttachmentContentHandle::Persisted(persisted)) => {
492 self.content = Some(AttachmentContentHandle::Persisted(
493 PersistedAttachmentHandle {
494 reader: persisted.reader.clone(),
495 key,
496 },
497 ));
498 }
499 Some(AttachmentContentHandle::ErasedPersisted(_)) => {
500 self.content = Some(AttachmentContentHandle::ErasedPersisted(key));
501 }
502 _ => {
503 self.content = Some(AttachmentContentHandle::ErasedPersisted(key));
504 }
505 }
506 }
507
508 pub fn set_persistence_reader(
512 &mut self,
513 reader: impl Fn(&str) -> BoxPlatformSendFuture<'static, std::io::Result<Arc<[u8]>>>
514 + Send
515 + Sync
516 + 'static,
517 ) {
518 match &self.content {
519 Some(AttachmentContentHandle::Persisted(persisted)) => {
520 self.content = Some(AttachmentContentHandle::Persisted(
521 PersistedAttachmentHandle {
522 reader: Arc::new(reader),
523 key: persisted.key.clone(),
524 },
525 ));
526 }
527 Some(AttachmentContentHandle::ErasedPersisted(key)) => {
528 self.content = Some(AttachmentContentHandle::Persisted(
529 PersistedAttachmentHandle {
530 reader: Arc::new(reader),
531 key: key.clone(),
532 },
533 ));
534 }
535 _ => {
536 ::log::warn!("Cannot set persistence reader on a non-persisted attachment");
537 }
538 }
539 }
540}