Node.js - chokidar で CSVファイルの変更検知して deep-diff で差分比較した情報をログ出力する

## はじめに

「[Node.js - chokidar と forever でファイルとディレクトリの変更を検知してログする - Qiita](https://qiita.com/anfangd/items/bbd0ae338588ab971110)」で実装したファイル検知の仕組みを応用して、CSVファイルの読み込みと差分検査を行う処理を実装してみました。

## 作るもの

今回の実装するアプリケーションの概要をまとめます。

### 前提条件

前提として、以下の `npm` と `node` がインストール済みであるものとします。

```powershell:command_powershell
> node -v
v10.16.0

> npm -v
6.10.1

```

### 動作仕様

今回のアプリケーションは次のような構成で動作することを想定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79222/542b50dd-df6f-bcc2-283a-10a18716af22.png)


アプリケーションは次のような機能を持ちます。

- ファイルの変更検知は `chokidar` を使用します。
- **変更検知する対象ディレクトリ**は **複数の異なるディレクトリ** です。
- **CSVファイルが更新されたら、既存のバックアップファイルと差分を比較**します。
- **比較結果はコンソール出力**します。
- 比較が終わったら、**既存ファイルを更新されたファイルで上書き**ます。


## 実装

実装に関する情報をまとめます。

### バージョン

関連モジュールのバージョンは次の通りです。

- node : v10.16.0
- npm : 6.10.1
- chokidar : 3.0.2
- fs : 0.0.1-security
- csv-parse : 4.4.3
- deep-diff : 1.0.2
- forever : 1.0.0

### 初期設定

まず、リポジトリを作成し、必要なモジュールをインストールします。

```bash:command_bash
# プロジェクトフォルダの作成
> mkdir node-app && cd $_

# リポジトリの初期化
> npm init -y

# 関連モジュールのインストール
> npm install --save chokidar
> npm install fs
> npm install csv-parse
> npm install deep-diff

# Global Install.
> sudo npm install -g forever

```

最終的には次のようなディレクトリ構成になります。
矢印で示した資源は自分で作成します。

```bash:command_bash
> tree -L 2
├── backup
│   └── input.csv        <-- 既存のバックアップファイル
├── data
│   └── input.csv        <-- 更新するファイル（１）
├── data-001
│   └── input.csv        <-- 更新するファイル（２）
├── node_modules
├── package-lock.json
├── package.json
└── watch.js             <-- アプリケーション本体
```

### コード

完成版のコードは次の通りです。

<details><summary>watch.js</summary><div>

```js:watch.js
'use strict';

// require
var chokidar = require("chokidar");
const fs = require('fs');
const csvSync = require('csv-parse/lib/sync');
var diff = require('deep-diff')

// Constants
// ________________________________
// Monitoring Target Directory
const target_directory_01 = 'data/';
const target_directory_02 = 'data-001/';
const backup_filename = 'backup/input.csv';

// Something to use when events are received.
const log = console.log.bind(console);

// Initialize
// ________________________________
// initialize chokidar
var watcher = chokidar.watch(target_directory_01, {
    ignored: /[\/\\]\./,
    persistent: true
});
// Add monitoring target.
watcher.add(target_directory_02);

// Monitoring
// ________________________________
// Ready for changes
watcher.on('ready', function () {

    // ready
    log('Initial scan complete. Ready for changes');

    // watched Paths
    var watchedPaths = watcher.getWatched();
    log("watchedPaths :", watchedPaths);

    // Files
    // _ _ _ _ _ _ _ _ _ _ _ _ _ _
    // Detect File added
    watcher.on('add', function (path, stats) {
        log(`File ${path} has been added`);
        if (stats) log(`File ${path} added size to ${stats.size}`, stats);

        var watchedPaths = watcher.getWatched();
        log("watchedPaths :", watchedPaths);
    });

    // Detect File changed
    watcher.on('change', function (path, stats) {
        log(`File ${path} has been changed`);
        if (stats) log(`File ${path} changed size to ${stats.size}`, stats);

        // 変更前後の CSVファイルを読み込みオブジェクトに変換する。
        let updatedData = readCSV(path);
        let originalData = readCSV(backup_filename);

        // 変更前後の CSVファイルの差分を検査する。
        var differences = diff(originalData, updatedData);
        log("differences: ", differences);
        log("differences: ", JSON.stringify(differences));
        
        // 変更後のCSVファイルで、変更前のCSVファイルを上書きする。
        fs.copyFileSync(path, backup_filename);
    });

    // Detect File removed
    watcher.on('unlink', function (path) {
        log(`File ${path} has been removed`);
    });

    // Directories
    // _ _ _ _ _ _ _ _ _ _ _ _ _ _
    // Detect Directory added
    watcher.on('addDir', function (path) {
        log(`Directory ${path} has been added`);
    });

    // Detect Directory removed
    watcher.on('unlinkDir', function (path) {
        log(`Directory ${path} has been removed`);
    });
    
    // Error
    // _ _ _ _ _ _ _ _ _ _ _ _ _ _
    // Detect Watcher Error
    watcher.on('error', function (path) {
        log(`Watcher error: ${error}`);
    });
    
});

/**
 * CSVファイルを読み込む。
 * 
 * @param {String} path 
 * @return {Object} res 
 */
function readCSV(path) {
    let data = fs.readFileSync(path);
    let res = csvSync(data);    
    return res;
}

```

</div></details>

### データ

事前に用意するテスト用の CSVデータは次の通りです。

```csv:data/input.csv
2019/07/15,yamada,000001,100
2019/07/16,tanaka,000100,240
2019/07/17,sasaki,010000,260
2019/07/19,suzuki,200000,400
2019/07/20,okayama,003000,301
```
```csv:backup/input.csv
2019/07/15,yamada,000001,100
2019/07/16,tanaka,000100,240
2019/07/17,sasaki,010000,260
2019/07/19,suzuki,200000,400
2019/07/20,okayama,003000,301
```

### デバッグ

動作確認は次のように行います。
`node watch.js` でアプリケーションを起動したら、監視対象のフォルダ配下で適当な操作をしてみましょう。

```bash:command_bash
> node watch.js
Initial scan complete. Ready for changes
watchedPaths : {}

# `data/input.csv` に次の行を追加して保存します。
# 2019/07/20,okayama,003000,303

File data/input.csv has been changed
File data/input.csv changed size to 180 Stats {
  dev: 12,
  mode: 33279,
  nlink: 1,
  uid: 1000,
  gid: 1000,
  rdev: 0,
  blksize: 512,
  ino: 11258999068805782,
  size: 180,
  blocks: 0,
  atimeMs: 1563177207926.31,
  mtimeMs: 1563181200379.9946,
  ctimeMs: 1563181200379.9946,
  birthtimeMs: 1563181200379.9946,
  atime: 2019-07-15T07:53:27.926Z,
  mtime: 2019-07-15T09:00:00.380Z,
  ctime: 2019-07-15T09:00:00.380Z,
  birthtime: 2019-07-15T09:00:00.380Z }
differences:  [ DiffArray {
    kind: 'A',
    index: 5,
    item: DiffNew { kind: 'N', rhs: [Array] } } ]
differences:  [{"kind":"A","index":5,"item":{"kind":"N","rhs":["2019/07/20","okayama","003000","303"]}}]
```

## おわりに

`Node.js` の使い勝手の良さに驚いています。

## 参考

今回の記事で参考にしたサイトを記載します。

### csv-parse

- [node.jsでcsvを同期読み込み(csv-parseを使用) - Qiita](https://qiita.com/daikiojm/items/28be6d7c87db6ea7c74b)
- [csv-parse - npm](https://www.npmjs.com/package/csv-parse)

### `use strict`

- [【JavasScript】use strictとは - Qiita](https://qiita.com/miri4ech/items/ffcebaf593f5baa1c112)
- [Node.jsにおけるuse strictの挙動 - Qiita](https://qiita.com/knknkn1162/items/495b2b767f48d83a6ea5)
- [Node.jsでlog4jsを使ってログを出力する - Developers.IO](https://dev.classmethod.jp/node-js/node-js-log4js/)

### fs

- [node.jsでファイルをコピーする - 山本隆の開発日誌](https://www.gesource.jp/weblog/?p=8211)

### deep-diff

- [deep-diff - npm](https://www.npmjs.com/package/deep-diff)