Express.js ファイルアップロード (express-form-data 編)


はじめに

Express.js でファイルアップロードする場合、multer と呼ばれるミドルウェアがしばしば使用されます。
しかし、ファイルアップロードに対応するミドルウェアは他にもいろいろあって express-form-data もその1つです。
express-form-data は単なるファイルアップロードのためのミドルウェアではなく、JavaScript の FormData オブジェクトを Express.js で扱うためのミドルウェアです。

Express.js では POST データはフォームから submit されたデータと JSON にのみ標準で対応しています。このため、Fetch API で FormData を POST してもリクエストボディが undefined になってしまい処理が続行できません。

そこで、express-form-data をミドルウェアにすることで、FormData も正しく扱えるようになります。

インストール

express-form-data は次のようにしてインストールできます。
npm install express-form-data

単一ファイルのアップロード

express-form-data では multer のように単一ファイル、複数ファイル、複数エレメントでコードを変える必要はなく、すべて req.files に保存されています。下の例では、input[type="file"] の name 属性が "file1" によりアップロードされたファイルを取得して、そのファイルの長い一時ファイル名を元の名前に変更するものです。

このケースでは、express-form-data をミドルウェアとして使用するので、モジュールの最初の方で次のようなコードを追加しておく必要があります。これは他の例でも同様です。
formData.parse() の引数は省略可能ですが、その場合、アップロードファイルの保存先が OS の一時フォルダになります。

const path = require('path');
const formData = require('express-form-data');
const updir = path.dirname(__dirname).replace(/\\/g, "/") + "/tmp"; // アップロード先のフォルダ
router.use(formData.parse({uploadDir:updir, autoClean:true}));

下のコードが post ハンドラの例です。multer のような第二引数は不要です。

router.post('/single', (req, res) => {
    const path1 = req.files.file1.path; // アップロードされたファイルのフルパス名
    const name = req.files.file1.name;  // 元のファイル名
    if (path1) {
        const dest = path.dirname(path1).replace(/\\/g, "/") + "/" + name;
        fs.renameSync(path1, dest);  // 一時ファイル名を元のファイル名に変更する。
        res.render('upload2', {message: dest + " にアップロードされました。"});
    }
    else {
        res.render('upload2', {message: "エラー:アップロードできませんでした。"});
    }
});

このときのフォームは次のような感じです。

<form id="form1" method="POST" enctype="multipart/form-data" action="/upload2/single">
    <fieldset class="form-group">
        <label for="file1">アップロードファイル </label>
        <input type="file" id="file1" name="file1" class="form-control" />
    </fieldset>
    <fieldset class="form-group">
        <input type="submit" class="btn btn-primary" value=" 送信 " />
    </fieldset>
    <br />
</form>
<br />
<p class="message" id="message"><%= message %></p>

複数ファイルのアップロード

express-form-data を使用する場合、アップロードファイルが単一か複数かの違いはなく req.files にそのデータが入っています。この例では input[type="file"] の name 属性が "file1" の場合、アップロードファイルの情報が req.files.file1 に入っていますが、これが配列になっており、そこからアップロードファイルの情報を取り出します。

(注意) express-form-data の初期化が必要。単一ファイルのアップロード の説明参照。

router.post('/multiple', (req, res) => {
    const n = req.files.files1.length;  // アップロードされたファイル数
    for (let i = 0; i < n; i++) {
        let src = req.files.files1[i].path.replace(/\\/g, "/");
        let dest = path.dirname(req.files.files1[i].path).replace(/\\/g, "/") + "/" + req.files.files1[i].name;
        fs.renameSync(src, dest);  // 長い一時ファイル名から元の名前に変更する。
    }
    if (n > 0) {
        res.render('upload2Multi', {message: n + " 個のファイルがアップロードされました。"});
    }
    else {
        res.render('upload2Multi', {message: "エラー:アップロードできませんでした。"});
    }
});

この例でのフォームは次のようになっています。

<form id="form1" method="POST" enctype="multipart/form-data" action="/upload2/multiple">
    <fieldset class="form-group">
        <label for="file1">アップロードファイル </label>
        <input type="file" id="files1" name="files1" class="form-control" multiple />
    </fieldset>
    <fieldset class="form-group">
        <input type="submit" class="btn btn-primary" value=" 送信 " />
    </fieldset>
    <br />
</form>
<br />
<p class="message" id="message"><%= message %></p>

Fetch API と FormData を使用してのアップロード

express-form-data を使うと FormData オブジェクトとして input[type="file"] を含むフォームを丸ごとサーバへ送信できるので、クライアント側のコードが簡単になります。
(注意) express-form-data の初期化が必要。単一ファイルのアップロード の説明参照。

router.post('/formdata', (req, res) => {
    const path1 = req.files.file1.path.replace(/\\/g, "/"); // アップロードされたファイルのフルパス名
    const name = req.files.file1.name;  // 元のファイル名
    if (path1) {
        const dest = path.dirname(path1).replace(/\\/g, "/") + "/" + name;
        fs.renameSync(path1, dest);  // 一時ファイル名を元のファイル名に変更する。
        res.send(dest + " にアップロードされました。");
    }
    else {
        res.send("エラー:アップロードできませんでした。");
    }
});

次のコードは、input[type="file"] エレメントを含むフォームの例です。アップロードの結果は id="message" で指定される p タグの内部文字列として表示されます。

<form id="form1" method="POST" enctype="multipart/form-data">
    <fieldset class="form-group">
        <label for="file1">アップロードファイル </label>
        <input type="file" id="file1" name="file1" class="form-control" />
    </fieldset>
    <fieldset class="form-group">
        <input type="button" class="btn btn-primary" value=" 送信 " onclick="javascript:submitClick()" />
    </fieldset>
    <br />
</form>
<br />
<p class="message" id="message"></p>

次がクライアント側のコードで、送信ボタンをクリックしたときのハンドラです。fetch 関数のパラメータが FormData オブジェクトなのですっきりします。

function element(id) {
    return document.getElementById(id);
}

function submitClick() {
    const formData = new FormData(form1);
    fetch('/upload2/formdata', {method:"POST", body:formData})
    .then(res => res.text())
    .then(text => element('message').innerText = text)
    .catch(err => element('message').innerHTML = err.message);
}

FormData を使った複数エレメントのファイルアップロード

この例は複数 input[type="file"] エレメント (name="file1", name="file2") を持つフォームからのファイルアップロードのコードです。基本的に単一ファイルの場合と同じで、name="file1", name="file2" について同じコードを書くだけです。

router.post('/formdata2', (req, res) => {
    const path1 = req.files.file1.path.replace(/\\/g, "/"); // アップロードされたファイルのフルパス名 (1)
    const path2 = req.files.file2.path.replace(/\\/g, "/"); // アップロードされたファイルのフルパス名 (2)
    const name1 = req.files.file1.name;  // 元のファイル名
    const name2 = req.files.file2.name;  // 元のファイル名
    if (path1) {
        var dest1 = path.dirname(path1).replace(/\\/g, "/") + "/" + name1;
        fs.renameSync(path1, dest1);  // 一時ファイル名を元のファイル名に変更する。
        if (path2) {
            var dest2 = path.dirname(path2).replace(/\\/g, "/") + "/" + name2;
            fs.renameSync(path2, dest2);  // 一時ファイル名を元のファイル名に変更する。
        }
        res.send(`"${dest1}", "${dest2}" にアップロードされました。`);
    }
    else {
        res.send("エラー:アップロードできませんでした。");
    }
});

次のコードは、この場合のフォームの例です。

<form id="form1" method="POST" enctype="multipart/form-data">
    <fieldset class="form-group">
        <label for="file1">アップロードファイル (1)</label>
        <input type="file" id="file1" name="file1" class="form-control" />
    </fieldset>
    <fieldset class="form-group">
        <label for="file1">アップロードファイル (2)</label>
        <input type="file" id="file2" name="file2" class="form-control" />
    </fieldset>
    <fieldset class="form-group">
        <input type="button" class="btn btn-primary" value=" 送信 " onclick="javascript:submitClick()" />
    </fieldset>
    <br />
</form>
<br />
<p class="message" id="message"></p>

次のコードは、クライアント側の JavaScript コードの例です。

function element(id) {
    return document.getElementById(id);
}

// 送信ボタンがクリックされたとき
function submitClick() {
    const formData = new FormData(form1);  // form1 を元に FormData オブジェクトを作成する。
    fetch('/upload2/formdata2', {method:"POST", body:formData})
    .then(res => res.text())
    .then(text => element('message').innerText = text)
    .catch(err => element('message').innerHTML = err.message);
}

express-form-data を使うと multer より簡単にファイルアップロードを行えます。特に、Fetch API を使う場合は、FormData が使えるので非常に簡単になります。

終わり