文件上传 #

文件上传是Web应用中常见的功能。Rocket提供了简单而强大的文件上传处理能力。

基本配置 #

添加依赖 #

toml
[dependencies]
rocket = { version = "0.5", features = ["json"] }

单文件上传 #

使用TempFile #

rust
use rocket::form::Form;
use rocket::fs::TempFile;

#[derive(FromForm)]
struct Upload<'r> {
    file: TempFile<'r>,
}

#[post("/upload", data = "<form>")]
async fn upload(mut form: Form<Upload<'_>>) -> String {
    let filename = form.file.name().unwrap_or("unknown");
    let size = form.file.len();
    format!("Uploaded '{}' ({} bytes)", filename, size)
}

保存文件 #

rust
use rocket::form::Form;
use rocket::fs::TempFile;
use std::path::Path;

#[derive(FromForm)]
struct Upload<'r> {
    file: TempFile<'r>,
}

#[post("/upload", data = "<form>")]
async fn upload(mut form: Form<Upload<'_>>) -> Result<String, String> {
    let filename = form.file.name().unwrap_or("uploaded");
    let dest = Path::new("uploads").join(filename);
    
    form.file.persist_to(&dest)
        .await
        .map_err(|e| format!("Failed to save: {}", e))?;
    
    Ok(format!("Saved to {:?}", dest))
}

HTML表单 #

html
<form action="/upload" method="POST" enctype="multipart/form-data">
    <input type="file" name="file" />
    <button type="submit">Upload</button>
</form>

多文件上传 #

固定数量文件 #

rust
#[derive(FromForm)]
struct MultiUpload<'r> {
    avatar: TempFile<'r>,
    banner: TempFile<'r>,
}

#[post("/upload-multi", data = "<form>")]
async fn upload_multi(form: Form<MultiUpload<'_>>) -> String {
    let avatar_name = form.avatar.name().unwrap_or("unknown");
    let banner_name = form.banner.name().unwrap_or("unknown");
    format!("Avatar: {}, Banner: {}", avatar_name, banner_name)
}

动态数量文件 #

rust
#[derive(FromForm)]
struct FilesUpload<'r> {
    files: Vec<TempFile<'r>>,
}

#[post("/upload-files", data = "<form>")]
async fn upload_files(form: Form<FilesUpload<'_>>) -> String {
    let count = form.files.len();
    let names: Vec<_> = form.files.iter()
        .filter_map(|f| f.name())
        .collect();
    format!("Uploaded {} files: {:?}", count, names)
}

文件信息 #

获取文件属性 #

rust
use rocket::fs::TempFile;
use rocket::http::ContentType;

#[derive(FromForm)]
struct Upload<'r> {
    file: TempFile<'r>,
}

#[post("/info", data = "<form>")]
async fn file_info(form: Form<Upload<'_>>) -> String {
    let info = format!(
        "Name: {:?}\nSize: {} bytes\nContent-Type: {:?}",
        form.file.name(),
        form.file.len(),
        form.file.content_type()
    );
    info
}

文件属性列表 #

属性 方法 说明
文件名 file.name() 原始文件名
大小 file.len() 文件大小(字节)
内容类型 file.content_type() MIME类型
路径 file.path() 临时文件路径

文件验证 #

验证文件类型 #

rust
use rocket::form::{Form, FromForm};
use rocket::fs::TempFile;
use rocket::http::ContentType;

#[derive(FromForm)]
struct ImageUpload<'r> {
    #[field(validate = validate_image_type)]
    image: TempFile<'r>,
}

fn validate_image_type(file: &TempFile<'_>) -> form::Result<'_, ()> {
    let valid_types = ["image/jpeg", "image/png", "image/gif"];
    
    match file.content_type() {
        Some(ct) if valid_types.contains(&ct.to_string().as_str()) => Ok(()),
        _ => Err(form::Error::validation("Invalid image type").into()),
    }
}

#[post("/upload-image", data = "<form>")]
async fn upload_image(form: Form<ImageUpload<'_>>) -> String {
    format!("Valid image: {:?}", form.image.name())
}

验证文件大小 #

rust
const MAX_SIZE: u64 = 10 * 1024 * 1024; // 10MB

#[derive(FromForm)]
struct LimitedUpload<'r> {
    #[field(validate = validate_size)]
    file: TempFile<'r>,
}

fn validate_size(file: &TempFile<'_>) -> form::Result<'_, ()> {
    if file.len() > MAX_SIZE {
        Err(form::Error::validation("File too large (max 10MB)").into())
    } else {
        Ok(())
    }
}

文件存储 #

本地存储 #

rust
use rocket::fs::TempFile;
use std::path::PathBuf;
use uuid::Uuid;

async fn save_file(file: &mut TempFile<'_>) -> Result<PathBuf, std::io::Error> {
    let upload_dir = PathBuf::from("uploads");
    std::fs::create_dir_all(&upload_dir)?;
    
    let extension = file.name()
        .and_then(|n| n.rsplit('.').next())
        .unwrap_or("bin");
    
    let filename = format!("{}.{}", Uuid::new_v4(), extension);
    let dest = upload_dir.join(&filename);
    
    file.persist_to(&dest).await?;
    Ok(dest)
}

#[derive(FromForm)]
struct Upload<'r> {
    file: TempFile<'r>,
}

#[post("/save", data = "<form>")]
async fn save_upload(mut form: Form<Upload<'_>>) -> Result<String, String> {
    match save_file(&mut form.file).await {
        Ok(path) => Ok(format!("Saved to {:?}", path)),
        Err(e) => Err(format!("Error: {}", e)),
    }
}

按日期组织 #

rust
use chrono::Local;

async fn save_with_date(file: &mut TempFile<'_>) -> Result<PathBuf, std::io::Error> {
    let now = Local::now();
    let date_dir = format!("uploads/{}/{}/{}", 
        now.format("%Y"), 
        now.format("%m"), 
        now.format("%d")
    );
    
    std::fs::create_dir_all(&date_dir)?;
    
    let filename = file.name().unwrap_or("uploaded");
    let dest = PathBuf::from(date_dir).join(filename);
    
    file.persist_to(&dest).await?;
    Ok(dest)
}

进度跟踪 #

使用Fairing跟踪 #

rust
use rocket::{Request, Data, Response};
use rocket::fairing::{Fairing, Info, Kind};

struct UploadProgress;

#[rocket::async_trait]
impl Fairing for UploadProgress {
    fn info(&self) -> Info {
        Info {
            name: "Upload Progress",
            kind: Kind::Request,
        }
    }

    async fn on_request(&self, request: &mut Request<'_>, _data: &mut Data<'_>) {
        if let Some(len) = request.headers().get_one("Content-Length") {
            println!("Upload size: {} bytes", len);
        }
    }
}

配置限制 #

Rocket.toml配置 #

toml
[default]
limits = { file = 10MiB, data-form = 5MiB }

[default.limits]
file = 10MiB
data-form = 5MiB
json = 1MiB

代码中配置 #

rust
use rocket::data::{Limits, ByteUnit};

#[launch]
fn rocket() -> _ {
    rocket::build()
        .configure(rocket::Config::figment()
            .merge(("limits.file", ByteUnit::Megabyte(20)))
        )
        .mount("/", routes![upload])
}

完整示例 #

rust
#[macro_use] extern crate rocket;

use rocket::form::{Form, FromForm};
use rocket::fs::TempFile;
use rocket::http::ContentType;
use std::path::PathBuf;
use uuid::Uuid;

const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
const ALLOWED_TYPES: &[&str] = &["image/jpeg", "image/png", "image/gif", "image/webp"];

#[derive(FromForm)]
struct Upload<'r> {
    description: String,
    
    #[field(validate = validate_file)]
    file: TempFile<'r>,
}

fn validate_file(file: &TempFile<'_>) -> form::Result<'_, ()> {
    if file.len() > MAX_FILE_SIZE {
        return Err(form::Error::validation("File too large (max 10MB)").into());
    }
    
    match file.content_type() {
        Some(ct) if ALLOWED_TYPES.contains(&ct.to_string().as_str()) => Ok(()),
        _ => Err(form::Error::validation("Invalid file type").into()),
    }
}

async fn save_upload(file: &mut TempFile<'_>) -> Result<PathBuf, std::io::Error> {
    let upload_dir = PathBuf::from("uploads");
    std::fs::create_dir_all(&upload_dir)?;
    
    let extension = file.name()
        .and_then(|n| n.rsplit('.').next())
        .unwrap_or("bin");
    
    let filename = format!("{}.{}", Uuid::new_v4(), extension);
    let dest = upload_dir.join(filename);
    
    file.persist_to(&dest).await?;
    Ok(dest)
}

#[derive(FromForm)]
struct MultiUpload<'r> {
    description: String,
    files: Vec<TempFile<'r>>,
}

#[get("/")]
fn index() -> &'static str {
    r#"
    <h1>File Upload</h1>
    <form action="/upload" method="POST" enctype="multipart/form-data">
        <input type="text" name="description" placeholder="Description" />
        <input type="file" name="file" accept="image/*" />
        <button type="submit">Upload</button>
    </form>
    
    <h2>Multiple Files</h2>
    <form action="/upload-multi" method="POST" enctype="multipart/form-data">
        <input type="text" name="description" placeholder="Description" />
        <input type="file" name="files" multiple accept="image/*" />
        <button type="submit">Upload All</button>
    </form>
    "#
}

#[post("/upload", data = "<form>")]
async fn upload(mut form: Form<Upload<'_>>) -> Result<String, String> {
    let filename = form.file.name().unwrap_or("unknown");
    let path = save_upload(&mut form.file)
        .await
        .map_err(|e| format!("Save error: {}", e))?;
    
    Ok(format!(
        "Saved '{}' to {:?}\nDescription: {}",
        filename, path, form.description
    ))
}

#[post("/upload-multi", data = "<form>")]
async fn upload_multi(mut form: Form<MultiUpload<'_>>) -> String {
    let mut results = Vec::new();
    
    for file in form.files.iter_mut() {
        match save_upload(file).await {
            Ok(path) => results.push(format!("Saved: {:?}", path)),
            Err(e) => results.push(format!("Error: {}", e)),
        }
    }
    
    format!("Description: {}\n{}", form.description, results.join("\n"))
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, upload, upload_multi])
}

测试示例 #

bash
# 上传单个文件
curl -X POST http://127.0.0.1:8000/upload \
  -F "description=My image" \
  -F "file=@photo.jpg"

# 上传多个文件
curl -X POST http://127.0.0.1:8000/upload-multi \
  -F "description=My images" \
  -F "files=@photo1.jpg" \
  -F "files=@photo2.jpg"

下一步 #

掌握了文件上传后,让我们继续学习 响应类型,了解如何返回各种类型的响应。

最后更新:2026-03-28