複数のファイルをアップロードしてExpressサーバーで保存する


# 概要

クライアントアプリからExpressサーバーへの複数ファイルのアップロード時にハマったので、
方法を記載します。

# 環境

開発環境はMacでNodebrewを使っています。

```
$ nodebrew 
nodebrew 1.0.1
$ node -v
v12.14.1
```

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

まず単一ファイルのアップロード方法を記載します。

## Expressサーバー立ち上げ

express-generatorでExpressサーバーを作成します。

```
$ npm install -g express-generator
```

express-generatorをインストールしてexpressコマンドが使えるようになったので
アプリを作成します。

```
$ express --view=ejs express-app

   create : express-app/
   create : express-app/public/
   create : express-app/public/javascripts/
   create : express-app/public/images/
   create : express-app/public/stylesheets/
   create : express-app/public/stylesheets/style.css
   create : express-app/routes/
   create : express-app/routes/index.js
   create : express-app/routes/users.js
   create : express-app/views/
   create : express-app/views/error.ejs
   create : express-app/views/index.ejs
   create : express-app/app.js
   create : express-app/package.json
   create : express-app/bin/
   create : express-app/bin/www

   change directory:
     $ cd express-app

   install dependencies:
     $ npm install

   run the app:
     $ DEBUG=express-app:* npm start
```

上の手順の通りにサーバーを立ち上げます。

```
$ cd express-app/
$ npm install
$ npm start
```

以下のURLにアクセスしてサーバーに接続できることを確認します。
http://localhost:3000

<img width="2160" alt="スクリーンショット 2020-01-12 15.52.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/68af3d0f-3597-d9ec-f059-c365d9d733a3.png">


## アップロードAPI作成

サーバーを立ち上げることができたので、一度停止してアップロードAPIを作成します。
app.jsを以下のように修正します。

``` app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
// 追加
var uploadRouter = require('./routes/upload');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);
// 追加
app.use('/upload', uploadRouter);
```

FormDataを処理するためにmulterを追加します。
https://github.com/expressjs/multer

```
$ npm install multer
```

routesフォルダ内にupload.jsを作成します。
destで指定したフォルダ内にアップロードされたファイルが保存されます。

``` upload.js
var express = require('express');
var multer  = require('multer');
var upload = multer({ dest: 'uploads/' });
var router = express.Router();

router.post('/', upload.single('file'), function(req, res, next) {
  console.log(req.file);
  console.log(req.body);
  res.send('upload success');
});

module.exports = router;
```

これでAPIが作成できたのでnpm startでサーバーを立ち上げておきます。

## アップロードフォーム作成

クライアントアプリ側に以下のようなファイルを作成してアップロードフォームを用意します。
Ajaxを使ってファイルと一緒に適当なパラメータも送信しています。

``` upload.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Uploader</title>
<script
  src="https://code.jquery.com/jquery-3.4.1.min.js"
  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  crossorigin="anonymous"></script>
</head>
<body>
    <div>
        <input type="file" id="upload">
        <input type="button" id="uploadButton" value="送信">
    </div>
<script>
$(function(){
    $('#uploadButton').click(function() {
        const files = $('#upload')[0].files;
        const formData = new FormData();
        formData.append('file', files[0]);
        formData.append('hoge', 123);

        $.ajax({
            url: 'http://localhost:3000/upload',
            method: 'post',
            data: formData,
            processData: false,
            contentType: false
        }).done(function(res){
            console.log(res);
        }).fail(function(err) {
            console.log(err);
        })
    })
});
</script>
</body>
</html>
```

## ファイルアップロード

Expressサーバーを立ち上げている状態で上で作成したupload.htmlをブラウザで開いて、
ファイルを選択後に送信ボタンを押下します。

Expressのコンソールに以下のような値が出力されています。
req.fileにアップロードしたファイルの情報、req.bodyにパラメータが格納されています。

```
{
  fieldname: 'file',
  originalname: 'upload_file.txt',
  encoding: '7bit',
  mimetype: 'text/plain',
  destination: 'uploads/',
  filename: '12dee747383a844dd7d1888578cf720e',
  path: 'uploads/12dee747383a844dd7d1888578cf720e',
  size: 247379
}
[Object: null prototype] { hoge: '123' }
POST /upload 200 5.874 ms - 14
```

multerのdestにuploadsを指定したので、Expressアプリのルートディレクトリにuploadsフォルダが作成されて
その中にアップロードしたファイルが保存されています。

もし送信ボタン押下時にクロスドメインのエラーが発生する場合は、
以下のようにExpressサーバーにCORSの許可設定を行なってください。

## CORS対応

ファイルアップロード時にクロスドメインエラーが発生した場合はExpressにCORSの許可を設定する必要があります。
方法はいくつかありますが、今回はcorsモジュールを使用します。

```
$ npm install cors
```

上のコマンドを実行後にapp.jsに以下を追記してください。
今回は全リクエストを許可としていますが、本番運用などする際は適切に設定を行なってください。

``` app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
// 追加
var cors = require('cors');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var uploadRouter = require('./routes/upload');

var app = express();

// 追加
app.use(cors());
```

この状態でサーバーを立ち上げてファイルアップロードを行うとクロスドメインエラーが発生しないようになっています。

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

前置きが長くなりましたが、次に複数ファイルのアップロードを行います。

## アップロードAPI作成

まずExpressに複数ファイルのアップロードAPIを作成します。
比較用に単一ファイル用のAPIも残しています。

``` upload.js
var express = require('express');
var multer  = require('multer');
var upload = multer({ dest: 'uploads/' });
var router = express.Router();

// 単一ファイルアップロード
router.post('/', upload.single('file'), function(req, res, next) {
  console.log(req.file);
  console.log(req.body);
  res.send('upload success');
});

// 追加
// 複数ファイルアップロード
router.post('/multiple', upload.array('files'), function(req, res, next) {
  console.log(req.files);
  console.log(req.body);
  res.send('multiple upload success');
});

module.exports = router;
```

## アップロードフォーム作成

クライアント側にも複数ファイルのアップロードフォームを追加します。
ポイントは複数ファイルをFormDataに追加する際に'files'を指定する部分です。

``` upload.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Uploader</title>
<script
  src="https://code.jquery.com/jquery-3.4.1.min.js"
  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  crossorigin="anonymous"></script>
</head>
<body>
    <!-- 単一ファイルアップロードフォーム -->
    <div>
        <input type="file" id="upload">
        <input type="button" id="uploadButton" value="送信">
    </div>
    <!-- 追加 -->
    <!-- 複数ファイルアップロードフォーム -->
    <div>
        <input type="file" id="multipleUpload" multiple>
        <input type="button" id="multipleUploadButton" value="送信">
    </div>
<script>
$(function(){
    // 単一ファイルアップロード
    $('#uploadButton').click(function() {
        const files = $('#upload')[0].files;
        const formData = new FormData();
        formData.append('file', files[0]);
        formData.append('hoge', 123);

        $.ajax({
            url: 'http://localhost:3000/upload',
            method: 'post',
            data: formData,
            processData: false,
            contentType: false
        }).done(function(res){
            console.log(res);
        }).fail(function(err) {
            console.log(err);
        })
    })

    // 追加
    // 複数ファイルアップロード
    $('#multipleUploadButton').click(function() {
        const files = $('#multipleUpload')[0].files;
        const formData = new FormData();
        for (let i = 0; i < files.length; i++) {
            formData.append('files', files[i]);
        }
        formData.append('hoge', 123);

        $.ajax({
            url: 'http://localhost:3000/upload/multiple',
            method: 'post',
            data: formData,
            processData: false,
            contentType: false
        }).done(function(res){
            console.log(res);
        }).fail(function(err) {
            console.log(err);
        })
    })
});
</script>
</body>
</html>
```

## ファイルアップロード

Expressサーバーを立ち上げている状態で、先ほどと同じようにブラウザから複数ファイルを選択してアップロードを行います。
Expressのコンソールでファイルの情報が出力されて、uploadsフォルダにファイルが保存されていることが確認できます。

```
[
  {
    fieldname: 'files',
    originalname: 'upload_file 2.txt',
    encoding: '7bit',
    mimetype: 'text/plain',
    destination: 'uploads/',
    filename: '2c8436e8d77723dfaf7a75e38fe1785c',
    path: 'uploads/2c8436e8d77723dfaf7a75e38fe1785c',
    size: 247379
  },
  {
    fieldname: 'files',
    originalname: 'upload_file.txt',
    encoding: '7bit',
    mimetype: 'text/plain',
    destination: 'uploads/',
    filename: '5ed6ed7671d65f269dfaa2c456b2b95b',
    path: 'uploads/5ed6ed7671d65f269dfaa2c456b2b95b',
    size: 247379
  }
]
[Object: null prototype] { hoge: '123' }
POST /upload/multiple 200 19.628 ms - 23
```

# まとめ

単一ファイルのアップロードは割と簡単に実装できましたが、
複数ファイルのアップロードでのクライアントからのFormDataへの追加と
サーバーでの保存がなかなかうまくいかずハマってしまいました。

Formタグを使えばもう少し簡単に実装できたかもしれませんが、
今回は使用せずに実装したかったためこのような方法になりました。