文件存储
大约 3 分钟
文件存储
仅保留当前本地文件存储实现:支持 Base64 图片保存与 Multipart 文件上传,并提供删除接口。不会涉及 S3/OSS 或 Nginx 配置。
- 目录按月分片:YYYY-MM
- 文件命名:日-dd + 全局 ID(GID)+ 扩展名
- 路径配置:
- 静态图保存目录:APPCOFIG.server.static_dir
- 普通上传目录:APPCOFIG.server.upload_dir(默认 data/upload)
- 返回 URL 前缀:APPCOFIG.server.domainname
路径规范
- Base64 图片保存
- 物理路径:{static_dir}/{YYYY-MM}/{dd}_{GID}.png
- 返回:(url_path, no_domain_path)
- url_path:{domainname}/{static_dir 去掉 data/ 前缀}/{YYYY-MM}/
- no_domain_path:{static_dir 去掉 data/ 前缀}/{YYYY-MM}/
- Multipart 上传
- 物理路径:{upload_dir}/{YYYY-MM}/{dd}-{GID}.
- 返回:{domainname}/{YYYY-MM}/
说明:
- 如果 static_dir 以 data/ 开头,生成 URL 时会去掉 data/ 前缀,便于直接以相对路径访问。
- 删除时会把传入的 URL 用 domainname 替换为 upload_dir 对应的物理路径进行删除。
参考实现(与现有代码一致)
必要导入:
use axum::extract::Multipart;
use tokio::{fs, io::AsyncWriteExt};
use base64::{engine::general_purpose, Engine};
use crate::config::APPCOFIG;
use crate::common::error::Result;
// 你项目中的全局唯一 ID 生成器
async fn GID() -> String { /* ... */ "123456".into() }
Base64 图片保存:
pub async fn save_base64_img(base64_data: &str) -> Result<(String, String)> {
let cleaned_str = remove_prefix(base64_data);
let server_config = APPCOFIG.server.clone();
let now = chrono::Local::now();
let file_path_t = format!("{}/{}", server_config.static_dir.clone(), &now.format("%Y-%m"));
fs::create_dir_all(&file_path_t).await?;
let fid = GID().await;
let file_name = format!("{}_{}{}", now.format("%d"), fid, ".png");
let file_path = format!("{}/{}", file_path_t, &file_name);
let decoded_data = general_purpose::STANDARD.decode(cleaned_str)?;
let mut file = fs::File::create(file_path).await?;
file.write_all(&decoded_data).await?;
let static_dir = if server_config.static_dir.starts_with("data/") {
&server_config.static_dir["data/".len()..]
} else {
server_config.static_dir.as_str()
};
let url_path = format!("{}/{}/{}/{}", server_config.domainname, static_dir, &now.format("%Y-%m"), &file_name);
let no_domain_path = format!("{}/{}/{}", static_dir, &now.format("%Y-%m"), &file_name);
Ok((url_path, no_domain_path))
}
fn remove_prefix(s: &str) -> &str {
if let Some(send) = s.strip_prefix("data:image/png;base64,") { send } else { s }
}
Multipart 文件上传:
pub async fn upload_file(mut multipart: Multipart) -> Result<String> {
if let Some(field) = multipart.next_field().await? {
let server_config = APPCOFIG.server.clone();
let content_type = field.content_type().map(ToString::to_string).unwrap_or_default();
let old_url = field.file_name().map(ToString::to_string).unwrap_or_default();
let file_type = get_file_type(&content_type);
let bytes = field.bytes().await?;
let now = chrono::Local::now();
let file_path_t = format!("{}/{}", server_config.upload_dir, &now.format("%Y-%m"));
let url_path_t = format!("{}/{}", server_config.domainname, &now.format("%Y-%m"));
fs::create_dir_all(&file_path_t).await?;
let fid = GID().await;
let file_name = format!("{}-{}{}", now.format("%d"), fid, &file_type);
let file_path = format!("{}/{}", file_path_t, &file_name);
let url_path = format!("{}/{}", url_path_t, &file_name);
let mut file = fs::File::create(&file_path).await?;
file.write_all(&bytes).await?;
if !old_url.is_empty() {
self::delete_file(&old_url).await;
}
Ok(url_path)
} else {
Err("Failed to upload file".into())
}
}
删除文件:
pub async fn delete_file(file_path: &str) {
let server_config = APPCOFIG.server.clone();
let path = file_path.replace(&server_config.domainname, &server_config.upload_dir);
if let Err(_) = fs::remove_file(&path).await {
tracing::error!("File deletion failed:{}", path);
}
}
MIME → 扩展名:
fn get_file_type(content_type: &str) -> String {
match content_type {
"image/jpeg" => ".jpg".to_string(),
"image/png" => ".png".to_string(),
"image/gif" => ".gif".to_string(),
_ => "".to_string(),
}
}
Base64 图片(富文本粘贴/截图):
// 将 data:image/png;base64,... 传给后端 save_base64_img
// 后端返回 (url_path, no_domain_path)
配置项
来自 APPCOFIG.server:
- upload_dir:本地上传目录(默认 data/upload)
- static_dir:本地静态图片目录(用于 Base64 保存)
- domainname:生成返回 URL 的前缀(如 https://example.com 或 /static 域)
请确保上述目录存在或具备创建权限;首次写入会自动创建月份目录。
注意事项与小建议
- Content-Type 白名单:目前允许 jpeg/png/gif,可按需扩展。为更安全,建议结合内容嗅探库进一步校验。
- 大小限制:建议在后端读取时检查 bytes.len(),限制单文件大小,避免资源被滥用。
- 文件名唯一性:使用 GID 避免冲突;无需客户端传文件名。
- 旧文件清理:当前实现使用 field.file_name() 传入旧文件 URL 并删除。更规范的做法是单独提交 old_url 字段。
- Base64 前缀:remove_prefix 仅去除 data:image/png;base64,;如需 jpg/webp 可扩展匹配。
- 路径替换:delete_file 通过把 domainname 替换为 upload_dir 得到物理路径,务必保证 domainname 与返回 URL 一致前缀,防止误删。