使用Rust語言搞定圖片上傳功能的示例詳解
1、下載引入
Cargo.toml安裝依賴
已經(jīng)有的就不需要額外添加了
這里我額外移入了uuid 生成唯一文件名
[dependencies]
actix-web = "4.0" # 開發(fā) RESTful API接口
actix-multipart = "0.4" # 處理文件上傳
tokio = { version = "1", features = ["full"] } # 異步運(yùn)行時(shí),網(wǎng)絡(luò)、文件I/O異步任務(wù)
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" # 靜態(tài)文件服務(wù)
uuid最新版本地址:https://crates.io/crates/uuid
2、使用
入口申明模塊
路由入口,我們新建一個(gè)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. 注入數(shù)據(jù)庫連接池
.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))
})
申明模塊入口
這里需要申明外層模塊和子模塊兩個(gè)部分
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
// # 處理函數(shù)(可選)
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use sqlx::{MySqlPool,MySql, Pool};
use crate::common::response::ApiResponse;// 導(dǎo)入 ApiResponse 模型
// 上傳圖片接口
pub async fn uploadimg() -> HttpResponse {
HttpResponse::Ok().json(ApiResponse {
code: 200,
msg: "接口信息",
data: None::<()>,
})
}
測試我們的接口,返回如下
{
"code": 200,
"msg": "接口信息"
}
3、功能實(shí)現(xiàn)
上傳文件邏輯
接下來我們參考我們之前的接口部分,返回上傳圖片成功以后的數(shù)據(jù),這里我們先以實(shí)現(xiàn)功能為主
// // # 處理函數(shù)(可選)
// 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;
// 定義響應(yīng)數(shù)據(jù)結(jié)構(gòu)
#[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 {
// 創(chuàng)建上傳目錄(如果不存在)
if let Err(e) = create_dir_all(UPLOAD_DIR) {
return internal_server_error(&format!("創(chuàng)建目錄失敗: {}", e));
}
// 獲取基礎(chǔ)URL(從環(huán)境變量或使用默認(rèn)值)
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)),
};
// 獲取內(nèi)容處置頭部
let content_disposition = field.content_disposition();
// 獲取文件名
let original_file_name = match content_disposition.get_filename() {
Some(name) => name.to_string(),
None => continue, // 跳過非文件字段
};
// 驗(yàn)證文件類型
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);
// 保存文件內(nèi)容并獲取文件大小
let file_size = match save_file(&mut field, &file_path).await {
Ok(size) => size,
Err(e) => {
return internal_server_error(&format!("文件保存失敗: {}", e));
}
};
// 創(chuàng)建響應(yīng)數(shù)據(jù)
let response_data = UploadResponse {
fullPath: absolute_path,
relativePath: relative_path,
size: file_size,
fileName: format!("圖片-{}", original_file_name),
fileType: mime_type,
fileUid: file_id,
};
// 返回成功響應(yīng)
return HttpResponse::Ok().json(ApiResponse {
code: 200,
msg: "圖片上傳成功",
data: Some(response_data),
});
}
// 沒有找到有效的文件字段
bad_request("未檢測到上傳的文件")
}
/// 根據(jù) MIME 類型獲取文件擴(kuò)展名
fn get_extension(mime_type: &str) -> &str {
match mime_type {
"image/jpeg" => "jpg",
"image/png" => "png",
"image/gif" => "gif",
_ => "bin", // 不會(huì)發(fā)生(前面已驗(yàn)證)
}
}
/// 保存上傳的文件并返回文件大小
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;
// 處理每個(gè)數(shù)據(jù)塊
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!("讀取數(shù)據(jù)塊失敗: {}", e)
)
})?;
// 寫入文件并更新大小
file.write_all(&chunk)?;
total_size += chunk.len() as u64;
}
file.flush()?;
Ok(total_size)
}
/// 400 錯(cuò)誤響應(yīng)
fn bad_request(msg: &str) -> HttpResponse {
HttpResponse::BadRequest().json(ApiResponse::<()> {
code: 400,
msg:"錯(cuò)誤",
data: None,
})
}
/// 500 錯(cuò)誤響應(yīng)
fn internal_server_error(msg: &str) -> HttpResponse {
HttpResponse::InternalServerError().json(ApiResponse::<()> {
code: 500,
msg:"錯(cuò)誤",
data: None,
})
}
測試上傳圖片接口
測試接口這個(gè)時(shí)候給我們返回的數(shù)據(jù)如下
{
"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"
}
}
文件歸位
現(xiàn)在可以看到我們傳入的文件都在upload下,我們分配一下,圖片和視頻區(qū)別后面
pub async fn upload_img(mut payload: Multipart) -> impl Responder {
// 創(chuàng)建圖片存儲(chǔ)目錄(如果不存在)
let image_dir = format!("{}/{}", BASE_UPLOAD_DIR, IMAGE_SUBDIR);
if let Err(e) = create_dir_all(&image_dir) {
return internal_server_error(&format!("創(chuàng)建目錄失敗: {}", e));
}
// 獲取基礎(chǔ)URL(從環(huán)境變量或使用默認(rèn)值)
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)),
};
// 獲取內(nèi)容處置頭部
let content_disposition = field.content_disposition();
// 獲取文件名
let original_file_name = match content_disposition.get_filename() {
Some(name) => name.to_string(),
None => continue, // 跳過非文件字段
};
// 驗(yàn)證文件類型
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);
// 文件存儲(chǔ)路徑(包含子目錄)
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);
// 保存文件內(nèi)容并獲取文件大小
let file_size = match save_file(&mut field, &file_path).await {
Ok(size) => size,
Err(e) => {
return internal_server_error(&format!("文件保存失敗: {}", e));
}
};
// 創(chuàng)建響應(yīng)數(shù)據(jù)
let response_data = UploadResponse {
fullPath: absolute_path,
relativePath: relative_path,
size: file_size,
fileName: format!("圖片-{}", original_file_name),
fileType: mime_type,
fileUid: file_id,
};
// 返回成功響應(yīng)
return HttpResponse::Ok().json(ApiResponse {
code: 200,
msg: "圖片上傳成功".to_string(),
data: Some(response_data),
});
}
// 沒有找到有效的文件字段
bad_request("未檢測到上傳的文件")
}
這個(gè)時(shí)候返回的接口地址,已經(jīng)成為我們想要的路徑了
{
"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"
}
}
文件靜態(tài)路徑
但是訪問我們的圖片地址,卻無法訪問,這是為什么呢?
這是因?yàn)槲覀兎?wù)器上還沒有裝靜態(tài)文件服務(wù),在我們跟入口文件之中配置
依賴前提必須安裝這個(gè)依賴
// 依賴前提 actix-files = "0.6.2" # 靜態(tài)文件服務(wù)
在主文件之中引入
// 文件服務(wù) use actix_files as fs; use std::fs::create_dir_all; // 創(chuàng)建目錄
入口文件之中添加我們的靜態(tài)文件地址
async fn main() -> std::io::Result<()> {
dotenv().ok(); // 一定要在讀取環(huán)境變量之前調(diào)用
// 顯式設(shè)置日志級別和輸出格式
Builder::new()
.parse_filters("info") // 設(shè)置日志級別為 info
.init(); // 初始化日志記錄器
info!("日志系統(tǒng)已初始化!??!");
// 確保上傳目錄存在
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!("創(chuàng)建目錄 {} 失敗: {}", dir, e);
// 生產(chǎn)環(huán)境中可能需要更嚴(yán)格的處理
}
}
info!("圖片服務(wù)器已準(zhǔn)備!");
// 1. 初始化數(shù)據(jù)庫連接池
// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL not set");
// 創(chuàng)建 MySQL 異步連接池
// let pool = MySqlPool::connect(&database_url).await.expect("連接數(shù)據(jù)庫失敗");
let database_url = env::var("DATABASE_URL").unwrap(); // 獲取數(shù)據(jù)庫連接字符串
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. 注入數(shù)據(jù)庫連接池
.app_data(web::Data::new(pool.clone()))
// 3. 注冊模塊路由加前綴
.service(
fs::Files::new("/uploads/images", "./uploads/images")
.prefer_utf8(true)
.show_files_listing() // 開發(fā)環(huán)境使用,生產(chǎn)環(huán)境應(yīng)移除
)
// 可以添加其他靜態(tài)文件目錄
.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
}
靜態(tài)資源目錄搭建好了以后,再次訪問,我們的圖片可以完美展示啦
快來跟我一起體驗(yàn)Rust之美吧,最近幾天都寫的很難,所幸都攻克了 簡單但是可能我是小白 很多問題總算踩過去了
到此這篇關(guān)于使用Rust語言搞定圖片上傳功能的示例詳解的文章就介紹到這了,更多相關(guān)Rust圖片上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Rust中使用Serde對json數(shù)據(jù)進(jìn)行反序列化
JSON作為目前流行的數(shù)據(jù)格式之一,被大家廣泛使用,在日常的開發(fā)實(shí)踐中,將JSON數(shù)據(jù)反序列化為對應(yīng)的類型具有重要的意義,在Rust中,Serde幾乎成了JSON數(shù)據(jù)解析的事實(shí)標(biāo)準(zhǔn),本文將給大家介紹Rust中使用Serde對json數(shù)據(jù)進(jìn)行反序列化,需要的朋友可以參考下2024-01-01

