actix-webでformいろいろ

作成: 2020年12月15日

更新: 2020年12月15日

概要

この記事はRust 3 Advent Calendar 2020 - Qiitaの15日目です.

actix-webでのいろいろなForm処理について備忘録的に残そうと思います.

actix-webの基本についてはactix-web,tera,dieselを用いたWebアプリケーション開発を参照してください.

テキストパラメータのみのForm

<html>
    <head><title>Form test</title></head>
    <body>
        <form class="form-horizontal" id="simpleForm" method="POST" action="/form/memo">
            <div class="form-group">
                <label for="content">Content</label>
                <input type="text" name="content" id="content" class="form-control">
            </div>
            <button type="submit" class="btn btn-primary">Post</button>
        </form>
    </body>
</html>

actix-web,tera,dieselを用いたWebアプリケーション開発でも紹介した最も単純なformです.

公式のexampleに則って以下のように処理できます.

#[derive(Serialize, Deserialize)]
pub struct FormParams {
    content: String,
}

async fn memo_form(
    params: web::Form<FormParams>,
) -> Result<HttpResponse, Error> {
    let content = params.content;
    // 何らかの処理
}

パラメータが増えても構造体のパラメータを同様に増やせば対処できます.

ファイルアップロード

<html>
    <head><title>Upload Test</title></head>
    <body>
        <form target="/" method="post" enctype="multipart/form-data">
            <input type="file" multiple name="file"/>
            <button type="submit">Submit</button>
        </form>
    </body>
</html>

multipart/form-data 形式でバイナリデータがサーバーに送信されます.

やはり公式のexampleに従って以下のように書けます.

async fn save_file(mut payload: Multipart) -> Result<HttpResponse, Error> {
    while let Ok(Some(mut field)) = payload.try_next().await {
        let content_type = field.content_disposition().unwrap();
        let filename = content_type.get_filename().unwrap();
        let filepath = format!("./tmp/{}", sanitize_filename::sanitize(&filename));

        // ファイル作成
        let mut f = web::block(|| std::fs::File::create(filepath))
            .await
            .unwrap();

        // バイナリをチャンクに分けてwhileループ
        while let Some(chunk) = field.next().await {
            let data = chunk.unwrap();
            // ファイルへの書き込み
            f = web::block(move || f.write_all(&data).map(|_| f)).await?;
        }
    }
    Ok(HttpResponse::Ok().into())
}

ここまでは公式のexample通りです.

テキストパラメータとファイルデータの同時送信

<html>
    <head><title>Upload Test</title></head>
    <body>
        <form target="/" method="post" enctype="multipart/form-data">
            <input type="text" name="parameter" class="form-control">
            <input type="file" multiple name="file"/>
            <button type="submit">Submit</button>
        </form>
    </body>
</html>

これもFormとしては一般的ですが上記2つの方法そのままだと処理できません.

multipart/form-data には複数のフォームデータが連続して入っているのでデータの名前からどのinputから送られてきたかを判別して処理することができます.

async fn save_file_with_paramater(mut payload: Multipart) -> Result<HttpResponse, Error> {
    let mut parameter: String = String::from("");
    while let Ok(Some(mut field)) = payload.try_next().await {
        let content_type = field.content_disposition().unwrap();
        let name = content_type.get_name().unwrap();
        // テキストパラメータ
        if name == "parameter" {
            // バイナリ->Stringへ変換して変数に格納
            while let Some(chunk) = field.next().await {
                let data = chunk.unwrap();
                parameter = str::from_utf8(&data).unwrap().parse().unwrap();
            }
        // ファイルデータ
        } else if name == "file" {
            let filename = content_type.get_filename().unwrap();
            let filepath = format!("./tmp/{}", sanitize_filename::sanitize(&filename));

            // ファイル作成
            let mut f = web::block(|| std::fs::File::create(filepath))
                .await
                .unwrap();

            // バイナリをチャンクに分けてwhileループ
            while let Some(chunk) = field.next().await {
                let data = chunk.unwrap();
                // ファイルへの書き込み
                f = web::block(move || f.write_all(&data).map(|_| f)).await?;
            }
        }
    }
    // parameterの処理
    Ok(HttpResponse::Ok().into())
}

配列の送信

<html>
    <head><title>Select form Test</title></head>
    <body>
        <form target="/" method="post" enctype="multipart/form-data">
            <select name="select" multiple>
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
            </select>
            <button type="submit">Submit</button>
        </form>
    </body>
</html>

複数選択可能なselect boxです.普通に考えれば配列が返ってくるので以下のような構造体を用いれば処理できそうに見えます.

#[derive(Serialize, Deserialize)]
pub struct SelectParams {
    select: Vec<i32>,
}

しかしactix-webではこれだとエラーが出て処理できません.

以下のIssueでも議論されているようにactix-webは配列のパラメータを正しく解釈できません.

Extracting data from select multiple · Issue #259 · actix/actix-web

これはactix-webというよりactix-webが依存していてシリアライズを行うserde_urlencoded側の問題で現在はserde_qsというクレートを用いることが推奨されています.

自分はこのクレートの使い方がいまいちわからなかったので先ほどと同様にmultipart/form-dataを利用して以下のように処理しました.

async fn select(mut payload: Multipart) -> Result<HttpResponse, Error> {
    let mut select_values: Vec<i32> = Vec::new();
    while let Ok(Some(mut field)) = payload.try_next().await {
        while let Some(chunk) = field.next().await {
            let data = chunk.unwrap();
            let select_value = str::from_utf8(&data).unwrap().to_string();
            select_values.push(select_value.parse().unwrap());
        }
    }
    // select_valuesを用いた何らかの処理
    Ok(HttpResponse::Ok().into())
}

結論

multipart/form-data は最強