文件上传 #
文件上传概述 #
Actix Web 支持多种文件上传方式:
| 方式 | Content-Type | 适用场景 |
|---|---|---|
| Multipart | multipart/form-data |
表单文件上传 |
| Raw Bytes | application/octet-stream |
原始文件流 |
| Base64 | application/json |
JSON 编码文件 |
基本文件上传 #
添加依赖 #
toml
[dependencies]
actix-web = "4"
actix-multipart = "0.6"
tokio = { version = "1", features = ["fs", "io-util"] }
uuid = { version = "1", features = ["v4"] }
mime = "0.3"
单文件上传 #
rust
use actix_multipart::Multipart;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use futures::{StreamExt, TryStreamExt};
use std::io::Write;
#[actix_web::post("/upload")]
async fn upload(mut payload: Multipart) -> impl Responder {
while let Ok(Some(mut field)) = payload.try_next().await {
let content_type = field.content_type().cloned();
let disposition = field.content_disposition();
let filename = disposition
.get_filename()
.unwrap_or("unknown")
.to_string();
let mut file_data = Vec::new();
while let Ok(Some(chunk)) = field.try_next().await {
file_data.extend_from_slice(&chunk);
}
println!("Uploaded: {} ({} bytes)", filename, file_data.len());
}
HttpResponse::Ok().json(serde_json::json!({
"status": "success"
}))
}
保存到磁盘 #
rust
use actix_multipart::Multipart;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use futures::{StreamExt, TryStreamExt};
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;
#[actix_web::post("/upload/save")]
async fn upload_save(mut payload: Multipart) -> impl Responder {
let upload_dir = PathBuf::from("uploads");
tokio::fs::create_dir_all(&upload_dir).await.ok();
let mut uploaded_files = Vec::new();
while let Ok(Some(mut field)) = payload.try_next().await {
let original_name = field
.content_disposition()
.get_filename()
.unwrap_or("unknown")
.to_string();
let extension = PathBuf::from(&original_name)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("bin");
let unique_name = format!("{}.{}", Uuid::new_v4(), extension);
let filepath = upload_dir.join(&unique_name);
let mut file = tokio::fs::File::create(&filepath)
.await
.expect("Failed to create file");
let mut size = 0u64;
while let Ok(Some(chunk)) = field.try_next().await {
file.write_all(&chunk).await.ok();
size += chunk.len() as u64;
}
uploaded_files.push(serde_json::json!({
"original_name": original_name,
"saved_name": unique_name,
"size": size
}));
}
HttpResponse::Ok().json(serde_json::json!({
"files": uploaded_files
}))
}
多文件上传 #
rust
use actix_multipart::Multipart;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use futures::{StreamExt, TryStreamExt};
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;
#[derive(serde::Serialize)]
struct UploadedFile {
original_name: String,
saved_name: String,
content_type: Option<String>,
size: u64,
}
#[actix_web::post("/upload/multiple")]
async fn upload_multiple(mut payload: Multipart) -> impl Responder {
let upload_dir = PathBuf::from("uploads");
tokio::fs::create_dir_all(&upload_dir).await.ok();
let mut uploaded_files: Vec<UploadedFile> = Vec::new();
while let Ok(Some(mut field)) = payload.try_next().await {
let content_type = field.content_type().map(|m| m.to_string());
let disposition = field.content_disposition();
let original_name = disposition
.get_filename()
.unwrap_or("unknown")
.to_string();
let extension = PathBuf::from(&original_name)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("bin");
let unique_name = format!("{}.{}", Uuid::new_v4(), extension);
let filepath = upload_dir.join(&unique_name);
let mut file = tokio::fs::File::create(&filepath)
.await
.expect("Failed to create file");
let mut size = 0u64;
while let Ok(Some(chunk)) = field.try_next().await {
file.write_all(&chunk).await.ok();
size += chunk.len() as u64;
}
uploaded_files.push(UploadedFile {
original_name,
saved_name: unique_name,
content_type,
size,
});
}
HttpResponse::Ok().json(serde_json::json!({
"count": uploaded_files.len(),
"files": uploaded_files
}))
}
文件类型验证 #
rust
use actix_multipart::Multipart;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use futures::{StreamExt, TryStreamExt};
use mime::Mime;
const ALLOWED_TYPES: &[&str] = &[
"image/jpeg",
"image/png",
"image/gif",
"application/pdf",
];
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024; // 10MB
fn is_allowed_content_type(content_type: Option<&mime::Mime>) -> bool {
content_type
.map(|ct| ALLOWED_TYPES.contains(&ct.to_string().as_str()))
.unwrap_or(false)
}
#[actix_web::post("/upload/validate")]
async fn upload_validate(mut payload: Multipart) -> impl Responder {
let upload_dir = std::path::PathBuf::from("uploads");
tokio::fs::create_dir_all(&upload_dir).await.ok();
let mut results = Vec::new();
while let Ok(Some(mut field)) = payload.try_next().await {
let content_type = field.content_type().cloned();
let original_name = field
.content_disposition()
.get_filename()
.unwrap_or("unknown")
.to_string();
if !is_allowed_content_type(content_type.as_ref()) {
results.push(serde_json::json!({
"filename": original_name,
"status": "rejected",
"error": "Invalid content type"
}));
continue;
}
let mut file_data = Vec::new();
while let Ok(Some(chunk)) = field.try_next().await {
file_data.extend_from_slice(&chunk);
if file_data.len() as u64 > MAX_FILE_SIZE {
results.push(serde_json::json!({
"filename": original_name,
"status": "rejected",
"error": "File too large"
}));
continue;
}
}
let extension = std::path::PathBuf::from(&original_name)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("bin");
let unique_name = format!("{}.{}", uuid::Uuid::new_v4(), extension);
let filepath = upload_dir.join(&unique_name);
tokio::fs::write(&filepath, &file_data).await.ok();
results.push(serde_json::json!({
"filename": original_name,
"saved_name": unique_name,
"status": "success",
"size": file_data.len()
}));
}
HttpResponse::Ok().json(serde_json::json!({
"results": results
}))
}
大文件流式处理 #
rust
use actix_multipart::Multipart;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use futures::{StreamExt, TryStreamExt};
use tokio::io::AsyncWriteExt;
#[actix_web::post("/upload/stream")]
async fn upload_stream(mut payload: Multipart) -> impl Responder {
let upload_dir = std::path::PathBuf::from("uploads");
tokio::fs::create_dir_all(&upload_dir).await.ok();
let mut results = Vec::new();
while let Ok(Some(mut field)) = payload.try_next().await {
let original_name = field
.content_disposition()
.get_filename()
.unwrap_or("unknown")
.to_string();
let unique_name = format!("{}.dat", uuid::Uuid::new_v4());
let filepath = upload_dir.join(&unique_name);
let mut file = tokio::fs::File::create(&filepath)
.await
.expect("Failed to create file");
let mut size = 0u64;
while let Ok(Some(chunk)) = field.try_next().await {
file.write_all(&chunk).await.ok();
size += chunk.len() as u64;
}
file.flush().await.ok();
results.push(serde_json::json!({
"original_name": original_name,
"saved_name": unique_name,
"size": size
}));
}
HttpResponse::Ok().json(serde_json::json!({
"files": results
}))
}
使用 actix-multipart-derive #
toml
[dependencies]
actix-multipart = { version = "0.6", features = ["derive"] }
tempfile = "3"
rust
use actix_multipart::form::{MultipartForm, tempfile::TempFile, text::Text};
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
#[derive(MultipartForm)]
struct UploadForm {
#[multipart(limit = "10MB")]
file: TempFile,
#[multipart(limit = "1KB")]
description: Text<String>,
#[multipart(limit = "100B", rename = "category")]
category: Text<String>,
}
#[actix_web::post("/upload/form")]
async fn upload_form(form: MultipartForm<UploadForm>) -> impl Responder {
let file = &form.file;
HttpResponse::Ok().json(serde_json::json!({
"description": form.description.as_str(),
"category": form.category.as_str(),
"filename": file.file_name.as_deref(),
"size": file.size,
"content_type": file.content_type.as_ref().map(|m| m.to_string())
}))
}
文件下载 #
rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use actix_files::NamedFile;
use std::path::PathBuf;
#[actix_web::get("/download/{filename:.*}")]
async fn download(path: web::Path<String>) -> impl Responder {
let filename = path.into_inner();
let filepath = PathBuf::from("uploads").join(&filename);
match NamedFile::open(&filepath) {
Ok(file) => file.into_response(&actix_web::HttpRequest::default()),
Err(_) => HttpResponse::NotFound().json(serde_json::json!({
"error": "File not found"
})),
}
}
完整示例 #
rust
use actix_multipart::Multipart;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use futures::{StreamExt, TryStreamExt};
use serde::Serialize;
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;
#[derive(Serialize)]
struct FileInfo {
id: String,
original_name: String,
content_type: Option<String>,
size: u64,
}
#[actix_web::post("/upload")]
async fn upload_file(mut payload: Multipart) -> impl Responder {
let upload_dir = PathBuf::from("uploads");
if let Err(e) = tokio::fs::create_dir_all(&upload_dir).await {
return HttpResponse::InternalServerError().json(serde_json::json!({
"error": format!("Failed to create upload directory: {}", e)
}));
}
let mut uploaded_files: Vec<FileInfo> = Vec::new();
while let Ok(Some(mut field)) = payload.try_next().await {
let content_type = field.content_type().map(|m| m.to_string());
let original_name = field
.content_disposition()
.get_filename()
.unwrap_or("unknown")
.to_string();
let file_id = Uuid::new_v4().to_string();
let extension = PathBuf::from(&original_name)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("bin");
let saved_name = format!("{}.{}", file_id, extension);
let filepath = upload_dir.join(&saved_name);
let mut file = match tokio::fs::File::create(&filepath).await {
Ok(f) => f,
Err(e) => {
eprintln!("Failed to create file: {}", e);
continue;
}
};
let mut size = 0u64;
while let Ok(Some(chunk)) = field.try_next().await {
if let Err(e) = file.write_all(&chunk).await {
eprintln!("Failed to write chunk: {}", e);
break;
}
size += chunk.len() as u64;
}
uploaded_files.push(FileInfo {
id: file_id,
original_name,
content_type,
size,
});
}
HttpResponse::Ok().json(serde_json::json!({
"uploaded": uploaded_files.len(),
"files": uploaded_files
}))
}
#[actix_web::get("/files")]
async fn list_files() -> impl Responder {
let upload_dir = PathBuf::from("uploads");
let mut files = Vec::new();
if let Ok(mut entries) = tokio::fs::read_dir(&upload_dir).await {
while let Ok(Some(entry)) = entries.next_entry().await {
if let Ok(metadata) = entry.metadata().await {
files.push(serde_json::json!({
"name": entry.file_name().to_string_lossy(),
"size": metadata.len()
}));
}
}
}
HttpResponse::Ok().json(serde_json::json!({
"files": files
}))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Server running at http://127.0.0.1:8080");
HttpServer::new(|| {
App::new()
.service(upload_file)
.service(list_files)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
测试文件上传 #
bash
# 单文件上传
curl -X POST http://localhost:8080/upload \
-F "file=@/path/to/file.txt"
# 多文件上传
curl -X POST http://localhost:8080/upload \
-F "file=@/path/to/file1.txt" \
-F "file=@/path/to/file2.txt"
# 带额外字段
curl -X POST http://localhost:8080/upload \
-F "description=My file" \
-F "file=@/path/to/file.txt"
下一步 #
现在你已经掌握了文件上传,继续学习 响应类型,深入了解响应处理!
最后更新:2026-03-29