在開發Web應用程式的時候,我們常會需要透過POST請求來取得客戶端上傳的檔案資料,這時候的HTTP主體通常就會使用multipart/form-data格式。然而,Rocket框架並未原生支援multipart/form-data的資料,必須要由程式開發人員自行處理HTTP主體中的原始資料(raw data)才行。不過,解析multipart/form-data格式的資料並不是一件容易的事情,所以這篇文章會介紹一個替代方案來處理multipart/form-data的資料。



Multipart Form Data for Rocket Framework

「Multipart Form Data for Rocket Framework」是筆者開發的套件,能解析Rocket框架所接收到的HTTP請求(request)的主體(body)中,類型為的multipart/form-data資料,並將主體中指定欄位的值分別儲存到記憶體或是檔案系統中。為了避免使用者在HTTP主體中夾帶過大或是錯誤的資料,這個套件還可以事先限制要解析的資料量以及類型(Content-Type)。

Crates.io

https://crates.io/crates/rocket-multipart-form-data

Cargo.toml

rocket-multipart-form-data = "*"

使用方法

rocket_multipart_form_data這個crate提供了MultipartFormDataOptionsMultipartFormDataField這兩個結構體,可以用來設定要讀取multipart/form-data的資料中的哪些欄位,並且可以針對該欄位的資料大小和類型進行限制。

建立MultipartFormDataOptionsMultipartFormDataField的結構實體的方式很簡單,程式如下:

extern crate rocket_multipart_form_data;

use rocket_multipart_form_data::{MultipartFormDataOptions, MultipartFormDataField, Repetition};

let mut options = MultipartFormDataOptions::with_multipart_form_data_fields(
    vec![
        MultipartFormDataField::file("photo").content_type_by_string(Some("image/*"),
        MultipartFormDataField::raw("fingerprint").size_limit(4096),
        MultipartFormDataField::text("name"),
        MultipartFormDataField::text("email").repetition(Repetition::fixed(3)),
        MultipartFormDataField::text("email")
    ]
);

以上的MultipartFormDataOptions結構實體,可以用來將multipart/form-data的資料中的photo欄位的資料讀取到檔案系統中,且限制其大小最多為8MiB(此為儲存到檔案系統的資料大小上限的預設值),類型必須只能是圖片。檔案的儲存的目錄預設為作業系統的暫存目錄,如果想要更改,可以設定MultipartFormDataOptions結構實體的temporary_dir欄位。另外,以上的MultipartFormDataOptions結構實體也可以將fingerprint欄位的資料讀取到記憶體中,且限制其大小最多為4KiB。也可以將name欄位的資料讀取到記憶體中,其必須可以被正常編碼成UTF-8,且限制其大小最多為1MiB(此為儲存到記憶的資料大小上限的預設值)。同時,它也可以將一個email欄位設定為重複讀取多次同名欄位(此處設為3+1次),形成陣列結構,這四個同名欄位的值必須可以被正常編碼成UTF-8,且個別的大小最多為1MiB

MultipartFormDataOptions結構實體建立並設定完成之後,就可以用來建立同樣由rocket_multipart_form_data這個crate提供的MultipartFormData結構體的實體。程式如下:

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use] extern crate rocket;

extern crate rocket_multipart_form_data;

use rocket::Data;
use rocket::http::ContentType;

use rocket_multipart_form_data::{MultipartFormDataOptions, MultipartFormDataField, MultipartFormData, Repetition};

#[post("/", data = "<data>")]
fn index(content_type: &ContentType, data: Data) {
    let mut options = MultipartFormDataOptions::with_multipart_form_data_fields(
        vec! [
            MultipartFormDataField::file("photo").content_type_by_string(Some(mime::IMAGE_STAR)).unwrap(),
            MultipartFormDataField::raw("fingerprint").size_limit(4096),
            MultipartFormDataField::text("name"),
            MultipartFormDataField::text("email").repetition(Repetition::fixed(3)),
            MultipartFormDataField::text("email"),
        ]
    );

    let multipart_form_data = MultipartFormData::parse(content_type, data, options).unwrap();
}

在使用MultipartFormData結構體的parse關聯函數來建立出MultipartFormData結構實體的同時,就會將HTTP主體中的資料解析完,此時被成功建立出來的MultipartFormData結構實體就是multipart/form-data資料的解析結果啦!

之後就是去讀取MultipartFormData結構實體中的資料,來進行處理啦!程式如下:

...

let mut multipart_form_data = MultipartFormData::parse(content_type, data, options).unwrap();

let photo = multipart_form_data.files.get("photo"); // Use the get method to preserve file fields from moving out of the MultipartFormData instance in order to delete them automatically when the MultipartFormData instance is being dropped
let fingerprint = multipart_form_data.raw.remove("fingerprint"); // Use the remove method to move raw fields out of the MultipartFormData instance (recommended)
let name = multipart_form_data.texts.remove("name"); // Use the remove method to move text fields out of the MultipartFormData instance (recommended)
let email = multipart_form_data.texts.remove("email");

if let Some(file_fields) = photo {
    let file_field = &file_fields[0]; // Because we only put one "photo" field to the allowed_fields, the max length of this file_fields is 1.

    let _content_type = &file_field.content_type;
    let _file_name = &file_field.file_name;
    let _path = &file_field.path;

    // You can now deal with the uploaded file.
}

if let Some(mut raw_fields) = fingerprint {
    let raw_field = raw_fields.remove(0); // Because we only put one "fingerprint" field to the allowed_fields, the max length of this raw_fields is 1.

    let _content_type = raw_field.content_type;
    let _file_name = raw_field.file_name;
    let _raw = raw_field.raw;

    // You can now deal with the raw data.
}

if let Some(mut text_fields) = name {
    let text_field = text_fields.remove(0); // Because we only put one "text" field to the allowed_fields, the max length of this text_fields is 1.

    let _content_type = text_field.content_type;
    let _file_name = text_field.file_name;
    let _text = text_field.text;

    // You can now deal with the text data.
}

if let Some(text_fields) = email {
    for text_field in text_fields { // We put "email" field to the allowed_fields for two times and let the first time repeat for 3 times, so the max length of this text_fields is 4.
        let _content_type = text_field.content_type;
        let _file_name = text_field.file_name;
        let _text = text_field.text;

        // You can now deal with the text data.
    }
}