Fix #372 - Do not create conflicted copies if the client has a newer version of a file, instead upstream that file

This commit is contained in:
gideonthomas 2014-10-15 14:11:56 -04:00
Родитель 2e7fecbc27
Коммит f64388e0d1
15 изменённых файлов: 631 добавлений и 272 удалений

Просмотреть файл

@ -81,6 +81,7 @@ var SyncManager = require('./sync-manager.js');
var SyncFileSystem = require('./sync-filesystem.js');
var Filer = require('../../lib/filer.js');
var EventEmitter = require('events').EventEmitter;
var resolvePath = require('../../lib/sync-path-resolver.js').resolveFromArray;
var MakeDrive = {};
module.exports = MakeDrive;
@ -117,6 +118,11 @@ function createFS(options) {
var autoSync;
var pathCache;
// Path that needs to be used for an upstream sync
// to sync files that were determined to be more
// up-to-date on the client during a downstream sync
var upstreamPath;
// State of the sync connection
sync.SYNC_DISCONNECTED = "SYNC DISCONNECTED";
sync.SYNC_CONNECTING = "SYNC CONNECTING";
@ -151,6 +157,30 @@ function createFS(options) {
manager = null;
}
function requestSync(path) {
// If we're not connected (or are already syncing), ignore this request
if(sync.state === sync.SYNC_DISCONNECTED || sync.state === sync.SYNC_ERROR) {
sync.emit('error', new Error('Invalid state. Expected ' + sync.SYNC_CONNECTED + ', got ' + sync.state));
return;
}
// If there were no changes to the filesystem and
// no path was passed to sync, ignore this request
if(!fs.pathToSync && !path) {
return;
}
// If a path was passed sync using it
if(path) {
return manager.syncPath(path);
}
// Cache the path that needs to be synced for error recovery
pathCache = fs.pathToSync;
fs.pathToSync = null;
manager.syncPath(pathCache);
}
// Turn on auto-syncing if its not already on
sync.auto = function(interval) {
var syncInterval = interval|0 > 0 ? interval|0 : 15 * 1000;
@ -180,7 +210,7 @@ function createFS(options) {
sync.onError = function(err) {
// Regress to the path that needed to be synced but failed
// (likely because of a sync LOCK)
fs.pathToSync = pathCache;
fs.pathToSync = upstreamPath || pathCache;
sync.state = sync.SYNC_ERROR;
sync.emit('error', err);
};
@ -200,21 +230,12 @@ function createFS(options) {
// Request that a sync begin.
sync.request = function() {
// If we're not connected (or are already syncing), ignore this request
if(sync.state === sync.SYNC_DISCONNECTED || sync.state === sync.SYNC_ERROR) {
sync.emit('error', new Error('Invalid state. Expected ' + sync.SYNC_CONNECTED + ', got ' + sync.state));
return;
}
// If there were no changes to the filesystem, ignore this request
if(!fs.pathToSync) {
return;
}
// Cache the path that needs to be synced for error recovery
pathCache = fs.pathToSync;
fs.pathToSync = null;
manager.syncPath(pathCache);
// sync.request does not take any parameters
// as the path to sync is determined internally
// requestSync on the other hand optionally takes
// a path to sync which can be specified for
// internal use
requestSync();
};
// Try to connect to the server.
@ -234,7 +255,7 @@ function createFS(options) {
// Upgrade connection state to `connecting`
sync.state = sync.SYNC_CONNECTING;
function downstreamSyncCompleted() {
function downstreamSyncCompleted(paths, needUpstream) {
// Re-wire message handler functions for regular syncing
// now that initial downstream sync is completed.
sync.onSyncing = function() {
@ -242,26 +263,36 @@ function createFS(options) {
sync.emit('syncing');
};
sync.onCompleted = function(paths) {
sync.onCompleted = function(paths, needUpstream) {
// If changes happened to the files that needed to be synced
// during the sync itself, they will be overwritten
// https://github.com/mozilla/makedrive/issues/129 and
// https://github.com/mozilla/makedrive/issues/3
// https://github.com/mozilla/makedrive/issues/129
function complete() {
sync.state = sync.SYNC_CONNECTED;
sync.emit('completed');
}
if(!paths) {
if(!paths && !needUpstream) {
return complete();
}
// Changes in the client are newer (determined during
// the sync) and need to be upstreamed
if(needUpstream) {
upstreamPath = resolvePath(needUpstream);
complete();
return requestSync(upstreamPath);
}
// If changes happened during a downstream sync
// Change the path that needs to be synced
manager.resetUnsynced(paths, function(err) {
if(err) {
return sync.onError(err);
}
upstreamPath = null;
complete();
});
};
@ -285,6 +316,16 @@ function createFS(options) {
}
sync.emit('connected');
// If the downstream was completed and some
// versions of files were not synced as they were
// newer on the client, upstream them
if(needUpstream) {
upstreamPath = resolvePath(needUpstream);
requestSync(upstreamPath);
} else {
upstreamPath = null;
}
}
function connect(token) {
@ -302,9 +343,9 @@ function createFS(options) {
sync.onSyncing = function() {
// do nothing, wait for onCompleted()
};
sync.onCompleted = function() {
sync.onCompleted = function(paths, needUpstream) {
// Downstream sync is done, finish connect() setup
downstreamSyncCompleted();
downstreamSyncCompleted(paths, needUpstream);
};
});
}

Просмотреть файл

@ -7,6 +7,8 @@ var deserializeDiff = require('../../lib/diff').deserialize;
var states = require('./sync-states');
var steps = require('./sync-steps');
var dirname = require('../../lib/filer').Path.dirname;
var async = require('../../lib/async-lite');
var fsUtils = require('../../lib/fs-utils');
function onError(syncManager, err) {
syncManager.session.step = steps.FAILED;
@ -124,9 +126,47 @@ function handleResponse(syncManager, data) {
}
function handlePatchAckResponse() {
var syncedPaths = data.content.syncedPaths;
session.state = states.READY;
session.step = steps.SYNCED;
sync.onCompleted(data.content.syncedPaths);
function stampChecksum(path, callback) {
fs.lstat(path, function(err, stats) {
if(err) {
if(err.code !== 'ENOENT') {
return callback(err);
}
// Non-existent paths (usually due to renames or
// deletes that are included in the syncedPaths)
// cannot be stamped with a checksum
return callback();
}
if(!stats.isFile()) {
return callback();
}
rsyncUtils.getChecksum(fs, path, function(err, checksum) {
if(err) {
return callback(err);
}
fsUtils.setChecksum(fs, path, checksum, callback);
});
});
}
// As soon as an upstream sync happens, the files synced
// become the last synced versions and must be stamped
// with their checksums to version them
async.eachSeries(syncedPaths, stampChecksum, function(err) {
if(err) {
return onError(syncManager, err);
}
sync.onCompleted(data.content.syncedPaths);
});
}
function handlePatchResponse() {
@ -150,9 +190,11 @@ function handleResponse(syncManager, data) {
return onError(syncManager, err);
}
var size = rsyncOptions.size || 5;
if(paths.needsUpstream.length) {
session.needsUpstream = paths.needsUpstream;
}
rsyncUtils.generateChecksums(fs, paths.synced, size, function(err, checksums) {
rsyncUtils.generateChecksums(fs, paths.synced, true, function(err, checksums) {
if(err) {
var message = SyncMessage.response.reset;
syncManager.send(message.stringify());
@ -160,7 +202,7 @@ function handleResponse(syncManager, data) {
}
var message = SyncMessage.response.patch;
message.content = {checksums: checksums, size: size};
message.content = {checksums: checksums};
syncManager.send(message.stringify());
});
});
@ -169,7 +211,9 @@ function handleResponse(syncManager, data) {
function handleVerificationResponse() {
session.srcList = null;
session.step = steps.SYNCED;
sync.onCompleted();
var needsUpstream = session.needsUpstream;
delete session.needsUpstream;
sync.onCompleted(null, needsUpstream);
}
function handleUpstreamResetResponse() {

Просмотреть файл

@ -7,7 +7,8 @@ module.exports = {
attributes: {
unsynced: 'makedrive-unsynced',
conflict: 'makedrive-conflict'
conflict: 'makedrive-conflict',
checksum: 'makedrive-checksum'
},
server: {

Просмотреть файл

@ -59,22 +59,10 @@ function fremoveUnsynced(fs, fd, callback) {
// Set the unsynced metadata for a path
function setUnsynced(fs, path, callback) {
fs.setxattr(path, constants.attributes.unsynced, Date.now(), function(err) {
if(err) {
return callback(err);
}
callback();
});
fs.setxattr(path, constants.attributes.unsynced, Date.now(), callback);
}
function fsetUnsynced(fs, fd, callback) {
fs.fsetxattr(fd, constants.attributes.unsynced, Date.now(), function(err) {
if(err) {
return callback(err);
}
callback();
});
fs.fsetxattr(fd, constants.attributes.unsynced, Date.now(), callback);
}
// Get the unsynced metadata for a path
@ -97,13 +85,71 @@ function fgetUnsynced(fs, fd, callback) {
});
}
// Remove the Checksum metadata from a path
function removeChecksum(fs, path, callback) {
fs.removexattr(path, constants.attributes.checksum, function(err) {
if(err && err.code !== 'ENOATTR') {
return callback(err);
}
callback();
});
}
function fremoveChecksum(fs, fd, callback) {
fs.fremovexattr(fd, constants.attributes.checksum, function(err) {
if(err && err.code !== 'ENOATTR') {
return callback(err);
}
callback();
});
}
// Set the Checksum metadata for a path
function setChecksum(fs, path, checksum, callback) {
fs.setxattr(path, constants.attributes.checksum, checksum, callback);
}
function fsetChecksum(fs, fd, checksum, callback) {
fs.fsetxattr(fd, constants.attributes.checksum, checksum, callback);
}
// Get the Checksum metadata for a path
function getChecksum(fs, path, callback) {
fs.getxattr(path, constants.attributes.checksum, function(err, value) {
if(err && err.code !== 'ENOATTR') {
return callback(err);
}
callback(null, value);
});
}
function fgetChecksum(fs, fd, callback) {
fs.fgetxattr(fd, constants.attributes.checksum, function(err, value) {
if(err && err.code !== 'ENOATTR') {
return callback(err);
}
callback(null, value);
});
}
module.exports = {
forceCopy: forceCopy,
// Unsynced attr utils
isPathUnsynced: isPathUnsynced,
removeUnsynced: removeUnsynced,
fremoveUnsynced: fremoveUnsynced,
setUnsynced: setUnsynced,
fsetUnsynced: fsetUnsynced,
getUnsynced: getUnsynced,
fgetUnsynced: fgetUnsynced
fgetUnsynced: fgetUnsynced,
// Checksum attr utils
removeChecksum: removeChecksum,
fremoveChecksum: fremoveChecksum,
setChecksum: setChecksum,
fsetChecksum: fsetChecksum,
getChecksum: getChecksum,
fgetChecksum: fgetChecksum
};

Просмотреть файл

@ -22,7 +22,7 @@ module.exports = function checksums(fs, path, srcList, options, callback) {
function checksumsForFile(checksumNode, sourceNode, callback) {
function generateChecksumsForFile() {
rsyncUtils.checksum(fs, sourceNode.path, options.size, function(err, checksums) {
rsyncUtils.blockChecksums(fs, sourceNode.path, options.size, function(err, checksums) {
if(err) {
return callback(err);
}
@ -72,7 +72,7 @@ module.exports = function checksums(fs, path, srcList, options, callback) {
return callback(err);
}
rsyncUtils.checksum(fs, linkContents, options.size, function(err, checksums) {
rsyncUtils.blockChecksums(fs, linkContents, options.size, function(err, checksums) {
if(err) {
return callback(err);
}

Просмотреть файл

@ -25,6 +25,21 @@ module.exports = function diff(fs, path, checksumList, options, callback) {
this.modified = modifiedTime;
}
// Compute the checksum for the file/link and
// append it to the diffNode.
function appendChecksum(diffNode, diffPath, callback) {
rsyncUtils.getChecksum(fs, diffPath, function(err, checksum) {
if(err) {
return callback(err);
}
diffNode.checksum = checksum;
diffList.push(diffNode);
callback(null, diffList);
});
}
function diffsForLink(checksumNode, callback) {
var checksumNodePath = checksumNode.path;
var diffNode = new DiffNode(checksumNodePath, checksumNode.type, checksumNode.modified);
@ -52,6 +67,13 @@ module.exports = function diff(fs, path, checksumList, options, callback) {
}
diffNode.diffs = rsyncUtils.rollData(data, checksumNode.checksums, options.size);
// If versions are enabled, add the checksum
// field to the diffNode for version comparison
if(options.versions) {
return appendChecksum(diffNode, linkContents, callback);
}
diffList.push(diffNode);
callback(null, diffList);
@ -77,6 +99,13 @@ module.exports = function diff(fs, path, checksumList, options, callback) {
}
diffNode.diffs = rsyncUtils.rollData(data, checksumNode.checksums, options.size);
// If versions are enabled, add the checksum
// field to the diffNode for version comparison
if(options.versions) {
return appendChecksum(diffNode, checksumNodePath, callback);
}
diffList.push(diffNode);
callback(null, diffList);

Просмотреть файл

@ -29,7 +29,8 @@ module.exports = function patch(fs, path, diffList, options, callback) {
var paths = {
synced: [],
failed: []
failed: [],
needsUpstream: []
};
var pathsToSync = extractPathsFromDiffs(diffList);
@ -147,12 +148,14 @@ module.exports = function patch(fs, path, diffList, options, callback) {
}
function maybeGenerateConflicted(nodePath, callback) {
// If the file has not been synced upstream
// and needs to be patched, create a conflicted copy
fsUtils.isPathUnsynced(fs, nodePath, function(err, unsynced) {
if(err) {
return handleError(err, callback);
}
// Do not generate a conflicted copy for an unsynced file
// Generate a conflicted copy only for an unsynced file
if(!unsynced) {
return callback();
}
@ -180,6 +183,43 @@ module.exports = function patch(fs, path, diffList, options, callback) {
var diffLength = diffNode.diffs ? diffNode.diffs.length : 0;
var filePath = diffNode.path;
// Compare the version of the file when it was last
// synced with the version of the diffNode by comparing
// checksums and modified times.
// If they match, the file is not patched and needs to
// be upstreamed
function compareVersions(data) {
fs.lstat(filePath, function(err, stats) {
if(err) {
return handleError(err, callback);
}
// If the file was modified before the
// diffNode's modified time, the file is outdated
// and needs to be patched
if(stats.mtime <= diffNode.modified) {
return applyPatch(getPatchedData(data));
}
fsUtils.getChecksum(fs, filePath, function(err, checksum) {
if(err) {
return handleError(err, callback);
}
// If the last synced checksum matches the
// diffNode's checksum, ignore the patch
// because it is a newer version than whats on
// the server
if(checksum === diffNode.checksum) {
paths.needsUpstream.push(filePath);
return callback(null, paths);
}
applyPatch(getPatchedData(data));
});
});
}
function updateModifiedTime() {
fs.utimes(filePath, diffNode.modified, diffNode.modified, function(err) {
if(err) {
@ -251,11 +291,25 @@ module.exports = function patch(fs, path, diffList, options, callback) {
}
fs.readFile(filePath, function(err, data) {
if(err && err.code !== 'ENOENT') {
return handleError(err, callback);
if(err) {
if(err.code !== 'ENOENT') {
return handleError(err, callback);
}
// Patch a non-existent file i.e. create it
return applyPatch(getPatchedData(new Buffer(0)));
}
applyPatch(getPatchedData(data || new Buffer(0)));
// If version comparing is not enabled, apply
// the patch directly
if(!options.versions) {
return applyPatch(getPatchedData(data));
}
// Check the last synced checksum with
// the given checksum and don't patch if they
// match
compareVersions(data);
});
}

Просмотреть файл

@ -27,6 +27,8 @@ var fsUtils = require('../fs-utils');
// false: do not change modified times of destination files [default]
// links - true: sync symbolic links as links in destination
// false: sync symbolic links as the files they link to in destination [default]
// versions - true: do not sync a node if the last synced version matches the version it needs to be synced to [default]
// false: sync nodes irrespective of the last synced version
function configureOptions(options) {
if(!options || typeof options === 'function') {
options = {};
@ -37,6 +39,7 @@ function configureOptions(options) {
options.recursive = options.recursive || false;
options.time = options.time || false;
options.links = options.links || false;
options.versions = options.versions !== false;
return options;
}
@ -63,17 +66,6 @@ function validateParams(fs, param2) {
return err;
}
// This function has been taken from lodash
// Licensed under the MIT license
// https://github.com/lodash/lodash
function sortObjects(list, prop) {
return list.sort(function(a,b) {
a = a[prop];
b = b[prop];
return (a === b) ? 0 : (a < b) ? -1 : 1;
});
}
// MD5 hashing for RSync
function md5sum(data) {
return MD5(data).toString();
@ -211,8 +203,11 @@ function roll(data, checksums, blockSize) {
return results;
}
// RSync function to calculate checksums
function checksum (fs, path, size, callback) {
// Rsync function to calculate checksums for
// a file by dividing it into blocks of data
// whose size is passed in and checksuming each
// block of data
function blockChecksums(fs, path, size, callback) {
var cache = {};
fs.readFile(path, function (err, data) {
@ -256,10 +251,29 @@ function checksum (fs, path, size, callback) {
});
}
// Generate the MD5 hash for the data of a file
// in its entirety
function getChecksum(fs, path, callback) {
fs.readFile(path, function(err, data) {
if(!err) {
callback(null, md5sum(data));
} else if(err.code === 'ENOENT') {
// File does not exist so the checksum is an empty string
callback(null, "");
} else {
callback(err);
}
});
}
// Generate checksums for an array of paths to be used for comparison
function generateChecksums(fs, paths, blockSize, callback) {
if(!blockSize || typeof callback !== 'function') {
return callback(new Errors.EINVAL('Insufficient data provided'));
// It also takes an optional parameter called stampNode, a boolean which
// indicates whether the checksum should be stamped as an xattr on the node.
function generateChecksums(fs, paths, stampNode, callback) {
// Maybe stampNode was not passed in
if(typeof callback !== 'function') {
callback = findCallback(callback, stampNode);
stampNode = false;
}
var paramError = validateParams(fs, paths);
@ -267,56 +281,81 @@ function generateChecksums(fs, paths, blockSize, callback) {
return callback(paramError);
}
var checksums = [];
var checksumList = [];
function ChecksumNode(path, checksum) {
function ChecksumNode(path, type, checksum) {
this.path = path;
this.checksum = checksum || [];
this.type = type;
this.checksum = checksum;
}
function addChecksumNode(path, nodeType, checksum, callback) {
var checksumNode;
// If no checksum was passed in
if(typeof checksum === 'function') {
callback = checksum;
checksumNode = new ChecksumNode(path, nodeType);
} else {
checksumNode = new ChecksumNode(path, nodeType, checksum);
}
checksumList.push(checksumNode);
callback();
}
// Only calculate the checksums for synced paths
function maybeAddChecksumNode(path, nodeType, callback) {
fsUtils.isPathUnsynced(fs, path, function(err, unsynced) {
if(err) {
return callback(err);
}
if(unsynced) {
return callback();
}
getChecksum(fs, path, function(err, checksum) {
if(err) {
return callback(err);
}
// If we shouldn't add the checksum stamp or
// the node does not exist (cannot add a stamp)
// immediately add the checksum
if(!stampNode || checksum === "") {
return addChecksumNode(path, nodeType, checksum, callback);
}
// Stamp the node with the checksum
fsUtils.setChecksum(fs, path, checksum, function(err) {
if(err) {
return callback(err);
}
addChecksumNode(path, nodeType, checksum, callback);
});
});
});
}
function calcChecksum(path, callback) {
var checksumNode;
fs.lstat(path, function(err, stat) {
var nodeType = stat && stat.type;
if(err) {
if(err.code !== 'ENOENT') {
return callback(err);
}
checksumNode = new ChecksumNode(path);
checksums.push(checksumNode);
return callback();
// Checksums for non-existent files
maybeAddChecksumNode(path, nodeType, callback);
} else if(stat.isDirectory()) {
// Directory checksums are not calculated i.e. are undefined
addChecksumNode(path, nodeType, callback);
} else {
// Checksums for synced files/links
maybeAddChecksumNode(path, nodeType, callback);
}
// Use contents of directory instead of checksums
if(stat.isDirectory()) {
checksumNode = new ChecksumNode(path);
checksums.push(checksumNode);
return callback();
}
fsUtils.isPathUnsynced(fs, path, function(err, unsynced) {
if(err) {
return callback(err);
}
if(unsynced) {
return callback();
}
// Calculate checksums for file or symbolic links
checksum(fs, path, blockSize, function(err, chksum) {
if(err) {
return callback(err);
}
checksumNode = new ChecksumNode(path, chksum);
checksums.push(checksumNode);
callback();
});
});
});
}
@ -325,7 +364,7 @@ function generateChecksums(fs, paths, blockSize, callback) {
return callback(err);
}
callback(null, checksums);
callback(null, checksumList);
});
}
@ -333,79 +372,45 @@ function generateChecksums(fs, paths, blockSize, callback) {
// checksums for a collection of paths in one file system
// against the checksums for the same those paths in
// another file system
function compareContents(fs, checksums, blockSize, callback) {
var EDIFF = 'DIFF';
function compareContents(fs, checksumList, callback) {
var ECHKSUM = "Checksums do not match";
if(!blockSize || typeof callback !== 'function') {
return callback(new Errors.EINVAL('Insufficient data provided'));
}
var paramError = validateParams(fs, checksums);
var paramError = validateParams(fs, checksumList);
if(paramError) {
return callback(paramError);
}
// Check if two checksum arrays are equal
function isEqual(checksumNode1, checksumNode2) {
var comparisonLength = checksumNode2.length;
var checksum1, checksum2;
if(checksumNode1.length !== comparisonLength) {
return false;
}
// Sort the checksum objects in each array by the 'index' property
checksumNode1 = sortObjects(checksumNode1, 'index');
checksumNode2 = sortObjects(checksumNode2, 'index');
// Compare each object's checksums
for(var i = 0; i < comparisonLength; i++) {
checksum1 = checksumNode1[i];
checksum2 = checksumNode2[i];
if(checksum1.weak !== checksum2.weak ||
checksum1.strong !== checksum2.strong) {
return false;
}
}
return true;
}
function compare(checksumNode, callback) {
var path = checksumNode.path;
fs.lstat(path, function(err, stat) {
if(err) {
if(err.code !== 'ENOENT') {
return callback(err);
}
if(err && err.code !== 'ENOENT') {
return callback(err);
}
// Checksums for a non-existent path are empty
if(checksumNode.checksum && !checksumNode.checksum.length) {
// If the types of the nodes on each fs do not match
// i.e. /a is a file on fs1 and /a is a directory on fs2
if(!err && checksumNode.type !== stat.type) {
return callback(ECHKSUM);
}
// If the node type is a directory, checksum should not exist
if(!err && stat.isDirectory()) {
if(!checksumNode.checksum) {
return callback();
}
return callback(EDIFF);
callback(ECHKSUM);
}
// Directory comparison of contents
if(stat.isDirectory()) {
return callback();
}
if(!checksumNode.checksum) {
return callback(EDIFF);
}
// Compare checksums for two files/symbolic links
checksum(fs, path, blockSize, function(err, checksum) {
// Checksum comparison for a non-existent path or file/link
getChecksum(fs, path, function(err, checksum) {
if(err) {
return callback(err);
}
if(!isEqual(checksum, checksumNode.checksum)) {
return callback(EDIFF);
if(checksum !== checksumNode.checksum) {
return callback(ECHKSUM);
}
callback();
@ -413,21 +418,18 @@ function compareContents(fs, checksums, blockSize, callback) {
});
}
async.eachSeries(checksums, compare, function(err) {
if(err && err !== EDIFF) {
return callback(err, false);
async.eachSeries(checksumList, compare, function(err) {
if(err && err !== ECHKSUM) {
return callback(err);
}
if(err === EDIFF) {
return callback(null, false);
}
callback(null, true);
callback(null, err !== ECHKSUM);
});
}
module.exports = {
checksum: checksum,
blockChecksums: blockChecksums,
getChecksum: getChecksum,
rollData: roll,
generateChecksums: generateChecksums,
compareContents: compareContents,

Просмотреть файл

@ -1,14 +1,15 @@
/**
* Sync path resolver is a library that provides
* functionality to determine 'syncable' paths
* It exposes the following method:
* It exposes the following methods:
*
* resolve - This method takes two paths as arguments.
* The goal is to find the most common ancestor
* between them. For e.g. the most common ancestor
* between '/dir' and '/dir/file.txt' is '/dir' while
* between '/dir' and '/file.txt' would be '/'.
* resolve - This method takes two paths as arguments.
* The goal is to find the most common ancestor
* between them. For e.g. the most common ancestor
* between '/dir' and '/dir/file.txt' is '/dir' while
* between '/dir' and '/file.txt' would be '/'.
*
* resolveFromArray - This method works exactly like resolve but works for arrays of paths instead
*/
var pathResolver = {};
@ -59,4 +60,18 @@ pathResolver.resolve = function(path1, path2) {
return commonAncestor(path1, path1Depth, path2, path2Depth);
};
pathResolver.resolveFromArray = function(paths) {
if(!paths) {
return '/';
}
var resolvedPath, i;
for(i = 1, resolvedPath = paths[0]; i < paths.length; i++) {
resolvedPath = pathResolver.resolve(resolvedPath, paths[i]);
}
return resolvedPath;
};
module.exports = pathResolver;

Просмотреть файл

@ -631,9 +631,8 @@ SyncProtocolHandler.prototype.handlePatchResponse = function(message) {
}
var checksums = message.content.checksums;
var size = message.content.size || 5;
rsync.utils.compareContents(client.fs, checksums, size, function(err, equal) {
rsync.utils.compareContents(client.fs, checksums, function(err, equal) {
var response;
// We need to check if equal is true because equal can have three possible

Просмотреть файл

@ -439,14 +439,12 @@ var downstreamSyncSteps = {
rsync.patch(fs, data.path, data.diffs, rsyncOptions, function(err, paths) {
expect(err, "[Rsync patch error: \"" + err + "\"]").not.to.exist;
var size = rsyncOptions.size || 5;
rsyncUtils.generateChecksums(fs, paths.synced, size, function(err, checksums) {
rsyncUtils.generateChecksums(fs, paths.synced, function(err, checksums) {
expect(err, "[Rsync path checksum error: \"" + err + "\"]").not.to.exist;
expect(checksums).to.exist;
var patchResponse = SyncMessage.response.patch;
patchResponse.content = {checksums: checksums, size: size};
patchResponse.content = {checksums: checksums};
socketPackage.socket.send(patchResponse.stringify());
});
@ -936,6 +934,7 @@ function ensureFilesystem(fs, layout, callback) {
if(err) {
return callback(err);
}
ensureFilesystemContents(fs, layout, callback);
});
}

Просмотреть файл

@ -4,7 +4,6 @@ var SyncMessage = require('../../lib/syncmessage');
var MakeDrive = require('../../client/src');
var Filer = require('../../lib/filer.js');
var fsUtils = require('../../lib/fs-utils.js');
var conflict = require('../../lib/conflict.js');
describe("Server bugs", function() {
describe("[Issue 169]", function() {
@ -77,24 +76,22 @@ describe('Client bugs', function() {
});
describe('[Issue 372]', function(){
function findConflictedFilename(entries) {
entries.splice(entries.indexOf('hello'), 1);
return Filer.Path.join('/', entries[0]);
}
/**
* This test creates a file and sync then disconenct
* and change the file's content then try to connect and sync again.
*/
it('should sync and create conflicted copy', function(done) {
it('should upstream newer changes made when disconnected and not create a conflicted copy', function(done) {
var fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true});
var sync = fs.sync;
var jar;
util.authenticatedConnection(function( err, result ) {
util.authenticatedConnection(function(err, result) {
if(err) throw err;
var layout = {'/hello': 'hello'};
var layout = {'/hello': 'hello',
'/dir/world': 'world'
};
jar = result.jar;
sync.once('connected', function onConnected() {
util.createFilesystemLayout(fs, layout, function(err) {
@ -111,33 +108,21 @@ describe('Client bugs', function() {
sync.once('disconnected', function onDisconnected() {
// Re-sync with server and make sure we get our empty dir back
sync.once('connected', function onSecondDownstreamSync() {
fs.readdir('/', function(err, entries) {
if(err) throw err;
layout['/hello'] = 'hello world';
expect(entries).to.have.length(2);
expect(entries).to.include('hello');
// Make sure this is a real conflicted copy, both in name
// and also in terms of attributes on the file.
var conflictedCopyFilename = findConflictedFilename(entries);
conflict.isConflictedCopy(fs, conflictedCopyFilename, function(err, conflicted) {
expect(err).not.to.exist;
expect(conflicted).to.be.true;
// Make sure the conflicted copy has the changes we expect
fs.readFile(conflictedCopyFilename, 'utf8', function(err, data) {
if(err) throw err;
// Should have the modified content
expect(data).to.equal('hello world');
done();
});
});
util.ensureFilesystem(fs, layout, function(err) {
expect(err).not.to.exist;
});
});
util.ensureRemoteFilesystem(layout, result.jar, function(err) {
sync.once('completed', function reconnectedUpstream() {
util.ensureRemoteFilesystem(layout, jar, function(err) {
expect(err).not.to.exist;
done();
});
});
util.ensureRemoteFilesystem(layout, jar, function(err) {
if(err) throw err;
fs.writeFile('/hello', 'hello world', function (err) {
@ -152,6 +137,7 @@ describe('Client bugs', function() {
util.getWebsocketToken(result, function(err, result) {
if(err) throw err;
jar = result.jar;
sync.connect(util.socketURL, result.token);
});
});

Просмотреть файл

@ -3,6 +3,7 @@ var util = require('../lib/util.js');
var Filer = require('../../lib/filer.js');
var FileSystem = Filer.FileSystem;
var fsUtils = require('../../lib/fs-utils.js');
var CHECKSUM = require('MD5')('This is data').toString();
describe('MakeDrive fs-utils.js', function(){
var fs;
@ -34,6 +35,12 @@ describe('MakeDrive fs-utils.js', function(){
expect(fsUtils.fsetUnsynced).to.be.a.function;
expect(fsUtils.getUnsynced).to.be.a.function;
expect(fsUtils.fgetUnsynced).to.be.a.function;
expect(fsUtils.removeChecksum).to.be.a.function;
expect(fsUtils.fremoveChecksum).to.be.a.function;
expect(fsUtils.setChecksum).to.be.a.function;
expect(fsUtils.fsetChecksum).to.be.a.function;
expect(fsUtils.getChecksum).to.be.a.function;
expect(fsUtils.fgetChecksum).to.be.a.function;
});
it('should copy an existing file on forceCopy()', function(done) {
@ -127,7 +134,7 @@ describe('MakeDrive fs-utils.js', function(){
});
});
it('should work with fd vs. path', function(done) {
it('should work with fd vs. path for unsynced metadata', function(done) {
fs.open('/dir/file', 'w', function(err, fd) {
if(err) throw err;
@ -154,4 +161,63 @@ describe('MakeDrive fs-utils.js', function(){
});
});
it('should give checksum for getChecksum() if path has checksum metadata', function(done) {
fsUtils.setChecksum(fs, '/dir/file', CHECKSUM, function(err) {
expect(err).not.to.exist;
fsUtils.getChecksum(fs, '/dir/file', function(err, checksum) {
expect(err).not.to.exist;
expect(checksum).to.equal(CHECKSUM);
done();
});
});
});
it('should remove checksum metadata when calling removeChecksum()', function(done) {
fsUtils.setChecksum(fs, '/dir/file', CHECKSUM, function(err) {
expect(err).not.to.exist;
fsUtils.getChecksum(fs, '/dir/file', function(err, checksum) {
expect(err).not.to.exist;
expect(checksum).to.equal(CHECKSUM);
fsUtils.removeChecksum(fs, '/dir/file', function(err) {
expect(err).not.to.exist;
fsUtils.getChecksum(fs, '/dir/file', function(err, checksum) {
expect(err).not.to.exist;
expect(checksum).not.to.exist;
done();
});
});
});
});
});
it('should work with fd vs. path for checksum metadata', function(done) {
fs.open('/dir/file', 'w', function(err, fd) {
if(err) throw err;
fsUtils.fsetChecksum(fs, fd, CHECKSUM, function(err) {
expect(err).not.to.exist;
fsUtils.fgetChecksum(fs, fd, function(err, checksum) {
expect(err).not.to.exist;
expect(checksum).to.equal(CHECKSUM);
fsUtils.fremoveChecksum(fs, fd, function(err) {
expect(err).not.to.exist;
fsUtils.fgetChecksum(fs, fd, function(err, checksum) {
expect(err).not.to.exist;
expect(checksum).not.to.exist;
fs.close(fd);
done();
});
});
});
});
});
});
});

Просмотреть файл

@ -3,9 +3,9 @@ var expect = require('chai').expect;
var fs;
var fs2;
var provider;
var CHUNK_SIZE = 5;
var rsyncUtils = require('../../lib/rsync').utils;
var testUtils = require('../lib/util.js');
var fsUtils = require('../../lib/fs-utils');
function fsInit() {
provider = new Filer.FileSystem.providers.Memory("rsync1");
@ -37,7 +37,7 @@ describe('[Rsync Util Tests]', function() {
it('should return an EINVAL error if a filesystem is not provided', function (done) {
var filesystem;
rsyncUtils.generateChecksums(filesystem, [], CHUNK_SIZE, function (err, checksums) {
rsyncUtils.generateChecksums(filesystem, [], function (err, checksums) {
expect(err).to.exist;
expect(err.code).to.equal('EINVAL');
expect(checksums).to.not.exist;
@ -46,16 +46,7 @@ describe('[Rsync Util Tests]', function() {
});
it('should return an EINVAL error if no paths are provided', function (done) {
rsyncUtils.generateChecksums(fs, null, CHUNK_SIZE, function (err, checksums) {
expect(err).to.exist;
expect(err.code).to.equal('EINVAL');
expect(checksums).to.not.exist;
done();
});
});
it('should return an error if chunk size is not provided', function (done) {
rsyncUtils.generateChecksums(fs, [], null, function (err, checksums) {
rsyncUtils.generateChecksums(fs, null, function (err, checksums) {
expect(err).to.exist;
expect(err.code).to.equal('EINVAL');
expect(checksums).to.not.exist;
@ -64,7 +55,7 @@ describe('[Rsync Util Tests]', function() {
});
it('should return empty checksums if empty paths are provided', function (done) {
rsyncUtils.generateChecksums(fs, [], CHUNK_SIZE, function (err, checksums) {
rsyncUtils.generateChecksums(fs, [], function (err, checksums) {
expect(err).to.not.exist;
expect(checksums).to.exist;
expect(checksums).to.have.length(0);
@ -72,13 +63,13 @@ describe('[Rsync Util Tests]', function() {
});
});
it('should return an empty checksum if the path to the node provided does not exist', function (done) {
rsyncUtils.generateChecksums(fs, ['/myfile.txt'], CHUNK_SIZE, function (err, checksums) {
it('should return an empty hash checksum if the path to the node provided does not exist', function (done) {
rsyncUtils.generateChecksums(fs, ['/myfile.txt'], function (err, checksums) {
expect(err).to.not.exist;
expect(checksums).to.exist;
expect(checksums).to.have.length(1);
expect(checksums[0]).to.include.keys('checksum');
expect(checksums[0].checksum).to.have.length(0);
expect(checksums[0].checksum).to.equal('');
done();
});
});
@ -86,12 +77,12 @@ describe('[Rsync Util Tests]', function() {
it('should return empty checksums for a directory path', function (done) {
fs.mkdir('/dir', function (err) {
if(err) throw err;
rsyncUtils.generateChecksums(fs, ['/dir'], CHUNK_SIZE, function (err, checksums) {
rsyncUtils.generateChecksums(fs, ['/dir'], function (err, checksums) {
expect(err).to.not.exist;
expect(checksums).to.exist;
expect(checksums).to.have.length(1);
expect(checksums[0]).to.include.keys('checksum');
expect(checksums[0].checksum).to.have.length(0);
expect(checksums[0].checksum).to.be.undefined;
done();
});
});
@ -109,26 +100,58 @@ describe('[Rsync Util Tests]', function() {
testUtils.createFilesystemLayout(fs, layout, function (err) {
if(err) throw err;
rsyncUtils.generateChecksums(fs, paths, CHUNK_SIZE, function (err, checksums) {
rsyncUtils.generateChecksums(fs, paths, function (err, checksums) {
expect(err).to.not.exist;
expect(checksums).to.exist;
expect(checksums).to.have.length(paths.length);
expect(checksums[0]).to.include.keys('checksum');
expect(checksums[0].checksum).to.have.length(0);
expect(checksums[0].checksum).to.be.undefined;
expect(checksums[1]).to.include.keys('checksum');
expect(checksums[1].checksum).to.have.length.above(0);
expect(checksums[2]).to.include.keys('checksum');
expect(checksums[2].checksum).to.have.length(0);
expect(checksums[2].checksum).to.be.undefined;
expect(checksums[3]).to.include.keys('checksum');
expect(checksums[3].checksum).to.have.length(0);
expect(checksums[3].checksum).to.be.undefined;
expect(checksums[4]).to.include.keys('checksum');
expect(checksums[4].checksum).to.have.length(0);
expect(checksums[4].checksum).to.be.undefined;
expect(checksums[5]).to.include.keys('checksum');
expect(checksums[5].checksum).to.have.length.above(0);
done();
});
});
});
it('should stamp checksums onto files if stampNode parameter is passed', function (done) {
var layout = {'/dir1/file1': 'This is a file',
'/dir2': null,
'/file2': 'This is another file'};
var paths = Object.keys(layout);
testUtils.createFilesystemLayout(fs, layout, function (err) {
if(err) throw err;
rsyncUtils.generateChecksums(fs, paths, true, function (err, checksums) {
expect(err).not.to.exist;
expect(checksums).to.exist;
fsUtils.getChecksum(fs, '/dir1/file1', function (err, checksum) {
expect(err).not.to.exist;
expect(checksum).to.be.a('string');
expect(checksum).to.have.length.above(0);
fsUtils.getChecksum(fs, '/dir2', function (err, checksum) {
expect(err).not.to.exist;
expect(checksum).to.be.undefined;
fsUtils.getChecksum(fs, '/file2', function (err, checksum) {
expect(err).not.to.exist;
expect(checksum).to.be.a('string');
expect(checksum).to.have.length.above(0);
done();
});
});
});
});
});
});
});
describe('Rsync CompareContents', function() {
@ -143,7 +166,7 @@ describe('[Rsync Util Tests]', function() {
it('should return an EINVAL error if a filesystem is not provided', function (done) {
var filesystem;
rsyncUtils.compareContents(filesystem, [], CHUNK_SIZE, function (err, equal) {
rsyncUtils.compareContents(filesystem, [], function (err, equal) {
expect(err).to.exist;
expect(err.code).to.equal('EINVAL');
expect(equal).to.not.exist;
@ -152,16 +175,7 @@ describe('[Rsync Util Tests]', function() {
});
it('should return an EINVAL error if no checksums are provided', function (done) {
rsyncUtils.compareContents(fs, null, CHUNK_SIZE, function (err, equal) {
expect(err).to.exist;
expect(err.code).to.equal('EINVAL');
expect(equal).to.not.exist;
done();
});
});
it('should return an error if chunk size is not provided', function (done) {
rsyncUtils.compareContents(fs, [], null, function (err, equal) {
rsyncUtils.compareContents(fs, null, function (err, equal) {
expect(err).to.exist;
expect(err.code).to.equal('EINVAL');
expect(equal).to.not.exist;
@ -170,7 +184,7 @@ describe('[Rsync Util Tests]', function() {
});
it('should return true if a checksum is provided for a path that does not exist', function (done) {
rsyncUtils.compareContents(fs, [{path: '/non-existent-file.txt', checksum: []}], CHUNK_SIZE, function (err, equal) {
rsyncUtils.compareContents(fs, [{path: '/non-existent-file.txt', checksum: ''}], function (err, equal) {
expect(err).to.not.exist;
expect(equal).to.equal(true);
done();
@ -189,10 +203,10 @@ describe('[Rsync Util Tests]', function() {
testUtils.createFilesystemLayout(fs, layout, function (err) {
if(err) throw err;
rsyncUtils.generateChecksums(fs, paths, CHUNK_SIZE, function (err, checksums) {
rsyncUtils.generateChecksums(fs, paths, function (err, checksums) {
expect(err).to.not.exist;
expect(checksums).to.exist;
rsyncUtils.compareContents(fs, checksums, CHUNK_SIZE, function (err, equal) {
rsyncUtils.compareContents(fs, checksums, function (err, equal) {
expect(err).to.not.exist;
expect(equal).to.equal(true);
done();
@ -222,12 +236,12 @@ describe('[Rsync Util Tests]', function() {
testUtils.createFilesystemLayout(fs2, layout2, function (err){
if(err) throw err;
rsyncUtils.generateChecksums(fs, paths, CHUNK_SIZE, function (err, checksums) {
rsyncUtils.generateChecksums(fs, paths, function (err, checksums) {
expect(err).to.not.exist;
expect(checksums).to.exist;
rsyncUtils.generateChecksums(fs2, paths, CHUNK_SIZE, function (err) {
rsyncUtils.generateChecksums(fs2, paths, function (err, checksums2) {
expect(err).to.not.exist;
rsyncUtils.compareContents(fs2, checksums, CHUNK_SIZE, function (err, equal) {
rsyncUtils.compareContents(fs2, checksums, function (err, equal) {
expect(err).to.not.exist;
expect(equal).to.equal(false);
done();
@ -238,4 +252,55 @@ describe('[Rsync Util Tests]', function() {
});
});
});
describe('Rsync GetChecksum', function () {
beforeEach(fsInit);
afterEach(fsCleanup);
it('should be a function', function () {
expect(rsyncUtils.getChecksum).to.be.a('function');
});
it('should return an error for a directory', function (done) {
rsyncUtils.getChecksum(fs, '/', function (err, checksum) {
expect(err).to.exist;
expect(checksum).not.to.exist;
done();
});
});
it('should return an empty checksum for a non-existent file', function (done) {
rsyncUtils.getChecksum(fs, '/file.txt', function (err, checksum) {
expect(err).not.to.exist;
expect(checksum).to.equal('');
done();
});
});
it('should return a non-empty checksum for a file without content', function (done) {
fs.writeFile('/file.txt', '', function (err) {
if(err) throw err;
rsyncUtils.getChecksum(fs, '/file.txt', function (err, checksum) {
expect(err).not.to.exist;
expect(checksum).to.be.a('string');
expect(checksum).to.have.length.above(0);
done();
});
});
});
it('should return the checksum of a file', function (done) {
fs.writeFile('/file.txt', 'This is a file', function (err) {
if(err) throw err;
rsyncUtils.getChecksum(fs, '/file.txt', function (err, checksum) {
expect(err).not.to.exist;
expect(checksum).to.be.a('string');
expect(checksum).to.have.length.above(0);
done();
});
});
});
});
});

Просмотреть файл

@ -2,43 +2,55 @@ var expect = require('chai').expect;
var pathResolver = require('../../lib/sync-path-resolver.js');
describe('Resolution path tests', function () {
it('should have resolve as a function', function (done) {
expect(pathResolver.resolve).to.be.a.function;
done();
it('should have resolve as a function', function () {
expect(pathResolver.resolve).to.be.a('function');
});
it('should return / as the common path', function (done) {
it('should have resolveFromArray as a function', function() {
expect(pathResolver.resolveFromArray).to.be.a('function');
});
it('should return / as the common path', function () {
expect(pathResolver.resolve(null, null)).to.equal('/');
done();
});
it('should return /dir as the common path', function (done) {
it('should return /dir as the common path', function () {
expect(pathResolver.resolve('/dir')).to.equal('/dir');
done();
});
it('should return /dir as the common path', function (done) {
it('should return /dir as the common path', function () {
expect(pathResolver.resolve(null, '/dir')).to.equal('/dir');
done();
});
it('should return /dir as the common path', function (done) {
it('should return /dir as the common path', function () {
expect(pathResolver.resolve('/dir/myfile.txt', '/dir/myfile2.txt')).to.equal('/dir');
done();
});
it('should return /dir as the common path', function (done) {
it('should return /dir as the common path', function () {
expect(pathResolver.resolve('/dir/myfile.txt', '/dir')).to.equal('/dir');
done();
});
it('should return / as the common path', function (done) {
it('should return / as the common path', function () {
expect(pathResolver.resolve('/dir/myfile.txt', '/dir2/myfile.txt')).to.equal('/');
done();
});
it('should return / as the common path', function (done) {
it('should return / as the common path', function () {
expect(pathResolver.resolve('/', '/dir/subdir/subsubdir')).to.equal('/');
done();
});
it('should return / as the common path', function () {
expect(pathResolver.resolveFromArray([null, null])).to.equal('/');
});
it('should return /dir as the common path', function () {
expect(pathResolver.resolveFromArray(['/dir'])).to.equal('/dir');
});
it('should return /dir as the common path', function () {
expect(pathResolver.resolveFromArray([null, '/dir', null, '/dir/file'])).to.equal('/dir');
});
it('should return /dir as the common path', function () {
expect(pathResolver.resolveFromArray(['/dir/myfile1', '/dir', '/dir/myfile2', '/dir/dir2/myfile3'])).to.equal('/dir');
});
});