use std::{
io::Write,
path::{Path, PathBuf},
};
use crate::constants::media_dir;
const MAX_DEDUP_INDEX: u32 = 100;
pub fn detect_mime(file_name: Option<&str>, buffer: &[u8]) -> String {
if let Some(name) = file_name {
let ext = Path::new(name)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let mime = mime_guess::from_ext(ext).first_or_octet_stream();
if mime != mime::APPLICATION_OCTET_STREAM {
return mime.to_string();
}
}
if let Some(kind) = infer::get(buffer) {
return kind.mime_type().to_string();
}
"application/octet-stream".to_string()
}
pub async fn save_media(
media_name: Option<&str>,
media_id: Option<&str>,
content_type: &str,
data: &[u8],
) -> anyhow::Result<PathBuf> {
let dir = media_dir();
tokio::fs::create_dir_all(&dir).await?;
let (stem, ext) = determine_file_name(media_name, media_id, content_type);
let mut tmp = tempfile::NamedTempFile::new_in(&dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
tmp.as_file()
.set_permissions(std::fs::Permissions::from_mode(0o600))?;
}
tmp.write_all(data)?;
tmp.as_file().sync_all()?;
let target = dir.join(make_file_name(&stem, None, &ext));
tmp = match tmp.persist_noclobber(&target) {
Ok(_) => return Ok(target),
Err(e) if e.error.kind() == std::io::ErrorKind::AlreadyExists => e.file,
Err(e) => return Err(e.error.into()),
};
for idx in 0..MAX_DEDUP_INDEX {
let target = dir.join(make_file_name(&stem, Some(idx), &ext));
tmp = match tmp.persist_noclobber(&target) {
Ok(_) => return Ok(target),
Err(e) if e.error.kind() == std::io::ErrorKind::AlreadyExists => e.file,
Err(e) => return Err(e.error.into()),
};
}
anyhow::bail!(
"媒体文件保存失败,目标文件已存在:{}",
target.to_string_lossy()
);
}
fn make_file_name(stem: &str, index: Option<u32>, ext: &str) -> String {
match (index, ext.is_empty()) {
(None, true) => stem.to_string(),
(None, false) => format!("{stem}.{ext}"),
(Some(i), true) => format!("{stem}.{i}"),
(Some(i), false) => format!("{stem}.{i}.{ext}"),
}
}
pub fn determine_file_name(
media_name: Option<&str>,
media_id: Option<&str>,
content_type: &str,
) -> (String, String) {
if let Some(name) = media_name.and_then(sanitize_file_name) {
let path = Path::new(&name);
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(&name)
.to_string();
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_string();
return (stem, ext);
}
let stem = media_id
.and_then(sanitize_file_name)
.unwrap_or_else(|| "media".to_string());
let ext = mime_guess::get_mime_extensions_str(content_type)
.and_then(|exts| exts.first())
.copied()
.unwrap_or("bin")
.to_string();
(stem, ext)
}
fn sanitize_file_name(name: &str) -> Option<String> {
let options = sanitize_filename::Options {
truncate: true,
windows: true,
replacement: "_",
};
let sanitized = sanitize_filename::sanitize_with_options(name, options);
if sanitized.is_empty() {
None
} else {
Some(sanitized)
}
}