在開發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};

let mut options = MultipartFormDataOptions::new();

options.allowed_fields.push(MultipartFormDataField::file("photo").content_type_by_string(Some("image/*")).unwrap());
options.allowed_fields.push(MultipartFormDataField::raw("fingerprint").size_limit(4096));
options.allowed_fields.push(MultipartFormDataField::text("name"));
options.allowed_fields.push(MultipartFormDataField::text("array_max_length_3"));
options.allowed_fields.push(MultipartFormDataField::text("array_max_length_3"));
options.allowed_fields.push(MultipartFormDataField::text("array_max_length_3"));

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

#[post("/", data = "<data>")]
fn index(content_type: &ContentType, data: Data) {
    let mut options = MultipartFormDataOptions::new();

    options.allowed_fields.push(MultipartFormDataField::file("photo").content_type_by_string(Some("image/*")).unwrap());
    options.allowed_fields.push(MultipartFormDataField::raw("fingerprint").size_limit(4096));
    options.allowed_fields.push(MultipartFormDataField::text("name"));
    options.allowed_fields.push(MultipartFormDataField::text("array_max_length_3"));
    options.allowed_fields.push(MultipartFormDataField::text("array_max_length_3"));
    options.allowed_fields.push(MultipartFormDataField::text("array_max_length_3"));

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

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

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

use rocket_multipart_form_data::{FileField, RawField, TextField};

...

let photo = multipart_form_data.files.get("photo");
let fingerprint = multipart_form_data.raw.get("fingerprint");
let name = multipart_form_data.texts.get("name");
let array = multipart_form_data.texts.get("array_max_length_3");

if let Some(photo) = photo {
    match photo {
        FileField::Single(file) => {
            let _content_type = &file.content_type;
            let _file_name = &file.file_name;
            let _path = &file.path;
            // You can now deal with the uploaded file. The file will be delete automatically when the MultipartFormData instance is dropped. If you want to handle that file by your own, instead of killing it, just remove it out from the MultipartFormData instance.
        }
        FileField::Multiple(_files) => {
            // Because we only put one "photo" field to the allowed_fields, this arm will not be matched.
        }
    }
}

if let Some(fingerprint) = fingerprint {
    match fingerprint {
        RawField::Single(raw) => {
            let _content_type = &raw.content_type;
            let _file_name = &raw.file_name;
            let _raw = &raw.raw;
            // You can now deal with the raw data.
        }
        RawField::Multiple(_bytes) => {
            // Because we only put one "fingerprint" field to the allowed_fields, this arm will not be matched.
        }
    }
}

if let Some(name) = name {
    match name {
        TextField::Single(text) => {
            let _content_type = &text.content_type;
            let _file_name = &text.file_name;
            let _text = &text.text;
            // You can now deal with the raw data.
        }
        TextField::Multiple(_texts) => {
            // Because we only put one "text" field to the allowed_fields, this arm will not be matched.
        }
    }
}

if let Some(array) = array {
    match array {
        TextField::Single(text) => {
            let _content_type = &text.content_type;
            let _file_name = &text.file_name;
            let _text = &text.text;
            // You can now deal with the text data.
        }
        TextField::Multiple(texts) => {
            // Because we put "array_max_length_3" field to the allowed_fields for three times, this arm will probably be matched.

            for text in texts { // The max length of the "texts" variable is 3
                let _content_type = &text.content_type;
                let _file_name = &text.file_name;
                let _text = &text.text;
                // You can now deal with the text data.
            }
        }
    }
}