Rust-搞定图片上传功能
1、下载引入
🍎Cargo.toml
安装依赖
已经有的就不需要额外添加了
这里我额外移入了uuid 生成唯一文件名
javascript
[dependencies]
actix-web = "4.0" # 开发 RESTful API接口
actix-multipart = "0.4" # 处理文件上传
tokio = { version = "1", features = ["full"] } # 异步运行时,网络、文件I/O异步任务
futures = "0.3" # 异步编程
serde = { version = "1.0", features = ["derive"] } # 序列化和反序列化
serde_json = "1.0" # 帮我们生成文件名
uuid = "1.0" # 帮我们生成文件名
mime_guess = "2.0" # 猜测文件类型
更换为下面的 2025-08-01
uuid = { version = "1.17.0", features = ["v4"] }
// 添加文件依赖
actix-files = "0.6.2" # 静态文件服务
javascript
// uuid最新版本地址
https://crates.io/crates/uuid
2、使用
🍎入口申明模块
路由入口,我们新建一个upload
模块
main.rs
文件之中申明模块
javascript
HttpServer::new(move || {
let cors = Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header(); // 允许所有来源
App::new()
// 添加 CORS 中间件
.wrap(cors)
// 2. 注入数据库连接池
.app_data(web::Data::new(pool.clone()))
// 3. 注册模块路由加前缀
.service(
web::scope("/api") // 这里加上 /api 前缀
.configure(modules::user::routes::config),
.configure(modules::upload::routes::config),
)
// 3. 注册路由
.route("/", web::get().to(welcome))
})
🍎申明模块入口
这里需要申明外层模块和子模块两个部分
javascript
src\modules\mod.rs
pub mod upload;
pub mod user;
src\modules\upload\mod.rs
pub mod handlers;
pub mod routes; // 必须有这一行,否则无法使用路由
🍎routes.rs
模块之中添加接口
routes.rs
模块添加接口
javascript
use actix_web::web;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.route("/upload/image", web::post().to(crate::modules::upload::handlers::uploadimg));
}
🍎测试接口逻辑
handlers.rs
之中处理方法逻辑,编写我们的上传,这里我们先测试一下
javascript
handlers.rs
// # 处理函数(可选)
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use sqlx::{MySqlPool,MySql, Pool};
use crate::common::response::ApiResponse;// 导入 ApiResponse 模型
// 上传图片接口
pub async fn uploadimg() -> HttpResponse {
HttpResponse::Ok().json(ApiResponse {
code: 200,
msg: "接口信息",
data: None::<()>,
})
}
测试我们的接口,返回如下
javascript
{
"code": 200,
"msg": "接口信息"
}
3、功能实现
🍎上传文件逻辑
接下来我们参考我们之前的接口部分,返回上传图片成功以后的数据,这里我们先以实现功能为主
javascript
// // # 处理函数(可选)
// use actix_web::{web, HttpRequest, HttpResponse, Responder};
// use sqlx::{MySqlPool,MySql, Pool};
// // 上传图片接口
// pub async fn uploadimg() -> HttpResponse {
// HttpResponse::Ok().json(ApiResponse {
// code: 200,
// msg: "接口信息",
// data: None::<()>,
// })
// }
use actix_web::{web, HttpResponse, Responder};
use actix_multipart::Multipart;
use futures::StreamExt;
use std::fs::{create_dir_all, File};
use std::io::Write;
use uuid::Uuid;
use crate::common::response::ApiResponse;
use std::env;
use std::path::Path;
// 定义响应数据结构
#[derive(serde::Serialize)]
struct UploadResponse {
fullPath: String,
relativePath: String,
size: u64,
fileName: String,
fileType: String,
fileUid: String,
}
const UPLOAD_DIR: &str = "./uploads";
const ALLOWED_MIME_TYPES: [&str; 3] = ["image/jpeg", "image/png", "image/gif"];
pub async fn upload_img(mut payload: Multipart) -> impl Responder {
// 创建上传目录(如果不存在)
if let Err(e) = create_dir_all(UPLOAD_DIR) {
return internal_server_error(&format!("创建目录失败: {}", e));
}
// 获取基础URL(从环境变量或使用默认值)
let base_url = env::var("BASE_URL")
.unwrap_or_else(|_| "http://localhost:8080".to_string());
// 遍历多部分表单字段
while let Some(field_result) = payload.next().await {
let mut field = match field_result {
Ok(f) => f,
Err(e) => return bad_request(&format!("字段解析失败: {}", e)),
};
// 获取内容处置头部
let content_disposition = field.content_disposition();
// 获取文件名
let original_file_name = match content_disposition.get_filename() {
Some(name) => name.to_string(),
None => continue, // 跳过非文件字段
};
// 验证文件类型
let mime_type = field.content_type().to_string();
if !ALLOWED_MIME_TYPES.contains(&mime_type.as_str()) {
return bad_request("只允许 JPEG、PNG 或 GIF 图片");
}
// 生成唯一文件名和路径
let extension = get_extension(&mime_type);
let file_id = Uuid::new_v4().to_string();
let unique_name = format!("{}.{}", file_id, extension);
let file_path = format!("{}/{}", UPLOAD_DIR, unique_name);
let relative_path = format!("/uploads/{}", unique_name);
let absolute_path = format!("{}{}", base_url, relative_path);
// 保存文件内容并获取文件大小
let file_size = match save_file(&mut field, &file_path).await {
Ok(size) => size,
Err(e) => {
return internal_server_error(&format!("文件保存失败: {}", e));
}
};
// 创建响应数据
let response_data = UploadResponse {
fullPath: absolute_path,
relativePath: relative_path,
size: file_size,
fileName: format!("图片-{}", original_file_name),
fileType: mime_type,
fileUid: file_id,
};
// 返回成功响应
return HttpResponse::Ok().json(ApiResponse {
code: 200,
msg: "图片上传成功",
data: Some(response_data),
});
}
// 没有找到有效的文件字段
bad_request("未检测到上传的文件")
}
/// 根据 MIME 类型获取文件扩展名
fn get_extension(mime_type: &str) -> &str {
match mime_type {
"image/jpeg" => "jpg",
"image/png" => "png",
"image/gif" => "gif",
_ => "bin", // 不会发生(前面已验证)
}
}
/// 保存上传的文件并返回文件大小
async fn save_file(field: &mut actix_multipart::Field, path: &str) -> std::io::Result<u64> {
let mut file = File::create(path)?;
let mut total_size = 0;
// 处理每个数据块
while let Some(chunk_result) = field.next().await {
// 处理可能的 MultipartError
let chunk = chunk_result.map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("读取数据块失败: {}", e)
)
})?;
// 写入文件并更新大小
file.write_all(&chunk)?;
total_size += chunk.len() as u64;
}
file.flush()?;
Ok(total_size)
}
/// 400 错误响应
fn bad_request(msg: &str) -> HttpResponse {
HttpResponse::BadRequest().json(ApiResponse::<()> {
code: 400,
msg:"错误",
data: None,
})
}
/// 500 错误响应
fn internal_server_error(msg: &str) -> HttpResponse {
HttpResponse::InternalServerError().json(ApiResponse::<()> {
code: 500,
msg:"错误",
data: None,
})
}
🍎测试上传图片接口
测试接口这个时候给我们返回的数据如下
javascript
{
"code": 200,
"msg": "图片上传成功",
"data": {
"fullPath": "http://localhost:8888/uploads/68007a03-497e-4982-8316-10881289cb1e.png",
"relativePath": "/uploads/68007a03-497e-4982-8316-10881289cb1e.png",
"size": 10739,
"fileName": "图片-imgjiance2.png",
"fileType": "image/png",
"fileUid": "68007a03-497e-4982-8316-10881289cb1e"
}
}
🍎文件归位
现在可以看到我们传入的文件都在upload下,我们分配一下,图片和视频区别后面
javascript
pub async fn upload_img(mut payload: Multipart) -> impl Responder {
// 创建图片存储目录(如果不存在)
let image_dir = format!("{}/{}", BASE_UPLOAD_DIR, IMAGE_SUBDIR);
if let Err(e) = create_dir_all(&image_dir) {
return internal_server_error(&format!("创建目录失败: {}", e));
}
// 获取基础URL(从环境变量或使用默认值)
let base_url = env::var("BASE_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
// 遍历多部分表单字段
while let Some(field_result) = payload.next().await {
let mut field = match field_result {
Ok(f) => f,
Err(e) => return bad_request(&format!("字段解析失败: {}", e)),
};
// 获取内容处置头部
let content_disposition = field.content_disposition();
// 获取文件名
let original_file_name = match content_disposition.get_filename() {
Some(name) => name.to_string(),
None => continue, // 跳过非文件字段
};
// 验证文件类型
let mime_type = field.content_type().to_string();
if !ALLOWED_MIME_TYPES.contains(&mime_type.as_str()) {
return bad_request("只允许 JPEG、PNG 或 GIF 图片");
}
// 生成唯一文件名和路径
let extension = get_extension(&mime_type);
let file_id = Uuid::new_v4().to_string();
let unique_name = format!("{}.{}", file_id, extension);
// 文件存储路径(包含子目录)
let file_path = format!("{}/{}", image_dir, unique_name);
// URL 路径(包含子目录)
let relative_path = format!("/uploads/{}/{}", IMAGE_SUBDIR, unique_name);
let absolute_path = format!("{}{}", base_url, relative_path);
// 保存文件内容并获取文件大小
let file_size = match save_file(&mut field, &file_path).await {
Ok(size) => size,
Err(e) => {
return internal_server_error(&format!("文件保存失败: {}", e));
}
};
// 创建响应数据
let response_data = UploadResponse {
fullPath: absolute_path,
relativePath: relative_path,
size: file_size,
fileName: format!("图片-{}", original_file_name),
fileType: mime_type,
fileUid: file_id,
};
// 返回成功响应
return HttpResponse::Ok().json(ApiResponse {
code: 200,
msg: "图片上传成功".to_string(),
data: Some(response_data),
});
}
// 没有找到有效的文件字段
bad_request("未检测到上传的文件")
}
这个时候返回的接口地址,已经成为我们想要的路径了
javascript
{
"code": 200,
"msg": "图片上传成功",
"data": {
"fullPath": "http://localhost:8888/uploads/images/a8d23e18-7155-429e-aea2-5c0f70a545e5.png",
"relativePath": "/uploads/images/a8d23e18-7155-429e-aea2-5c0f70a545e5.png",
"size": 10739,
"fileName": "图片-imgjiance2.png",
"fileType": "image/png",
"fileUid": "a8d23e18-7155-429e-aea2-5c0f70a545e5"
}
}
🍎文件静态路径
但是访问我们的图片地址,却无法访问,这是为什么呢?
这是因为我们服务器上还没有装静态文件服务,在我们跟入口文件之中配置
依赖前提必须安装这个依赖
javascript
// 依赖前提
actix-files = "0.6.2" # 静态文件服务
在主文件之中引入
javascript
// 文件服务
use actix_files as fs;
use std::fs::create_dir_all; // 创建目录
入口文件之中添加我们的静态文件地址
javascript
async fn main() -> std::io::Result<()> {
dotenv().ok(); // 一定要在读取环境变量之前调用
// 显式设置日志级别和输出格式
Builder::new()
.parse_filters("info") // 设置日志级别为 info
.init(); // 初始化日志记录器
info!("日志系统已初始化!!!");
// 确保上传目录存在
let upload_dirs = [
"./uploads",
"./uploads/images",
"./uploads/documents",
"./uploads/videos",
"./uploads/others"
];
for dir in &upload_dirs {
if let Err(e) = create_dir_all(dir) {
eprintln!("创建目录 {} 失败: {}", dir, e);
// 生产环境中可能需要更严格的处理
}
}
info!("图片服务器已准备!");
// 1. 初始化数据库连接池
// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL not set");
// 创建 MySQL 异步连接池
// let pool = MySqlPool::connect(&database_url).await.expect("连接数据库失败");
let database_url = env::var("DATABASE_URL").unwrap(); // 获取数据库连接字符串
let pool = MySqlPool::connect(&database_url).await.unwrap();
HttpServer::new(move || {
let cors = Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header(); // 允许所有来源
App::new()
// 添加 CORS 中间件
.wrap(cors)
// 2. 注入数据库连接池
.app_data(web::Data::new(pool.clone()))
// 3. 注册模块路由加前缀
.service(
fs::Files::new("/uploads/images", "./uploads/images")
.prefer_utf8(true)
.show_files_listing() // 开发环境使用,生产环境应移除
)
// 可以添加其他静态文件目录
.service(
fs::Files::new("/uploads/documents", "./uploads/documents")
)
.service(
web::scope("/api") // 这里加上 /api 前缀
.configure(modules::user::routes::config)
.configure(modules::upload::routes::config)
)
// 3. 注册路由
.route("/", web::get().to(welcome))
})
.bind("0.0.0.0:8888")?
.run()
.await
}
静态资源目录搭建好了以后,再次访问,我们的图片可以完美展示啦
ok
快来跟我一起体验Rust之美吧,最近几天都写的很难,所幸都攻克了 简单但是可能我是小白 很多问题总算踩过去了