文件上传 #

文件上传概述 #

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