waffle
Version:
シンプルなWEBアプリケーションフレームワークです。(ALL YOUR NODE ARE BELONG TO US)
368 lines (316 loc) • 8.43 kB
JavaScript
/*
* Copyright 2012 Katsunori Koyanagi
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
/**
* @overview ファイル及びディレクトリの変更及び削除の監視機能が含まれます。
*/
"use strict";
require("./patch");
var fs = require("fs");
var lang = require("./Lang");
var EventEmitter = require("events").EventEmitter;
/**
* ファイルの監視機能を提供します。
* <p>
* このクラスはEventEmitterを継承し、3つのイベントをサポートします。 "watch"、"change"、"remove"です。
* </p>
* <table>
* <tr>
* <th>"watch"</th>
* <td>監視間隔によって定義された監視処理の開始で呼ばれる</td>
* <td>引数なし</td>
* </tr>
* <tr>
* <th>"change"</th>
* <td>ファイルが変更された時に呼ばれる</td>
* <td>引数にはファイルのパスが渡される</td>
* </tr>
* <tr>
* <th>"change"</th>
* <td>ディレクトリの子要素のファイルが変更削除追加された時に呼ばれる</td>
* <td>引数にはディレクトリのパスが渡される</td>
* </tr>
* <tr>
* <th>"remove"</th>
* <td>ファイル及びディレクトリが削除された時に呼ばれる</td>
* <td>引数にはファイル及びディレクトリのパスが渡される</td>
* </tr>
* </table>
*
* @class ファイルの監視機能を提供します。
* @constructor
* @param {Number}
* interval 監視間隔を示すミリ秒の値(0以下や省略した場合は1000)
*/
function FSWatcher(interval) {
EventEmitter.call(this);
this.__interval__ = interval;
this.watch0 = this.watch.bind(this);
this.onExists0 = this.onExists.bind(this);
this.onStat0 = this.onStat.bind(this);
this.onStatSubFiles0 = this.onStatSubFiles.bind(this);
this.destroyed = false;
this.count = 0;
this.files = {};
this.subFiles = {};
this.filenames = [];
this.timestamps = [];
this.index = 0;
this.subFilenames = [];
this.subTimestamps = [];
this.subIndex = 0;
this.subChanged = false;
this.destroyed = false;
this.watch();
}
require("util").inherits(FSWatcher, EventEmitter);
/**
* デフォルトの監視間隔を示すミリ秒の時間です。初期値では1000です。
*
* @type Number
*/
FSWatcher.defaultInterval = 1000;
/**
* 監視間隔を示すミリ秒の時間です。
*
* @type Number
*/
// JSDOC用のダミー
FSWatcher.prototype.interval = null;
lang.defineProperty(FSWatcher.prototype, "interval", function() {
var interval = this.__interval__;
if (interval * 0.0 !== 0.0 || interval <= 0) {
interval = FSWatcher.defaultInterval;
}
if (interval * 0.0 !== 0.0 || interval <= 0) {
interval = 1000;
}
return interval;
}, function(interval) {
this.__interval__ = interval;
});
/**
* 指定のファイルを監視対象に追加します。
*
* @param {String}
* file ファイル(フルパス)
*/
FSWatcher.prototype.add = function(file) {
if (this.destroyed) {
return;
}
if (!file || (file in this.files)) {
return;
}
if (!fs.existsSync(file)) {
return;
}
var stats = fs.statSync(file);
this.files[file] = stats.mtime.getTime();
this.count++;
if (stats.isDirectory()) {
this.updateSubFiles(file);
}
if (this.count === 1) {
setTimeout(this.watch0, this.interval);
}
};
/**
* 監視を停止します。
* <p>
* このメソッドを呼び出した以降は再利用できません。停止によって、このオブジェクトに設定されているイベントのリスナは全て解除されます。
* このメソッドを呼び出しても、直後に監視が停止されるとは限りません。非同期処理が全て完了するまでは動作します。
* </p>
*/
FSWatcher.prototype.destroy = function() {
if (this.destroyed) {
return;
}
this.destroyed = true;
this.removeAllListeners();
};
/**
* 指定のファイルを監視対象から外します。
* <p>
* 通常、監視中のファイルが削除された場合は、自動的に監視対象から外れますので、 削除のイベントの後に、このメソッドを呼び出す必要はありません。
* </p>
*
* @param {String}
* file ファイル(フルパス)
*/
FSWatcher.prototype.remove = function(file) {
if (this.destroyed) {
return;
}
if (file) {
this.count--;
delete this.files[file];
delete this.subFiles[file];
}
};
/**
* @private
*/
FSWatcher.prototype.watch = function() {
if (this.destroyed) {
return;
}
if (this.count === 0) {
return;
}
this.emit("watch");
var timestamps = this.timestamps;
var filenames = this.filenames;
var i = 0;
for ( var file in this.files) {
timestamps[i] = this.files[file];
filenames[i++] = file;
}
this.index = i;
this.watchInternal();
};
/**
* @private
*/
FSWatcher.prototype.onExists = function(result) {
if (this.destroyed) {
return;
}
var file = this.filenames[this.index];
if (!result) {
this.remove(file);
this.emit("remove", file, "remove");
this.watchInternal();
return;
}
fs.stat(file, this.onStat0);
};
/**
* @private
*/
FSWatcher.prototype.updateSubFiles = function(file) {
if (this.destroyed) {
return;
}
this.subFiles[file] = {};
var subFiles = fs.readdirSync(file);
for ( var i = 0, j = subFiles.length; i < j; i++) {
var subFile = fs.join(file, subFiles[i]);
this.subFiles[file][subFile] = fs.statSync(subFile).mtime.getTime();
}
};
/**
* @private
*/
FSWatcher.prototype.compareSubFiles = function(file) {
if (this.destroyed) {
return;
}
var timestamps = this.subTimestamps;
var filenames = this.subFilenames;
var files = this.subFiles[file];
var i = 0;
for ( var file in files) {
timestamps[i] = files[file];
filenames[i++] = file;
}
this.subIndex = i;
this.subChanged = false;
this.compareSubFilesInternal();
};
/**
* @private
*/
FSWatcher.prototype.onStatSubFiles = function(err, stats) {
if (this.destroyed) {
return;
}
var files = this.subFiles[this.filenames[this.index]];
var file = this.subFilenames[this.subIndex];
if (err) {
this.subChanged = true;
this.compareSubFilesInternal();
return;
}
var timestamp = stats.mtime.getTime();
if (timestamp !== files[file]) {
files[file] = timestamp;
this.subChanged = true;
}
this.compareSubFilesInternal();
};
/**
* @private
*/
FSWatcher.prototype.compareSubFilesInternal = function() {
if (this.destroyed) {
return;
}
if (--this.subIndex === -1) {
if (this.subChanged) {
this.emit("change", this.filenames[this.index], "change");
}
this.watchInternal();
return;
}
fs.stat(this.subFilenames[this.subIndex], this.onStatSubFiles0);
};
/**
* @private
*/
FSWatcher.prototype.onStat = function(err, stats) {
if (this.destroyed) {
return;
}
var file = this.filenames[this.index];
if (err) {
this.remove(file);
this.emit("remove", file, "remove");
this.watchInternal();
return;
}
var timestamp = stats.mtime.getTime();
if (this.timestamps[this.index] === timestamp) {
if (!stats.isDirectory()) {
this.watchInternal();
return;
}
this.compareSubFiles(file);
return;
}
if (stats.isDirectory()) {
this.updateSubFiles(file);
}
this.files[file] = timestamp;
this.emit("change", file, "change");
this.watchInternal();
};
/**
* @private
*/
FSWatcher.prototype.watchInternal = function() {
if (this.destroyed) {
return;
}
if (--this.index === -1) {
setTimeout(this.watch0, this.interval);
return;
}
fs.exists(this.filenames[this.index], this.onExists0);
};
//
// expose
//
module.exports = FSWatcher;