第1回 - Web API と Web UI でファイルダウンロード(Web API 編)

## はじめに
今回から２回に分けてファイルをダウンロードする簡単な Web API と Web UI のサンプルを紹介します。
第1回目の今回は [Node.js](https://nodejs.org/en/) + [Express.js](http://expressjs.com/) での Web API です。
第2回目は [Vue.js](https://jp.vuejs.org/index.html) + [Vuetify](https://vuetifyjs.com/ja/) で簡単な Web UI を作り、今回の Web API を叩いて実際にファイルをダウンロードします。

1. Web API 編(←イマココ)
1. [Web UI 編](https://qiita.com/ponko2bunbun/items/514c7a167cb2049446ef)

## 環境
OS: macOS Mojave 10.14.6
Node.js: 10.16.3
Express.js: 4.17.1

## API 仕様(概略)
* GET
* URI  
`/api/download`
* クエリパラメータ  
`file=ファイル名`  
ファイル名は `sample.png` or `sample.csv` が指定可能

## ディレクトリ構成
root/
　┣ api/
　┃ ┗ download.js
　┣ static/
　┃ ┣ sample.csv
　┃ ┗ sample.png
　┗ app.js

## download.js
```js:download.js
'use strict';

const express = require('express');
const router = express.Router();
const { check, validationResult } = require('express-validator');
const path = require('path');
const fs = require('fs').promises;

const ContentType = [
    {
        ext: 'csv',
        value: 'text/csv'
    },
    {
        ext: 'png',
        value: 'image/png'
    },
]

router.get('/', validateParam(), async (req, res) => {
    const errors = validationResult(req);
    if(!errors.isEmpty()) {
        res.status(400).end();
        return;
    }

    const filepath = path.join('static', req.query.file);
    const file = await fs.readFile(filepath)
    .catch((error) => {
        console.log(error);
        res.status(404).end();
        return;
    });

    const filenames = req.query.file.split('.');
    const contentTypes = getContentType(filenames[filenames.length - 1]);
    const headers = {
        'Access-Control-Allow-Origin': req.headers.origin,
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Expose-Headers': 'Content-Disposition',
        'Access-Control-Allow-Methods': 'GET',
        'Content-Type': (contentTypes.length > 0) ? contentTypes[0].value : null,
        'Content-Disposition': 'attachment; filename=' + req.query.file
    };

    res.set(headers).send(file);
});

function validateParam() {
	return [
		check('file')
			.exists({checkNull: true})
    ];
}

function getContentType(ext) {
    return ContentType.filter((contentType) => {
        if (ext === contentType.ext) return contentType
    })
}

module.exports = router;
```

### ファイル読込
```js:download.js(抜粋)
const path = require('path');
const fs = require('fs').promises;

~中略~

    const filepath = path.join('static', req.query.file);
    const file = await fs.readFile(filepath)
    .catch((error) => {
        console.log(error);
        res.status(404).end();
        return;
    });
```

#### 解説
`fs.readFile()` で static 配下からファイルを読み込んでいます。
ファイルが無い場合は 404 を返却して終了しています。

### ヘッダー設定
```js:download.js(抜粋)
const ContentType = [
    {
        ext: 'csv',
        value: 'text/csv'
    },
    {
        ext: 'png',
        value: 'image/png'
    },
]

~中略~

    const filenames = req.query.file.split('.');
    const contentTypes = getContentType(filenames[filenames.length - 1]);
    const headers = {
        'Access-Control-Allow-Origin': req.headers.origin,
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Expose-Headers': 'Content-Disposition',
        'Access-Control-Allow-Methods': 'GET',
        'Content-Type': (contentTypes.length > 0) ? contentTypes[0].value : null,
        'Content-Disposition': 'attachment; filename=' + req.query.file
    };

~中略~

function getContentType(ext) {
    return ContentType.filter((contentType) => {
        if (ext === contentType.ext) return contentType
    })
}
```

#### 解説
ヘッダーにファイルダウンロードをさせるのに必要な設定をしていきます。
まず、 [Content-Disposition](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Disposition) に `attachment; filename=xxx` の形式で指定します。
今回はファイル形式がわかっているので、 Content-Type も指定します（決め打ちでも良かったんですがそれだとイマイチなので、適当に関数を作りましたが、結果イマイチでしたね）。
また、Web UI からのファイルダウンロードを想定するので、 `Access-Control-*` を指定して CORS 対策をします。
あと、Web UI 側でダウンロードする際、 Content-Disposition からファイル名を取得したいので、`'Access-Control-Expose-Headers': 'Content-Disposition'` を指定します。

## 動作確認
![download.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520557/492d4368-52d6-fd4d-9ae7-03214f60d2c3.gif)
画質が荒いので(スミマセン)ちょっと見づらいですが、ブラウザからURLを叩いてファイルがダウンロードされているのがわかると思います。

## まとめ
今回はファイルをダウンロードする Web API のサンプルを紹介しました。
ファイルの取得方法や、 CORS 対策は実用途によって色々とアレンジが必要になりますが、ファイルダウンロードの基本的な部分は変わりません。
また、 API の引数チェックに express-validator を使用していますが今回は説明を省いています。気になる方は[コチラ](https://qiita.com/ponko2bunbun/items/a703346bedda1ee1a4eb)もどうぞ。
[次回](https://qiita.com/ponko2bunbun/items/514c7a167cb2049446ef)は Web UI から Web API を実行し、ファイルをダウンロードします。

今回、使用したコードはGitHubで公開しています。
https://github.com/ponko2bunbun/express-file-download-api-sample