inotifywait
Version:
Yet another nodejs fs.watch/inotify implementation. Good for big directories structures and lot of files.
207 lines (181 loc) • 5.81 kB
JavaScript
/*jslint node: true, maxlen: 100, maxerr: 50, indent: 2 */
;
var fs = require('fs');
var util = require('util');
var spawn = require('child_process').spawn;
var Lazy = require('lazy');
var EventEmitter = require('events').EventEmitter;
var path = require("path");
// Constructor
var INotifyWait = function(wpath, options) {
var self = this;
self.wpath = wpath;
self.options = mixin({
bin: 'inotifywait',
recursive: true,
watchDirectory: false,
excludes: [],
files: [],
events: [],
spawnArgs: {}
}, options);
self.currentEvents = {};
self.runProcess = function () {
var args = [
(self.options.recursive ? '-r' : ''),
'--format',
'{ "type": "%e", "file": "%w%f", "date": "%T" }',
'--timefmt',
'%s',
'-m'
];
// having --exclude
if(self.options.excludes.length > 0) {
self.options.excludes.forEach(function(item){
args.push("--exclude");
args.push(item);
});
}
// having @
if(self.options.files.length > 0) {
self.options.files.forEach(function(item){
args.push("@" + item);
});
}
// having --event
if(self.options.events.length > 0) {
self.options.events.forEach(function (item) {
args.push("--event");
args.push(item);
});
}
//add path
args.push(wpath);
// run inotifywait command in background
self.inwp = spawn(self.options.bin, args, self.options.spawnArgs);
self.inwp.on('close', function (err) {
self.inwp = null;
self.emit('close', err);
});
self.inwp.on('error', function (err) {
self.emit('error', err);
});
// parse stdout of the inotifywatch command
Lazy(self.inwp.stdout)
.lines
.map(String)
.map(function (line) {
try {
return JSON.parse(line);
} catch (err) {
self.emit('error', new Error( err + ' -> ' + line));
return { type: '', file: '' , date: new Date()};
}
})
.map(function (event) {
event.type = event.type.split(',');
// Unix Epoch * 1000 = JavaScript Epoch
event.date = new Date(event.date * 1000);
return event;
})
.forEach(function (event) {
// skip directories ?
var isDir = (event.type.indexOf('ISDIR') != -1);
if (isDir && !self.options.watchDirectory) {
return;
}
var stats = {isDir: isDir, date: event.date};
if (event.type.indexOf('CREATE') != -1) {
self.currentEvents[event.file] = 'add';
fs.lstat(event.file, function (err, lstats) {
if (!err && !lstats.isDirectory() && (lstats.isSymbolicLink() || lstats.nlink > 1)) {
// symlink and hard link does not receive any CLOSE event
self.emit('add', event.file, stats);
delete self.currentEvents[event.file];
}
});
} else if (event.type.indexOf('MOVED_TO') != -1) {
self.currentEvents[event.file] = 'move';
fs.lstat(event.file, function (err, lstats) {
if (!err && !lstats.isDirectory()) {
// symlink and hard link does not receive any CLOSE event
self.emit('move', event.file, stats);
delete self.currentEvents[event.file];
}
});
} else if (event.type.indexOf('MODIFY') != -1 || // to detect modifications on files
event.type.indexOf('ATTRIB') != -1) { // to detect touch on hard link
if (self.currentEvents[event.file] != 'add') {
self.currentEvents[event.file] = 'change';
}
} else if (event.type.indexOf('DELETE') != -1) {
self.emit('unlink', event.file, stats);
} else if (event.type.indexOf('CLOSE') != -1) {
if (self.currentEvents[event.file]) {
self.emit(self.currentEvents[event.file], event.file, stats);
delete self.currentEvents[event.file];
} else {
self.emit('unknown', event.file, event, stats);
}
}
});
// parse stderr of the inotifywatch command
Lazy(self.inwp.stderr)
.lines
.map(String)
.forEach(function (line) {
if (/^Watches established/.test(line)) {
// tell when the watch is ready
self.emit('ready', self.inwp);
} else if (/^Setting up watches/.test(line)) {
// ignore this message
} else {
self.emit('error', new Error(line));
}
});
// Maybe it's not this module job to trap the SIGTERM event on the process
// ======>
// check if the nodejs process is killed
// then kill inotifywait shell command
// process.on('SIGTERM', function () {
// if (self.inwp) {
// self.inwp.kill();
// }
// });
};
self.runProcess();
}
INotifyWait.prototype = Object.create(EventEmitter.prototype);
INotifyWait.prototype.close = function (cb) {
// if already killed
if (!this.inwp) {
if (cb) {
this.removeAllListeners(); // cleanup
return cb(null);
}
return;
}
// if not already killed
this.on('close', function (err) {
this.removeAllListeners(); // cleanup
if (cb) {
return cb(err);
}
});
this.inwp.kill();
};
module.exports = INotifyWait;
/**
* Mixing object properties.
*/
var mixin = function() {
var mix = {};
[].forEach.call(arguments, function(arg) {
for (var name in arg) {
if (arg.hasOwnProperty(name)) {
mix[name] = arg[name];
}
}
});
return mix;
};