rust - 如何高效地使用 Actix Multipart 将单个文件上传到磁盘?

标签 rust upload multipart actix-web

TL;博士:

在迭代“数据 block ”并将其保存到单个文件时,我无法理解 Actix Multipart;同时不会扰乱 Rust 错误处理、高效内存管理和异步处理。

详细信息和背景:

我了解一些 C++ 和 REST API 基础理论,但之前从未实现过 Web 服务。此外,我是 Rust 的新手,希望使用 Actix 创建一个简单的文件服务器作为我的第一个 Rust 项目。该文件服务器将在 Kubernetes 中的简单容器中运行,可以随时添加和删除该容器的实例。文件存储在单个目录中,该目录通过安装的卷在所有容器实例之间共享。每个实例应使用尽可能少的内存。 目标是提供...

  1. 一个简单的 HTTP GET API 端点,专注于单个文件下载的最大速度。
  2. 一个简单的 HTTP PUT API 端点,专注于单个文件上传的最大稳健性和安全性。

有一些变化,例如使用 zstd 进行文件可选文件压缩、使用 xxhash128 进行散列、预写式日志记录或 WAL(如 SQLite 中的)等等,出于简单原因,这些变化将从代码片段中删除。

我也欢迎进一步提出 Acitx Multipart 问题之外的改进建议。

HTTP GET: 我对此并不满意,但它确实有效。

#[get("/file/{file_id}")]
pub async fn get_file(file_id: web::Path<String>, data_path: web::Data<Config>) -> impl Responder {
    let mut file_path = data_path.data_path.clone();
    file_path.push('/');
    file_path.push_str(&file_id);
    if let Ok(mut file) = File::open(file_path) {
        let mut contents = Vec::new();
        if let Err(_) = file.read_to_end(&mut contents) {
            return HttpResponse::InternalServerError().finish();
        }
        HttpResponse::Ok().body(contents)
    } else {
        HttpResponse::NotFound().finish()
    }
}
}

HTTP PUT: while 循环中的所有内容绝对是垃圾。这就是我需要你帮助的地方。

#[put("/file/{file_id}")]
pub async fn put_file(
    data_path: web::Data<Config>, mut payload: Multipart, request: HttpRequest) -> impl Responder {
    // 10 MB
    const MAX_FILE_SIZE: u64 = 1024 * 1024 * 10;
    const MAX_FILE_COUNT: i32 = 1;

    // detect malformed requests
    let content_length: u64 = match request.headers().get("content-length") {
        Some(header_value) => header_value.to_str().unwrap_or("0").parse().unwrap_or(0),
        None => 0,
    };

    // reject malformed requests
    match content_length {
        0 => return HttpResponse::BadRequest().finish(),
        length if length > MAX_FILE_SIZE => {
            return HttpResponse::BadRequest()
                .body(format!("The uploaded file is too large. Maximum size is {} bytes.", MAX_FILE_SIZE));
        },
        _ => {}
    };

    let file_path = data_path.data_path.clone();
    let mut file_count = 0;

    while let Some(mut field) = payload.try_next().await.unwrap_or(None) {
        if let Some(filename) = field.content_disposition().get_filename() {
            if file_count == MAX_FILE_COUNT {
                return HttpResponse::BadRequest().body(format!(
                    "Too many files uploaded. Maximum count is {}.", MAX_FILE_COUNT
                ));
            }

            let file_path = format!("{}{}-{}", file_path, "1", sanitize_filename::sanitize(&filename));
            let mut file: File = File::create(&file_path).unwrap();

            while let Some(chunk) = field.try_next().await.unwrap_or(None) {
                file.write_all(&chunk).map_err(|e| {
                    HttpResponse::InternalServerError().body(format!(
                        "Failed to write to file: {}", e
                    ))
                });
            }

            file.flush().map_err(|e| {
                HttpResponse::InternalServerError().body(format!(
                    "Failed to flush file: {}", e
                ))
            });

            file_count += 1;
        }
    }

    if file_count != 1 {
        return HttpResponse::BadRequest().body("Exactly one file must be uploaded.");
    }

    HttpResponse::Ok().finish()
}

最佳答案

我使用主要的 actix-web 包解决了这个问题。

请注意,此解决方案依赖于默认的 actix multipart 行为,该行为在接收上传的文件时创建临时文件。以下是官方文档中的一个重要通知:

The default constructor, NamedTempFile::new(), creates files in the location returned by std::env::temp_dir().

我使用std::fs:rename()将此文件移动到我的目标目录。这种“临时文件”行为对我来说很有用,因为我的磁盘存储性能非常好,而内存使用是我主要关心的问题。另请记住, std::fs:rename() 的作用与移动“mv”类似。因此,请确保 std::env::temp_dir() 和您的目标目的地设置为同一文件系统上的路径,以防止完整的文件复制。

#[derive(MultipartForm)]
pub struct Upload {
    file: TempFile,
}

#[put("/file")]
pub async fn put_file(
    config: web::Data<Config>, form: MultipartForm<Upload>) -> impl Responder {
    const MAX_FILE_SIZE: u64 = 1024 * 1024 * 10; // 10 MB
    const MAX_FILE_COUNT: i32 = 1;

    // reject malformed requests
    match form.file.size {
        0 => return HttpResponse::BadRequest().finish(),
        length if length > MAX_FILE_SIZE.try_into().unwrap() => {
            return HttpResponse::BadRequest()
                .body(format!("The uploaded file is too large. Maximum size is {} bytes.", MAX_FILE_SIZE));
        },
        _ => {}
    };
    
    let temp_file_path = form.file.file.path();
    let file_name: &str = form
        .file
        .file_name
        .as_ref()
        .map(|m| m.as_ref())
        .unwrap_or("null");

    let mut file_path = PathBuf::from(&config.data_path);
    file_path.push(&sanitize_filename::sanitize(&file_name));

    match std::fs::rename(temp_file_path, file_path) {
        Ok(_) => HttpResponse::Ok().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

关于rust - 如何高效地使用 Actix Multipart 将单个文件上传到磁盘?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/75848399/

相关文章:

MySQL LOAD DATA [LOCAL] INFILE 路径名语法错误

spring - Apache HttpClient 向 Spring @Controller 类进行多部分 POST

rust - 为具有 Deserialize trait bound 的泛型派生 Deserialize 时,无法推断类型参数的类型

cakephp - 让 TinyMCE Tinybrowser 直接上传到 CakePHP webroot

rust - 为类型引用实现特征时返回对临时值的引用时出错

swift - 上传数据不填充 $_FILES

java - Spring 中的多部分文件下载?

安卓分段上传

compiler-construction - 写语法扩展时,是否可以查询注解类型以外的类型信息?

rust - 借用循环内使用的可变成员