File: app/server/watcher/DirectoryWatcher.js
'use strict';
/**
* @module watcher
*/
var path = require('path');
var util = require('util');
var events = require('events');
var fs = require('fs');
var async = require('async');
var openVeoApi = require('@openveo/api');
var DirectoryFsWatcher = process.requirePublish('app/server/watcher/DirectoryFsWatcher.js');
var WatcherError = process.requirePublish('app/server/watcher/WatcherError.js');
/**
* Fired when an error occurred.
*
* @event error
* @param {WatcherError} The error
*/
/**
* Fired when a new resource (file or directory) has been added to the watched folder.
*
* @event create
* @param {String} Path of the added resource
*/
/**
* Fired when a resource (file or directory) has been deleted from the watched folder.
*
* @event delete
* @param {String} Path of the resource before it has been removed
*/
/**
* Fired when a directory is added to the watched directory.
*
* Fired after "create" event in case the directory is added to the watched directory.
*
* @event watch
* @param {String} Path of the directory
*/
/**
* Defines a directory watcher to watch for changes inside a directory and its sub directories.
*
* @class DirectoryWatcher
* @constructor
* @param {Array} directoryPath The absolute path of the directory to watch
* @param {Object} [options] Watcher options
* @param {Number} [options.stabilityThreshold] Number of milliseconds to wait before considering a file
* as stable
*/
function DirectoryWatcher(directoryPath, options) {
Object.defineProperties(this, {
/**
* The absolute path of the watched directory.
*
* @property directoryPath
* @type String
* @final
*/
directoryPath: {
value: path.resolve(directoryPath)
},
/**
* List of watchers for this directory and its sub directories.
*
* @property fsWatchers
* @type Array
* @final
*/
fsWatchers: {
value: []
},
/**
* Watcher options.
*
* @property options
* @type Object
* @final
*/
options: {
value: options
}
});
}
module.exports = DirectoryWatcher;
util.inherits(DirectoryWatcher, events.EventEmitter);
/**
* Checks if a sub directory is actually being watched.
*
* @method isWatched
* @private
* @param {String} directoryPath The absolute path of the directory to check
* @return {Boolean} true if directory is actually being watched, false otherwise
*/
function isWatched(directoryPath) {
directoryPath = path.resolve(directoryPath);
for (var i = 0; i < this.fsWatchers.length; i++) {
if (directoryPath === this.fsWatchers[i].directoryPath)
return true;
}
return false;
}
/**
* Creates a watcher on the given directory.
*
* @method createWatcher
* @private
* @async
* @param {String} directoryPath The absolute path of the directory to watch
* @param {Function} callback The function to call when its done
* - **Error** An error if something went wrong
*/
function createWatcher(directoryPath, callback) {
// Make sure the directory is not already watched before going any further
if (isWatched.call(this, directoryPath)) return;
var directoryFsWatcher = new DirectoryFsWatcher(directoryPath, this.options);
// Listen to new resources in directory or sub directories
directoryFsWatcher.on('create', (function(resourcePath) {
this.emit('create', resourcePath);
// Analyze the new resource to add a watcher on it if it is a directory
fs.stat(resourcePath, (function(error, stats) {
// If resource has not been found (ENOENT) it's because, meanwhile, it has been removed
if (error) {
if (error.code !== 'ENOENT')
return this.emit('error', new WatcherError(error.message, error.code, resourcePath));
} else if (stats.isDirectory())
createWatcher.call(this, resourcePath);
}).bind(this));
}).bind(this));
// Listen to removed resources
directoryFsWatcher.on('delete', (function(resourcePath) {
// Stop watching the resource if it was a directory
this.close(resourcePath);
this.emit('delete', resourcePath);
}).bind(this));
// Listen to watcher errors
directoryFsWatcher.on('error', (function(error) {
this.emit('error', error);
}).bind(this));
this.fsWatchers.push(directoryFsWatcher);
directoryFsWatcher.watch((function(error) {
if (!error) this.emit('watch', directoryPath);
callback && callback(error);
}).bind(this));
}
/**
* Watches a directory and all its sub directories.
*
* @method watch
* @async
* @param {Function} callback The function to call when its done
* - **Error** An error if something went wrong
*/
DirectoryWatcher.prototype.watch = function(callback) {
// Get all resources of the directory (recursively)
openVeoApi.fileSystem.readdir(this.directoryPath, (function(error, resources) {
if (error) return callback(error);
var asyncTasks = [];
// Watch sub directories for changes
resources.forEach((function(resource) {
if (resource.isDirectory()) {
// Resource is a sub directory
// Watch it
asyncTasks.push((function(callback) {
createWatcher.call(this, resource.path, callback);
}).bind(this));
}
}).bind(this));
// Finally watch the directory itself
asyncTasks.push((function(callback) {
createWatcher.call(this, this.directoryPath, callback);
}).bind(this));
async.parallel(asyncTasks, callback);
}).bind(this));
};
/**
* Stops watching the directory and all its sub directories.
*
* @method close
* @param {String} [directoryPath] Absolute path of the directory to stop watching. If not specified
* the directory and all its sub directories won't be watched anymore
*/
DirectoryWatcher.prototype.close = function(directoryPath) {
var index = -1;
for (var i = 0; i < this.fsWatchers.length; i++) {
var fsWatcher = this.fsWatchers[i];
if (directoryPath) {
if (directoryPath === fsWatcher.directoryPath) {
index = i;
fsWatcher.close();
break;
}
} else {
fsWatcher.close();
}
}
if (directoryPath) {
if (index > -1) this.fsWatchers.splice(index, 1);
} else
this.fsWatchers.splice(0, this.fsWatchers.length);
};