castor-load
Version:
Traverse a directory to build a MongoDB collection with the found files. Then it's enable to keep directory and collection synchronised.
393 lines (356 loc) • 9.46 kB
JavaScript
;
var path = require('path')
, basename = path.basename(__filename, '.js')
, debug = require('debug')('castor:load:' + basename)
, util = require('util')
, assert = require('assert')
, async = require('async')
, extend = require('extend')
, File = require('./file.js')
, mkdirp = require('mkdirp')
, parseBytes = require('bytes')
;
function Sync(mountor, options)
{
if (!(this instanceof Sync)) {
return new Sync(mountor, options);
}
var self = this;
self.options = {};
self.options.collectionName = options.collectionName || 'filerake';
self.options.connexionURI = options.connexionURI || 'mongodb://localhost:27017/test/';
self.options.connexion = require('url').parse(self.options.connexionURI);
self.options.concurrency = options.concurrency || 1;
self.options.maxFileSize = options.maxFileSize || '128mb';
self.options.delay = options.delay || 1000;
self.options.synckey = options.synckey || null;
self.dateSynchronised = new Date();
self.mountor = mountor;
self.checker = [];
function worker(file, done) {
self.compare(file, function(err) {
if (err) {
debug('error', file.doc.filename, err);
}
done(err);
});
}
self.queue = async.queue(worker, self.options.concurrency);
self.queue.drain = function () {
debug('queue drain', self.dateSynchronised);
}
}
Sync.prototype.connect = function (fn) {
var self = this;
if (self.coll) {
return fn(null, self.coll, self.db);
}
var func = function (err, db) {
assert.ifError(err, 'connect failed');
assert.notEqual(db, null, 'connect failed');
db.collection(self.options.collectionName, function(err, coll) {
self.db = db;
self.coll = coll;
self.coll.ensureIndex({ fid: 1 }, {unique: true, dropDups:true, w: 1 }, function(err, indexName) {
fn(err, self.coll, self.db);
})
});
}
if (self.options.connexion.protocol === 'mongodb:') {
require('mongodb').MongoClient.connect(self.options.connexionURI, func);
}
else if (self.options.connexion.protocol === 'tingodb:') {
var colldir = path.join(self.options.directory, self.options.connexion.path, 'filerake');
debug('Tingodb\'s path', colldir);
mkdirp(colldir, function (err) {
assert.ifError(err);
var tingodb = require('tingodb')();
var handle = new tingodb.Db(colldir, {});
handle.open(func);
}
)
}
else {
throw new Error('Unsupported engine : ' + self.options.connexion.protocol)
}
}
Sync.prototype.check = function (file, next)
{
assert(file instanceof File);
var self = this;
if (self.options.maxFileSize && self.options.maxFileSize < file.doc.filesize) {
debug('reject', file.doc.filename, file.doc.fid);
next(new Error('File exceeds the maximum size.'));
}
else if (self.checker.indexOf(file.doc.fid) === -1) {
if (self.queue.length() >= self.options.concurrency) {
debug('delayed', file.doc.filename, file.doc.fid);
setTimeout(function() { self.check(file, next);}, self.options.delay);
}
else {
debug('check', file.doc.filename, file.doc.fid);
self.checker.push(file.doc.fid);
self.queue.push(file, function(err) {
self.checker = self.checker.filter(function(i) { return (i !== file.doc.fid) });
next(err);
});
}
}
else {
debug('in progress', file.doc.filename, file.doc.fid);
// next()
}
}
Sync.prototype.drop = function (file, next)
{
assert(file instanceof File);
var self = this;
var doc = file.get();
var selector = {fid: doc.fid};
var sortor = [['state', 1]];
self.connect(function(err, collection) {
if (err) {
return next(err);
}
collection.findAndModify(selector, sortor, { $set : { state : 'deleted' } }, function (err, doc) {
if (err) {
return next(err);
}
if (doc) {
debug('delete', doc.filename);
}
next();
});
});
}
Sync.prototype.cancel = function (file, next)
{
assert(file instanceof File);
var self = this;
var doc = file.get();
var selector = {fid: doc.fid};
self.connect(function(err, collection) {
if (err) {
return next(err);
}
collection.findOne(selector, function (err, res) {
if (err) {
return next(err);
}
if (res) {
debug('cancel', doc.filename);
collection.remove(doc, {w: 0});
}
next();
}
);
}
);
}
Sync.prototype.clean = function (condition, next)
{
var self = this;
var selector = {
$and : [
{ dateSynchronised : { $ne: self.dateSynchronised } },
condition
]
};
var sortor = [['state', 1]];
self.connect(function(err, collection) {
if (err) {
return next(err);
}
collection.findAndModify(selector, sortor, { $set : { state : 'deleted', dateSynchronised : self.dateSynchronised } }, function (err, doc) {
if (err) {
return next(err);
}
if (doc) {
debug('clean', doc.filename);
}
next();
});
}
);
}
Sync.prototype.update = function (fid, doc, next)
{
var self = this;
var selector = {
fid : doc.fid
};
self.connect(function(err, collection) {
if (err) {
return next(err);
}
collection.update(selector, doc, next);
});
}
Sync.prototype.insert = function (doc, next)
{
var self = this;
var selector = {
sha1 : doc.sha1,
dateModified: doc.dateModified,
state: 'deleted'
};
self.connect(function(err, collection) {
if (err) {
return next(err);
}
collection.findOne(selector, {limit: 1}, function (err, dta) {
if (!err && dta) {
self.rename(dta.fid, next);
}
else {
doc.state = 'inserted';
doc.dateSynchronised = self.dateSynchronised;
collection.insert(doc, next);
}
});
});
}
Sync.prototype.updateAll = function (docs, next)
{
var self = this;
async.eachSerie(docs, function(doc, callback) {
doc.state = 'updated';
doc.dateSynchronised = self.dateSynchronised;
self.update(doc.fid, doc, callback);
},
function (err) {
self.clean({ fid: docs[0].fid }, next);
}
);
}
Sync.prototype.insertAll = function (docs, next)
{
var self = this;
var selector = {
sha1 : docs[0].sha1,
dateModified: docs[0].dateModified,
state: 'deleted'
};
self.connect(function(err, collection) {
if (err) {
return next(err);
}
collection.findOne(selector, {limit: 1}, function (err, dta) {
if (!err && dta) {
self.rename(dta.fid, next);
}
else {
async.mapLimit(docs.map(function(i) {
i.state = 'inserted';
i.dateSynchronised = self.dateSynchronised;
return i;
})
, self.options.concurrency
, function (item, callback) {
debug('insert item', item.filename);
collection.insert(item, callback);
}
, function(err, results) {
next();
// results is now an array of stats for each file
});
}
});
});
}
Sync.prototype.rename = function (fid, next)
{
var self = this;
var selector = {
fid : fid
};
var doc = {
state : 'renamed',
dateSynchronised : self.dateSynchronised
}
self.connect(function(err, collection) {
if (err) {
return next(err);
}
collection.update(selector, doc, next);
}
);
}
Sync.prototype.compare = function (file, next)
{
assert(file instanceof File);
var self = this;
var doc = file.get();
var selector = {fid: doc.fid};
self.connect(function(err, collection) {
collection.findOne(selector, {limit: 1}, function (err, dta) {
if (err) {
return next(err);
}
if (!dta) {
dta = {};
}
self.compareFileAndDoc(file, dta, next);
});
});
}
Sync.prototype.compareFileAndDoc = function (file, d, next)
{
assert(file instanceof File);
var doc = file.get();
var self = this;
if ( (
d.dateModified && doc.dateModified && d.dateModified.getTime() === doc.dateModified.getTime()
)
&&
(
! doc.dateConfig || (
d.dateConfig && doc.dateConfig && d.dateConfig.getTime() === doc.dateConfig.getTime()
)
) ) {
debug('unchange', doc.fid);
next();
}
else {
self.mountor.append(file, function worker(dta, cb) {
self.compareDocAndDoc(d, dta, cb);
}, next);
}
}
Sync.prototype.compareDocAndDoc = function (logical, physical, next)
{
var self = this
, physicalSHA1
, logicalSHA1 = logical.sha1 ? logical.sha1 : false;
if (Array.isArray(physical)) {
physicalSHA1 = physical[0].sha1 ? physical[0].sha1 : false;
}
else {
physicalSHA1 = physical.sha1 ? physical.sha1 : false;
}
if (logicalSHA1) {
var state = logicalSHA1 === physicalSHA1 ? 'unmodified' : 'updated';
debug(state, physical.filename);
if (Array.isArray(physical)) {
self.updateAll(logical.fid, physical, next);
}
else {
var newdoc = {};
extend(true, newdoc, logical, physical);
newdoc.dateSynchronised = self.dateSynchronised;
newdoc.state = state;
self.update(logical.fid, newdoc, next);
}
}
else {
if (Array.isArray(physical)) {
debug('inserted', physical[0].filename, '(Exploded)');
self.insertAll(physical, next);
}
else {
debug('inserted', physical.filename);
self.insert(physical, next);
}
}
}
module.exports = Sync;