使用Rust语言搞定图片上传功能的示例详解
作者:林太白
这篇文章主要为大家详细介绍了如何使用Rust语言搞定图片上传功能,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下
1、下载引入
Cargo.toml安装依赖
已经有的就不需要额外添加了
这里我额外移入了uuid 生成唯一文件名
[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" # 静态文件服务
uuid最新版本地址:https://crates.io/crates/uuid
2、使用
入口申明模块
路由入口,我们新建一个upload
模块
main.rs
文件之中申明模块
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)) })
申明模块入口
这里需要申明外层模块和子模块两个部分
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
模块添加接口
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
之中处理方法逻辑,编写我们的上传,这里我们先测试一下
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::<()>, }) }
测试我们的接口,返回如下
{ "code": 200, "msg": "接口信息" }
3、功能实现
上传文件逻辑
接下来我们参考我们之前的接口部分,返回上传图片成功以后的数据,这里我们先以实现功能为主
// // # 处理函数(可选) // 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, }) }
测试上传图片接口
测试接口这个时候给我们返回的数据如下
{ "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下,我们分配一下,图片和视频区别后面
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("未检测到上传的文件") }
这个时候返回的接口地址,已经成为我们想要的路径了
{ "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" } }
文件静态路径
但是访问我们的图片地址,却无法访问,这是为什么呢?
这是因为我们服务器上还没有装静态文件服务,在我们跟入口文件之中配置
依赖前提必须安装这个依赖
// 依赖前提 actix-files = "0.6.2" # 静态文件服务
在主文件之中引入
// 文件服务 use actix_files as fs; use std::fs::create_dir_all; // 创建目录
入口文件之中添加我们的静态文件地址
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 }
静态资源目录搭建好了以后,再次访问,我们的图片可以完美展示啦
快来跟我一起体验Rust之美吧,最近几天都写的很难,所幸都攻克了 简单但是可能我是小白 很多问题总算踩过去了
到此这篇关于使用Rust语言搞定图片上传功能的示例详解的文章就介绍到这了,更多相关Rust图片上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!