use std::path::{Path, PathBuf};
use base64::{engine::general_purpose, Engine as _};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MediaError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid media content: empty or corrupted")]
InvalidContent,
}
pub async fn download_media(
url: &str,
filename: Option<&str>,
save_dir: &PathBuf,
) -> Result<PathBuf, MediaError> {
tokio::fs::create_dir_all(save_dir).await?;
let save_filename = match filename {
Some(s) => s.to_string(),
None => uuid::Uuid::new_v4().to_string(),
};
let save_path = save_dir.join(save_filename);
let client = reqwest::Client::new();
let response = client.get(url).send().await?;
let bytes = response.bytes().await?;
if bytes.is_empty() {
return Err(MediaError::InvalidContent);
}
tokio::fs::write(&save_path, &bytes).await?;
Ok(save_path)
}
pub async fn download_and_encode_base64(
url: &str,
content_type: &str,
) -> Result<String, MediaError> {
let client = reqwest::Client::new();
let bytes = client.get(url).send().await?.bytes().await?;
if bytes.is_empty() {
return Err(MediaError::InvalidContent);
}
let base64 = general_purpose::STANDARD.encode(&bytes);
Ok(format!("data:{};base64,{}", content_type, base64))
}
pub async fn encode_file_base64(path: &Path) -> Result<String, MediaError> {
let bytes = tokio::fs::read(path).await?;
if bytes.is_empty() {
return Err(MediaError::InvalidContent);
}
let mime_type = mime_from_extension(path);
let base64 = general_purpose::STANDARD.encode(&bytes);
Ok(format!("data:{};base64,{}", mime_type, base64))
}
fn mime_from_extension(path: &Path) -> &'static str {
match path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.as_deref()
{
Some("png") => "image/png",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("gif") => "image/gif",
Some("webp") => "image/webp",
_ => "application/octet-stream",
}
}