1use std::path::{Path, PathBuf};
4
5use base64::{engine::general_purpose, Engine as _};
6use thiserror::Error;
7
8#[derive(Debug, Error)]
10pub enum MediaError {
11 #[error("HTTP error: {0}")]
12 Http(#[from] reqwest::Error),
13 #[error("IO error: {0}")]
14 Io(#[from] std::io::Error),
15 #[error("Invalid media content: empty or corrupted")]
16 InvalidContent,
17}
18
19pub async fn download_media(
23 url: &str,
24 filename: Option<&str>,
25 save_dir: &PathBuf,
26) -> Result<PathBuf, MediaError> {
27 tokio::fs::create_dir_all(save_dir).await?;
29
30 let save_filename = match filename {
32 Some(s) => s.to_string(),
33 None => uuid::Uuid::new_v4().to_string(),
34 };
35 let save_path = save_dir.join(save_filename);
36
37 let client = reqwest::Client::new();
39 let response = client.get(url).send().await?;
40 let bytes = response.bytes().await?;
41
42 if bytes.is_empty() {
43 return Err(MediaError::InvalidContent);
44 }
45
46 tokio::fs::write(&save_path, &bytes).await?;
47
48 Ok(save_path)
49}
50
51pub async fn download_and_encode_base64(
55 url: &str,
56 content_type: &str,
57) -> Result<String, MediaError> {
58 let client = reqwest::Client::new();
59 let bytes = client.get(url).send().await?.bytes().await?;
60
61 if bytes.is_empty() {
62 return Err(MediaError::InvalidContent);
63 }
64
65 let base64 = general_purpose::STANDARD.encode(&bytes);
66 Ok(format!("data:{};base64,{}", content_type, base64))
67}
68
69pub async fn encode_file_base64(path: &Path) -> Result<String, MediaError> {
74 let bytes = tokio::fs::read(path).await?;
75
76 if bytes.is_empty() {
77 return Err(MediaError::InvalidContent);
78 }
79
80 let mime_type = mime_from_extension(path);
81 let base64 = general_purpose::STANDARD.encode(&bytes);
82 Ok(format!("data:{};base64,{}", mime_type, base64))
83}
84
85fn mime_from_extension(path: &Path) -> &'static str {
89 match path
90 .extension()
91 .and_then(|e| e.to_str())
92 .map(|e| e.to_ascii_lowercase())
93 .as_deref()
94 {
95 Some("png") => "image/png",
96 Some("jpg") | Some("jpeg") => "image/jpeg",
97 Some("gif") => "image/gif",
98 Some("webp") => "image/webp",
99 _ => "application/octet-stream",
100 }
101}