Node.jsとexpressで作成したWebAPIでファイルの読み書きを行う

# 概要
Node.jsとexpressを使って作成したWebAPIで、ファイルの書き出しとファイルの読込を行いたい。

# 環境
Windows 10
Node.js `v8.9.1`
npm `5.5.1`
express `4.16.0`
express-generator
Google Chrome `71.0.3578.98`
Visual Studio Code

# 前提
前回の記事で、Node.jsとexpressで簡単なWebAPIを作成・動作確認できていること。
[Node.jsとexpressを使って簡単なWebAPIを作成する](https://qiita.com/s_harada/items/8e9413b981cfbff4db2d)

# ファイルへの書き出し（簡単編）
Nodeのfsモジュールに入っている`fs.writeFile()`でファイルへの書き出しを行う。

[nodejs.org - fs.writeFile](https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback)

## コード
```index.js
const express = require('express');
const app = express();
const fs = require('fs');

// ルート設定
app.get('/', (req, res) => res.send('Hello World'));

// api-create-request-fileの設定
app.get('/api/v1/create-request-file', (req, res) =>{
    fs.writeFile("C:/test/request.txt" , "書き込みデータ" , (err) => {
        if(err){
            return res.status(500).send("エラーが発生しました");
        }
        else{
            return res.status(200).send("ファイルが正常に書き出しされました");
        }
    });
});

// イベント待機
app.listen(3000, () => console.log('Listening on port 3000'));
```

## 実行結果
### 正常に書き出せた場合
正常にファイルに書き込みができた場合、ステータスコード200（正常）がHTTPレスポンスとして返される。

![WS000083.JPG](https://qiita-image-store.s3.amazonaws.com/0/329353/a0d58b72-2280-f27c-4a44-cf1f265410a8.jpeg)

### 正常に書き出せなかった場合
指定されたパスが無いなど、正常に書き出せなかった場合はステータスコード500（サーバエラー）がHTTPレスポンスとして返される。

![WS000082.JPG](https://qiita-image-store.s3.amazonaws.com/0/329353/d7c95f0e-69d3-a6bc-a406-c005b2c21aad.jpeg)

# ファイルへの書き出し（ちゃんとチェックする編）
簡単編ではフォルダの存在有無やアクセス権をチェックせず、writeFileでいきなりファイルの書き込みを行っているが、実際にはフォルダの存在有無やアクセス権をチェックした方がよい。

## コード

```index.js
const express = require('express');
const app = express();
const fs = require('fs');
const path = require('path');

// ルート設定
app.get('/', (req, res) => res.send('Hello World'));

// api-create-request-fileの設定
app.get('/api/v1/create-request-file', (req, res) =>{
    var filePath = "C:/test/request.txt"
    var dirName = path.dirname(filePath);
    fs.access(dirName, fs.constants.R_OK | fs.constants.W_OK, (err) => {
        if (err) {
            if (err.code === "ENOENT") {
                fs.mkdirSync(dirName);
                console.log("フォルダ " + dirName + " を作成しました");
            } else {
                console.log("フォルダ " + dirName + " の書き込み権限がありません");
                return res.status(500).send("エラーが発生しました");
            } 
        }

        fs.writeFile(filePath , "書き込みデータ" , (err) => {
            if(err){
                return res.status(500).send("エラーが発生しました");
            }
            else{
                return res.status(200).send("ファイルが正常に書き出しされました");
            }
        });
    });
});

// イベント待機
app.listen(3000, () => console.log('Listening on port 3000'));
```

## コードの解説
### fs.stat() or fs.access()
Nodeでディレクトリの有無を確かめるには`fs.stat()`か`fs.access()`を使用するが、書き込みを行うのであれば書き込み権限もまとめてチェックできた方がいいので今回は`fs.access()`を使用している。

[nodejs.org - fs.access](https://nodejs.org/api/fs.html#fs_fs_access_path_mode_callback)

`fs.access()`は第一引数にpath、第二引数（オプション）にmodeを指定し、コールバックで結果(err)が返ってくる。

### fs.access()のmodeに指定する値
デフォルトでは`fs.constants.F_OK`となっている。
`fs.constants.F_OK`は指定されたpathの存在有無しかチェックしてくれないので、権限も含めてチェックしたい場合はmodeも指定することになる。

[nodejs.org - FS Constants](https://nodejs.org/api/fs.html#fs_fs_constants_1)

| 定数 | 説明 |
|:-:|:-:|
| fs.constants.F_OK | リソースが存在するか（読み書きの権限についてはチェックしない） |
| fs.constants.R_OK | リソースが読み取り可能か |
| fs.constants.W_OK | リソースが書き込み可能か |
| fs.constants.X_OK | リソースが実行可能か（Windows環境の場合はF_OKと同じ動作となる） |

今回はファイルの読み取りと書き込みが可能かをチェックしたいので、`fs.access()`のmodeには`fs.constants.R_OK`と`fs.constants.W_OK`をパイプ(`|`)でつないで、`fs.constants.R_OK | fs.constants.W_OK`と指定することになる。

### コールバック(err)の扱い
modeで指定したチェック内容に対してエラー（存在しない、読み取り不可、書き込み不可など）がある場合、コールバックのerrに情報が詰められて返される。
err.codeに格納されたエラーコードでエラーの内容がわかる。エラーコードはLinuxのファイルシステムのエラーコードに則っているらしい。

[nodejs.org - Common System Errors](https://nodejs.org/api/errors.html#errors_common_system_errors)
[Errors: Linux System Errors](http://www-numi.fnal.gov/offline_software/srt_public_context/WebDocs/Errors/unix_system_errors.html)

| err.codeの値 | 説明 |
|:-:|:-:|
| ENOENT | 指定されたリソースが存在しない |
| EPERM | 指定されたリソースが存在するが権限が不足している |
| EBUSY | 指定されたリソースがビジー状態（他のプロセスで使用されている） |

今回は`fs.constants.R_OK | fs.constants.W_OK`でエラーが返された場合、フォルダが無い場合はフォルダを作成したい。
そのため`err.code`の値が`ENOENT`の場合（指定したフォルダが存在しない場合）は`fs.mkdirSync()`でフォルダを作成している。

[nodejs.org - fs.mkdirSync](https://nodejs.org/api/fs.html#fs_fs_mkdirsync_path_options)

# ファイルの読み込み
ファイルの読み込みはNodeのfsモジュールに組み込まれている`fs.readFile()`で行う。

[nodejs.org - fs.readFile](https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback)

## コード

```index.js
const express = require('express');
const app = express();
const fs = require('fs');
const path = require('path');

// ルート設定
app.get('/', (req, res) => res.send('Hello World'));

// api-create-request-fileの設定
app.get('/api/v1/create-request-file', (req, res) =>{
    var filePath = "C:/test/request.txt"
    var dirName = path.dirname(filePath);
    fs.access(dirName, fs.constants.R_OK | fs.constants.W_OK, (err) => {
        if (err) {
            if (err.code === "ENOENT") {
                fs.mkdirSync(dirName);
                console.log("フォルダ " + dirName + " を作成しました");
            } else {
                console.log("フォルダ " + dirName + " の書き込み権限がありません");
                return res.status(500).send("エラーが発生しました");
            } 
        }

        fs.writeFile(filePath , "書き込みデータ" , (err) => {
            if(err){
                return res.status(500).send("エラーが発生しました");
            }
            else{
                return res.status(200).send("ファイルが正常に書き出しされました");
            }
        });
    });
});

// api-read-response-fileの設定
app.get('/api/v1/read-response-file', (req, res) =>{
    var filePath = "C:/test/response.txt"
    var dirName = path.dirname(filePath);
    fs.access(dirName, fs.constants.R_OK, (err) => {
        if (err) {
            if (err.code === "ENOENT") {
                console.log("フォルダ " + dirName + " が存在しません");
            } else{
                console.log("フォルダ " + dirName + " の読み取り権限がありません");
            }
            return res.status(500).send("エラーが発生しました");
        }

        fs.readFile(filePath , "utf8", (err, data) => {
            if(err){
                return res.status(500).send("エラーが発生しました");
            }
            else{
                return res.status(200).send(data);
            }
        });
    });
});

// イベント待機
app.listen(3000, () => console.log('Listening on port 3000'));
```

## 読み込むファイルの内容

```response.txt
読み込みデータ
```

## 実行結果
### 正常に読み込めた場合
正常にファイルを読み込めた場合、ステータスコード200（正常）で読み込んだdataがHTTPレスポンスとして返される。

![WS000084.JPG](https://qiita-image-store.s3.amazonaws.com/0/329353/9b4379ab-9612-e9f3-4bef-69c80018618f.jpeg)

### 正常に読み込めなかった場合
指定されたパスが無いなど、正常に読み込めなかった場合はステータスコード500（サーバエラー）がHTTPレスポンスとして返される。

![WS000085.JPG](https://qiita-image-store.s3.amazonaws.com/0/329353/b060b5c3-ce47-1a6f-9370-716e9918c2da.jpeg)