UNPKG

makedrive

Version:
1,026 lines (873 loc) 28.8 kB
var request = require('request'); var expect = require('chai').expect; var ws = require('ws'); var filesystem = require('../../server/lib/filesystem.js'); var SyncMessage = require('../../lib/syncmessage'); var rsync = require('../../lib/rsync'); var rsyncUtils = rsync.utils; var rsyncOptions = require('../../lib/constants').rsyncDefaults; var Filer = require('../../lib/filer.js'); var Buffer = Filer.Buffer; var Path = Filer.Path; var uuid = require( "node-uuid" ); var async = require('async'); var diffHelper = require("../../lib/diff"); var deepEqual = require('deep-equal'); var MakeDrive = require('../../client/src/index.js'); // Ensure the client timeout restricts tests to a reasonable length var env = require('../../server/lib/environment'); env.set('CLIENT_TIMEOUT_MS', 1000); // Set maximum file size limit to 2000000 bytes env.set('MAX_SYNC_SIZE_BYTES', 2000000); // Enable a username:password for BASIC_AUTH_USERS to enable /api/get route env.set('BASIC_AUTH_USERS', 'testusername:testpassword'); env.set('AUTHENTICATION_PROVIDER', 'passport-webmaker'); var server = require('../../server/server.js'); var app = server.app; var serverURL = 'http://127.0.0.1:' + env.get('PORT'), socketURL = serverURL.replace( 'http', 'ws' ); // Mock Webmaker auth app.post('/mocklogin/:username', function(req, res) { var username = req.param('username'); if(!username){ // Expected username. res.send(500); } else if( req.session && req.session.user && req.session.user.username === username) { // Already logged in. res.send(401); } else{ // Login worked. req.session.user = {username: username}; res.send(200); } }); // Mock File Upload into Filer FileSystem. URLs will look like this: // /upload/:username/:path (where :path will contain /s) app.post('/upload/*', function(req, res) { var parts = req.path.split('/'); var username = parts[2]; var path = '/' + parts.slice(3).join('/'); var fileData = []; req.on('data', function(chunk) { fileData.push(new Buffer(chunk)); }); req.on('end', function() { fileData = Buffer.concat(fileData); var fs = filesystem.create(username); fs.writeFile(path, fileData, function(err) { if(err) { res.send(500, {error: err}); return; } res.send(200); }); }); }); /** * Misc Helpers */ function ready(callback) { if(server.ready) { callback(); } else { server.once('ready', callback); } } function uniqueUsername() { return 'user' + uuid.v4(); } function upload(username, path, contents, callback) { ready(function() { request.post({ url: serverURL + '/upload/' + username + path, headers: { 'Content-Type': 'application/octet-stream' }, body: contents }, function(err, res) { expect(err).not.to.exist; expect(res.statusCode).to.equal(200); callback(); }); }); } function comparePaths(a, b) { // If objects have a .path property, use it. if(a.path && b.path) { a = a.path; b = b.path; } if(a > b) return 1; if(a < b) return -1; return 0; } function toSyncMessage(string) { try { string = JSON.parse(string); string = SyncMessage.parse(string); } catch(e) { expect(e, "[Error parsing a SyncMessage to JSON]").to.not.exist; } return string; } /** * Connection Helpers */ function getWebsocketToken(options, callback){ // Fail early and noisily when missing options.jar if(!(options && options.jar)) { throw('Expected options.jar'); } ready(function() { request({ url: serverURL + '/api/sync', jar: options.jar, json: true }, function(err, response, body) { expect(err, "[Error getting a token: " + err + "]").to.not.exist; expect(response.statusCode, "[Error getting a token: " + response.body.message + "]").to.equal(200); options.token = body; callback(null, options); }); }); } function jar() { return request.jar(); } function authenticate(options, callback){ // If no options passed, generate a unique username and jar if(typeof options === 'function') { callback = options; options = {}; } options.jar = options.jar || jar(); options.username = options.username || uniqueUsername(); options.logoutUser = function (cb) { // Reset the jar to kill existing auth cookie options.jar = jar(); cb(); }; ready(function() { request.post({ url: serverURL + '/mocklogin/' + options.username, jar: options.jar }, function(err, res) { if(err) { return callback(err); } expect(res.statusCode).to.equal(200); callback(null, options); }); }); } function authenticateAndConnect(options, callback) { if(typeof options === 'function') { callback = options; options = {}; } authenticate(options, function(err, result) { if(err) { return callback(err); } getWebsocketToken(result, function(err, result1) { if(err){ return callback(err); } var testDone = result1.done; result1.done = function() { options.logoutUser(function() { testDone && testDone(); }); }; callback(null, result1); }); }); } /** * Socket Helpers */ function openSocket(socketData, options) { if (typeof options !== "object") { if (socketData && !socketData.token) { options = socketData; socketData = null; } } options = options || {}; var socket = new ws(socketURL); function defaultHandler(msg, failout) { failout = failout || true; return function(code, data) { var details = ""; if (code) { details += ": " + code.toString(); } if (data) { details += " " + data.toString(); } expect(failout, "[Unexpected socket on" + msg + " event]" + details).to.be.false; }; } if (socketData) { var customMessageHandler = options.onMessage; options.onOpen = function() { socket.send(JSON.stringify({ token: socketData.token })); }; options.onMessage = function(message) { expect(message).to.equal(SyncMessage.response.authz.stringify()); if (customMessageHandler) { socket.once("message", customMessageHandler); } socket.send(SyncMessage.response.authz.stringify()); }; } var onOpen = options.onOpen || defaultHandler("open"); var onMessage = options.onMessage || defaultHandler("message"); var onError = options.onError || defaultHandler("error"); var onClose = options.onClose || defaultHandler("close"); socket.once("message", onMessage); socket.once("error", onError); socket.once("open", onOpen); socket.once("close", onClose); return { socket: socket, onClose: onClose, onMessage: onMessage, onError: onError, onOpen: onOpen, setClose: function(func){ socket.removeListener("close", onClose); socket.once("close", func); this.onClose = socket._events.close.listener; }, setMessage: function(func){ socket.removeListener("message", onMessage); socket.once("message", func); this.onMessage = socket._events.message.listener; }, setError: function(func){ socket.removeListener("error", onError); socket.once("error", func); this.onError = socket._events.error.listener; }, setOpen: function(func){ socket.removeListener("open", onOpen); socket.once("open", func); this.onOpen = socket._events.open.listener; } }; } // Expects 1 parameter, with each subsequent one being an object // containing a socket and an onClose callback to be deregistered function cleanupSockets(done) { var sockets = Array.prototype.slice.call(arguments, 1); sockets.forEach(function(socketPackage) { var socket = socketPackage.socket; socket.removeListener('close', socketPackage.onClose); socket.close(); }); done(); } function sendSyncMessage(socketPackage, syncMessage, callback) { socketPackage.setMessage(callback); socketPackage.socket.send(syncMessage.stringify()); } /** * Sync Helpers */ var downstreamSyncSteps = { requestSync: function(socketPackage, data, fs, customAssertions, cb) { if (!cb) { cb = customAssertions; customAssertions = null; } socketPackage.socket.removeListener("message", socketPackage.onMessage); socketPackage.socket.once("message", function(message) { // Reattach original listener socketPackage.socket.once("message", socketPackage.onMessage); if (!customAssertions) { message = toSyncMessage(message); expect(message.type, "[Diffs error: \"" + (message.content && message.content.error) + "\"]").to.equal(SyncMessage.REQUEST); expect(message.name).to.equal(SyncMessage.CHKSUM); expect(message.content).to.exist; expect(message.content.srcList).to.exist; expect(message.content.path).to.exist; return cb(null, data); } customAssertions(message, cb); }); // send response reset var resetDownstream = SyncMessage.response.reset; socketPackage.socket.send(resetDownstream.stringify()); }, generateDiffs: function(socketPackage, data, fs, customAssertions, cb) { if (!cb) { cb = customAssertions; customAssertions = null; } var path = data.path; var srcList = data.srcList; socketPackage.socket.removeListener("message", socketPackage.onMessage); socketPackage.socket.once("message", function(message) { // Reattach original listener socketPackage.socket.once("message", socketPackage.onMessage); if (!customAssertions) { message = toSyncMessage(message); expect(message.type, "[Diffs error: \"" + (message.content && message.content.error) + "\"]").to.equal(SyncMessage.RESPONSE); expect(message.name).to.equal(SyncMessage.DIFFS); expect(message.content).to.exist; expect(message.content.diffs).to.exist; expect(message.content.path).to.exist; data.diffs = diffHelper.deserialize(message.content.diffs); return cb(null, data); } customAssertions(message, cb); }); rsync.checksums(fs, path, srcList, rsyncOptions, function( err, checksums ) { if(err){ cb(err); } var diffRequest = SyncMessage.request.diffs; diffRequest.content = { checksums: checksums }; socketPackage.socket.send(diffRequest.stringify()); }); }, patchClientFilesystem: function(socketPackage, data, fs, customAssertions, cb) { if (!cb) { cb = customAssertions; customAssertions = null; } socketPackage.socket.removeListener("message", socketPackage.onMessage); socketPackage.socket.once("message", function(message) { // Reattach the original listener socketPackage.socket.once("message", socketPackage.onMessage); if(!customAssertions) { message = toSyncMessage(message); expect(message).to.exist; expect(message).to.deep.equal(SyncMessage.response.verification); return cb(null, data); } customAssertions(message, cb); }); rsync.patch(fs, data.path, data.diffs, rsyncOptions, function(err, paths) { expect(err, "[Rsync patch error: \"" + err + "\"]").not.to.exist; 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}; socketPackage.socket.send(patchResponse.stringify()); }); }); } }; var upstreamSyncSteps = { requestSync: function(socketPackage, data, customAssertions, cb) { if (!cb) { cb = customAssertions; customAssertions = null; } socketPackage.socket.removeListener("message", socketPackage.onMessage); socketPackage.socket.once("message", function(message) { // Reattach original listener socketPackage.socket.once("message", socketPackage.onMessage); if (!customAssertions) { message = toSyncMessage(message); expect(message).to.exist; expect(message.type).to.equal(SyncMessage.RESPONSE); expect(message.name, "[SyncMessage Type error. SyncMessage.content was: " + message.content + "]").to.equal(SyncMessage.SYNC); return cb(data); } customAssertions(message, cb); }); var requestSyncMessage = SyncMessage.request.sync; requestSyncMessage.content = {path: data.path}; socketPackage.socket.send(requestSyncMessage.stringify()); }, generateChecksums: function(socketPackage, data, customAssertions, cb) { if (!cb) { cb = customAssertions; customAssertions = null; } socketPackage.socket.removeListener("message", socketPackage.onMessage); socketPackage.socket.once("message", function(message) { // Reattach original listener socketPackage.socket.once("message", socketPackage.onMessage); message = toSyncMessage(message); if (!customAssertions) { expect(message).to.exist; expect(message.type).to.equal(SyncMessage.REQUEST); expect(message.name, "[SyncMessage Type error. SyncMessage.content was: " + message.content + "]").to.equal(SyncMessage.DIFFS); expect(message.content).to.exist; expect(message.content.checksums).to.exist; expect(message.content.path).to.exist; return cb(); } customAssertions(message, cb); }); var requestChksumMsg = SyncMessage.request.chksum; requestChksumMsg.content = { srcList: data.srcList }; socketPackage.socket.send(requestChksumMsg.stringify()); }, patchServerFilesystem: function(socketPackage, data, fs, customAssertions, cb) { if (!cb) { cb = customAssertions; customAssertions = null; } var path = data.path; var checksums = data.checksums; socketPackage.socket.removeListener("message", socketPackage.onMessage); socketPackage.socket.once("message", function(message) { // Reattach original listener socketPackage.socket.once("message", socketPackage.onMessage); if (!customAssertions) { message = toSyncMessage(message); expect(message.type, "[Diffs error: \"" + (message.content && message.content.error) + "\"]").to.equal(SyncMessage.RESPONSE); expect(message.name).to.equal(SyncMessage.PATCH); return cb(); } customAssertions(message, cb); }); rsync.diff(fs, path, checksums, rsyncOptions, function( err, diffs ) { if(err){ return cb(err); } var patchResponse = SyncMessage.response.patch; patchResponse.content = { diffs: diffHelper.serialize(diffs) }; socketPackage.socket.send(patchResponse.stringify()); }); } }; function prepareDownstreamSync(finalStep, username, token, cb){ if (typeof cb !== "function") { cb = token; token = username; username = finalStep; finalStep = null; } var testFile = { name: "test.txt", content: "Hello world!" }; // Set up server filesystem upload(username, '/' + testFile.name, testFile.content, function() { // Set up client filesystem var fs = filesystem.create(username + 'client'); var socketPackage = openSocket({ onMessage: function(message) { message = toSyncMessage(message); expect(message).to.exist; expect(message.type).to.equal(SyncMessage.RESPONSE); expect(message.name).to.equal(SyncMessage.AUTHZ); expect(message.content).to.be.null; socketPackage.socket.once("message", function(message) { message = toSyncMessage(message); expect(message).to.exist; expect(message.type).to.equal(SyncMessage.REQUEST); expect(message.name).to.equal(SyncMessage.CHKSUM); expect(message.content).to.exist; expect(message.content.srcList).to.exist; expect(message.content.path).to.exist; var downstreamData = { srcList: message.content.srcList, path: message.content.path }; // Complete required sync steps if (!finalStep) { return cb(null, downstreamData, fs, socketPackage); } downstreamSyncSteps.generateDiffs(socketPackage, downstreamData, fs, function(err, data1) { if(err){ return cb(err); } if (finalStep === "generateDiffs") { return cb(null, data1, fs, socketPackage); } downstreamSyncSteps.patchClientFilesystem(socketPackage, data1, fs, function(err, data2) { if(err){ return cb(err); } cb(null, data2, fs, socketPackage); }); }); }); socketPackage.socket.send(SyncMessage.response.authz.stringify()); }, onOpen: function() { socketPackage.socket.send(JSON.stringify({ token: token })); } }); }); } function completeDownstreamSync(username, token, cb) { prepareDownstreamSync("patch", username, token, function(err, data, fs, socketPackage) { if(err){ cb(err); } cb(null, data, fs, socketPackage); }); } function prepareUpstreamSync(finalStep, username, token, cb){ if (typeof cb !== "function") { cb = token; token = username; username = finalStep; finalStep = null; } completeDownstreamSync(username, token, function(err, data, fs, socketPackage) { if(err){ return cb(err); } // Complete required sync steps if (!finalStep) { return cb(null, data, fs, socketPackage); } upstreamSyncSteps.requestSync(socketPackage, data, function(data1) { if (finalStep === "requestSync") { return cb(data1, fs, socketPackage); } upstreamSyncSteps.generateChecksums(socketPackage, data1, fs, function(data2) { if (finalStep === "generateChecksums") { return cb(data2, fs, socketPackage); } upstreamSyncSteps.patchServerFilesystem(socketPackage, data2, fs, function(err, data3) { if(err){ return cb(err); } cb(null, data3, fs, socketPackage); }); }); }); }); } function createFilesystemLayout(fs, layout, callback) { var paths = Object.keys(layout); var sh = fs.Shell(); function createPath(path, callback) { var contents = layout[path]; // Path is either a file (string/Buffer) or empty dir (null) if(contents) { sh.mkdirp(Path.dirname(path), function(err) { if(err) { return callback(err); } fs.writeFile(path, contents, callback); }); } else { sh.mkdirp(path, callback); } } async.eachSeries(paths, createPath, callback); } /** * Deletes all paths specified in paths array, or everything * if no paths are given. */ function deleteFilesystemLayout(fs, paths, callback) { if(!paths) { fs.readdir('/', function(err, entries) { if(err) { return callback(err); } entries = entries.map(function(path) { return Path.join('/', path); }); deleteFilesystemLayout(fs, entries, callback); }); } else { var sh = fs.Shell(); var rm = function(path, callback) { sh.rm(path, {recursive: true}, callback); }; async.eachSeries(paths, rm, callback); } } // Strip .modified times from ever element in the array, or its .contents function stripModified(listing) { function strip(item) { delete item.modified; if(item.contents) { item.contents = stripModified(item.contents); } return item; } if(Array.isArray(listing)) { return listing.map(strip); } else { return strip(listing); } } /** * Makes sure that the layout given matches what's actually * in the current fs. Use ensureFilesystemContents if you * want to ensure file/dir contents vs. paths. */ function ensureFilesystemLayout(fs, layout, callback) { // Start by creating the layout, then compare a deep ls() var fs2 = new Filer.FileSystem({provider: new Filer.FileSystem.providers.Memory(uniqueUsername())}); createFilesystemLayout(fs2, layout, function(err) { if(err) { return callback(err); } var sh = fs.Shell(); sh.ls('/', {recursive: true}, function(err, fsListing) { if(err) { return callback(err); } var sh2 = fs2.Shell(); sh2.ls('/', {recursive: true}, function(err, fs2Listing) { if(err) { return callback(err); } // Remove modified fsListing = stripModified(fsListing); fs2Listing = stripModified(fs2Listing); expect(deepEqual(fsListing, fs2Listing, {ignoreArrayOrder: true, compareFn: comparePaths})).to.be.true; callback(); }); }); }); } /** * Makes sure that the layout given matches what's actually * in the remote fs. Use ensureFilesystemContents if you * want to ensure file/dir contents vs. paths. */ function ensureRemoteFilesystemLayout(layout, jar, callback) { // Start by creating the layout, then compare a deep ls() var layoutFS = new Filer.FileSystem({provider: new Filer.FileSystem.providers.Memory(uniqueUsername())}); createFilesystemLayout(layoutFS, layout, function(err) { if(err) { return callback(err); } var sh = layoutFS.Shell(); sh.ls('/', {recursive: true}, function(err, layoutFSListing) { if(err) { return callback(err); } ready(function() { // Now grab the remote server listing using the /j/* route request.get({ url: serverURL + '/j/', jar: jar, json: true }, function(err, res, remoteFSListing) { expect(err).not.to.exist; expect(res.statusCode).to.equal(200); // Remove modified layoutFSListing = stripModified(layoutFSListing); remoteFSListing = stripModified(remoteFSListing); expect(deepEqual(remoteFSListing, layoutFSListing, {ignoreArrayOrder: true, compareFn: comparePaths})).to.be.true; callback(err); }); }); }); }); } /** * Ensure that the files and dirs match the layout's contents. * Use ensureFilesystemLayout if you want to ensure file/dir paths vs. contents. */ function ensureFilesystemContents(fs, layout, callback) { function ensureFileContents(filename, expectedContents, callback) { var encoding = Buffer.isBuffer(expectedContents) ? null : 'utf8'; fs.readFile(filename, encoding, function(err, actualContents) { if(err) { return callback(err); } expect(actualContents).to.deep.equal(expectedContents); callback(); }); } function ensureEmptyDir(dirname, callback) { fs.stat(dirname, function(err, stats) { if(err) { return callback(err); } expect(stats.isDirectory()).to.be.true; // Also make sure it's empty fs.readdir(dirname, function(err, entries) { if(err) { return callback(err); } expect(entries.length).to.equal(0); callback(); }); }); } function processPath(path, callback) { var contents = layout[path]; if(contents) { ensureFileContents(path, contents, callback); } else { ensureEmptyDir(path, callback); } } async.eachSeries(Object.keys(layout), processPath, callback); } /** * Ensure that the remote files and dirs match the layout's contents. * Use ensureRemoteFilesystemLayout if you want to ensure file/dir paths vs. contents. */ function ensureRemoteFilesystemContents(layout, jar, callback) { function ensureRemoteFileContents(filename, expectedContents, callback) { request.get({ url: serverURL + '/j' + filename, jar: jar, json: true }, function(err, res, actualContents) { expect(err).not.to.exist; expect(res.statusCode).to.equal(200); if(!Buffer.isBuffer(expectedContents)) { expectedContents = new Buffer(expectedContents); } if(!Buffer.isBuffer(actualContents)) { actualContents = new Buffer(actualContents); } expect(actualContents).to.deep.equal(expectedContents); callback(err); }); } function ensureRemoteEmptyDir(dirname, callback) { request.get({ url: serverURL + '/j' + dirname, jar: jar, json: true }, function(err, res, listing) { expect(err).not.to.exist; expect(res.statusCode).to.equal(200); expect(Array.isArray(listing)).to.be.true; expect(listing.length).to.equal(0); callback(err); }); } function processPath(path, callback) { ready(function() { var contents = layout[path]; if(contents) { ensureRemoteFileContents(path, contents, callback); } else { ensureRemoteEmptyDir(path, callback); } }); } async.eachSeries(Object.keys(layout), processPath, callback); } /** * Runs ensureFilesystemLayout and ensureFilesystemContents on fs * for given layout, making sure all paths and files/dirs match expected. */ function ensureFilesystem(fs, layout, callback) { ensureFilesystemLayout(fs, layout, function(err) { if(err) { return callback(err); } ensureFilesystemContents(fs, layout, callback); }); } /** * Runs ensureRemoteFilesystemLayout and ensureRemoteFilesystemContents * for given layout, making sure all paths and files/dirs match expected. */ function ensureRemoteFilesystem(layout, jar, callback) { ensureRemoteFilesystemLayout(layout, jar, function(err) { if(err) { return callback(err); } ensureRemoteFilesystemContents(layout, jar, callback); }); } /** * Setup a new client connection and do a downstream sync, leaving the * connection open. If a layout is given, we also sync that up to the server. * Callers should disconnect the client when done. Callers can pass Filer * FileSystem options on the options object. */ function setupSyncClient(options, callback) { authenticateAndConnect(options, function(err, result) { if(err) { return callback(err); } // Make sure we have sane defaults on the options object for a filesystem options.provider = options.provider || new Filer.FileSystem.providers.Memory(result.username + Date.now()); options.manual = options.manual !== false; options.forceCreate = true; var fs = MakeDrive.fs(options); var sync = fs.sync; var client = { jar: result.jar, username: result.username, fs: fs, sync: sync }; sync.once('connected', function onConnected() { if(options.layout) { sync.once('completed', function onUpstreamCompleted() { callback(null, client); }); createFilesystemLayout(fs, options.layout, function(err) { if(err) { return callback(err); } sync.request(); }); } else { callback(null, client); } }); sync.once('error', function(err) { // This should never happen, and if it does, we need to fail loudly. console.error('Unexepcted sync `error` event', err.stack); throw err; }); sync.connect(socketURL, result.token); }); } module.exports = { // Misc helpers ready: ready, app: app, serverURL: serverURL, socketURL: socketURL, username: uniqueUsername, createJar: jar, toSyncMessage: toSyncMessage, // Connection helpers authenticate: authenticate, authenticatedConnection: authenticateAndConnect, getWebsocketToken: getWebsocketToken, // Socket helpers openSocket: openSocket, upload: upload, cleanupSockets: cleanupSockets, // Filesystem helpers createFilesystemLayout: createFilesystemLayout, deleteFilesystemLayout: deleteFilesystemLayout, ensureFilesystemContents: ensureFilesystemContents, ensureFilesystemLayout: ensureFilesystemLayout, ensureRemoteFilesystemContents: ensureRemoteFilesystemContents, ensureRemoteFilesystemLayout: ensureRemoteFilesystemLayout, ensureFilesystem: ensureFilesystem, ensureRemoteFilesystem: ensureRemoteFilesystem, // Sync helpers prepareDownstreamSync: prepareDownstreamSync, prepareUpstreamSync: prepareUpstreamSync, downstreamSyncSteps: downstreamSyncSteps, upstreamSyncSteps: upstreamSyncSteps, sendSyncMessage: sendSyncMessage, completeDownstreamSync: completeDownstreamSync, setupSyncClient: setupSyncClient };