2017-09-22 14:40:37 -07:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
var assert = require('assert'),
|
2017-09-26 15:28:04 -07:00
|
|
|
async = require('async'),
|
2017-09-26 11:59:45 -07:00
|
|
|
debug = require('debug')('box:syncer'),
|
2017-09-22 14:40:37 -07:00
|
|
|
path = require('path'),
|
|
|
|
|
paths = require('./paths.js'),
|
|
|
|
|
safe = require('safetydance');
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
|
|
|
|
sync: sync
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function readCache(cacheFile) {
|
|
|
|
|
assert.strictEqual(typeof cacheFile, 'string');
|
|
|
|
|
|
|
|
|
|
var cache = safe.fs.readFileSync(cacheFile, 'utf8');
|
|
|
|
|
if (!cache) return [ ];
|
2017-09-26 11:59:45 -07:00
|
|
|
var result = cache.trim().split('\n').map(JSON.parse);
|
2017-09-22 14:40:37 -07:00
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readTree(dir) {
|
|
|
|
|
assert.strictEqual(typeof dir, 'string');
|
|
|
|
|
|
|
|
|
|
var list = safe.fs.readdirSync(dir).sort();
|
|
|
|
|
if (!list) return [ ];
|
|
|
|
|
|
2017-09-26 11:59:45 -07:00
|
|
|
return list.map(function (e) { return { stat: safe.fs.lstatSync(path.join(dir, e)), name: e }; });
|
2017-09-22 14:40:37 -07:00
|
|
|
}
|
|
|
|
|
|
2017-09-26 15:28:04 -07:00
|
|
|
function sync(dir, taskProcessor, concurrency, callback) {
|
2017-09-22 14:40:37 -07:00
|
|
|
assert.strictEqual(typeof dir, 'string');
|
|
|
|
|
assert.strictEqual(typeof taskProcessor, 'function');
|
2017-09-26 15:28:04 -07:00
|
|
|
assert.strictEqual(typeof concurrency, 'number');
|
2017-09-22 14:40:37 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2017-09-26 15:28:04 -07:00
|
|
|
var curCacheIndex = 0, addQueue = [ ], delQueue = [ ];
|
|
|
|
|
|
2017-09-26 16:31:08 -07:00
|
|
|
var cacheFile = path.join(paths.BACKUP_INFO_DIR, path.basename(dir) + '.sync.cache'),
|
|
|
|
|
newCacheFile = path.join(paths.BACKUP_INFO_DIR, path.basename(dir) + '.sync.cache.new');
|
2017-09-26 07:57:20 -07:00
|
|
|
|
2017-09-27 17:34:49 -07:00
|
|
|
if (!safe.fs.existsSync(cacheFile)) { // if cache is missing, start out empty. TODO: do a remote listDir and rebuild
|
2017-09-27 12:09:19 -07:00
|
|
|
delQueue.push({ operation: 'removedir', path: '' });
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-26 07:57:20 -07:00
|
|
|
var cache = readCache(cacheFile);
|
|
|
|
|
|
2017-09-26 11:59:45 -07:00
|
|
|
var newCacheFd = safe.fs.openSync(newCacheFile, 'w'); // truncates any existing file
|
|
|
|
|
if (newCacheFd === -1) return callback(new Error('Error opening new cache file: ' + safe.error.message));
|
2017-09-22 14:40:37 -07:00
|
|
|
|
|
|
|
|
function advanceCache(entryPath) {
|
2017-09-27 17:34:49 -07:00
|
|
|
// TODO: detect and issue removedir
|
2017-09-22 14:40:37 -07:00
|
|
|
for (; curCacheIndex !== cache.length && (entryPath === '' || cache[curCacheIndex].path < entryPath); ++curCacheIndex) {
|
2017-09-26 15:28:04 -07:00
|
|
|
delQueue.push({ operation: 'remove', path: cache[curCacheIndex].path });
|
2017-09-22 14:40:37 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function traverse(relpath) {
|
|
|
|
|
var entries = readTree(path.join(dir, relpath));
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < entries.length; i++) {
|
|
|
|
|
var entryPath = path.join(relpath, entries[i].name);
|
2017-09-26 11:41:01 -07:00
|
|
|
var stat = entries[i].stat;
|
2017-09-22 14:40:37 -07:00
|
|
|
|
2017-09-26 11:59:45 -07:00
|
|
|
if (!stat) continue; // some stat error
|
2017-09-26 11:41:01 -07:00
|
|
|
if (!stat.isDirectory() && !stat.isFile()) continue;
|
|
|
|
|
if (stat.isSymbolicLink()) continue;
|
2017-09-22 14:40:37 -07:00
|
|
|
|
2017-09-26 11:41:01 -07:00
|
|
|
if (stat.isDirectory()) {
|
2017-09-22 14:40:37 -07:00
|
|
|
traverse(entryPath);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-27 18:43:20 -07:00
|
|
|
safe.fs.appendFileSync(newCacheFd, JSON.stringify({ path: entryPath, mtime: stat.mtime.getTime(), size: stat.size, inode: stat.inode }) + '\n');
|
2017-09-22 14:40:37 -07:00
|
|
|
|
|
|
|
|
advanceCache(entryPath);
|
|
|
|
|
|
|
|
|
|
if (curCacheIndex !== cache.length && cache[curCacheIndex].path === entryPath) {
|
2017-09-27 18:43:20 -07:00
|
|
|
if (stat.mtime.getTime() !== cache[curCacheIndex].mtime || stat.size != cache[curCacheIndex].size || stat.inode !== cache[curCacheIndex].inode) {
|
2017-09-26 15:28:04 -07:00
|
|
|
addQueue.push({ operation: 'add', path: entryPath });
|
2017-09-22 14:40:37 -07:00
|
|
|
}
|
|
|
|
|
++curCacheIndex;
|
|
|
|
|
} else {
|
2017-09-26 15:28:04 -07:00
|
|
|
addQueue.push({ operation: 'add', path: entryPath });
|
2017-09-22 14:40:37 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
traverse('');
|
2017-09-26 11:59:45 -07:00
|
|
|
advanceCache(''); // remove rest of the cache entries
|
2017-09-22 14:40:37 -07:00
|
|
|
|
2017-09-26 11:59:45 -07:00
|
|
|
safe.fs.closeSync(newCacheFd);
|
|
|
|
|
|
2017-09-26 15:28:04 -07:00
|
|
|
debug('Processing %s deletes and %s additions', delQueue.length, addQueue.length);
|
|
|
|
|
|
|
|
|
|
async.eachLimit(delQueue, concurrency, taskProcessor, function (error) {
|
|
|
|
|
debug('Done processing deletes', error);
|
|
|
|
|
|
|
|
|
|
async.eachLimit(addQueue, concurrency, taskProcessor, function (error) {
|
|
|
|
|
debug('Done processing adds', error);
|
|
|
|
|
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
safe.fs.unlinkSync(cacheFile);
|
|
|
|
|
|
|
|
|
|
if (!safe.fs.renameSync(newCacheFile, cacheFile)) debug('Unable to save new cache file');
|
2017-09-22 14:40:37 -07:00
|
|
|
|
2017-09-26 15:28:04 -07:00
|
|
|
callback();
|
|
|
|
|
});
|
|
|
|
|
});
|
2017-09-26 16:31:08 -07:00
|
|
|
}
|