From 72f85bd7937cc0cb27379022eeede7fd34012bf7 Mon Sep 17 00:00:00 2001 From: benweet Date: Sun, 4 May 2014 18:31:57 +0100 Subject: [PATCH] Added teamserver provider/helper --- public/res/constants.js | 3 +- public/res/helpers/googleHelper.js | 41 ++-- public/res/helpers/teamserverHelper.js | 232 ++++++++++++++++++ public/res/providers/gdriveProviderBuilder.js | 17 +- public/res/providers/teamserverProvider.js | 5 + .../providers/teamserverProviderBuilder.js | 200 +++++++++++++++ public/res/settings.js | 1 + public/res/synchronizer.js | 1 + 8 files changed, 461 insertions(+), 39 deletions(-) create mode 100644 public/res/helpers/teamserverHelper.js create mode 100644 public/res/providers/teamserverProvider.js create mode 100644 public/res/providers/teamserverProviderBuilder.js diff --git a/public/res/constants.js b/public/res/constants.js index a15ea491..d29e693d 100644 --- a/public/res/constants.js +++ b/public/res/constants.js @@ -27,8 +27,9 @@ define([], function() { constants.PICASA_PROXY_URL = "https://stackedit-picasa-proxy.herokuapp.com/"; constants.SSH_PROXY_URL = "https://stackedit-ssh-proxy.herokuapp.com/"; constants.HTMLTOPDF_URL = "https://stackedit-htmltopdf.herokuapp.com/"; + constants.TEAM_SERVER_URL = "http://localhost:11583/"; - // Site dependent + // Site dependent constants.BASE_URL = "http://localhost/"; constants.GOOGLE_CLIENT_ID = '241271498917-lev37kef013q85avc91am1gccg5g8lrb.apps.googleusercontent.com'; constants.GITHUB_CLIENT_ID = 'e47fef6055344579799d'; diff --git a/public/res/helpers/googleHelper.js b/public/res/helpers/googleHelper.js index 535a7ee1..d6543043 100644 --- a/public/res/helpers/googleHelper.js +++ b/public/res/helpers/googleHelper.js @@ -63,12 +63,10 @@ define([ task.onRun(function() { if(isOffline === true) { connected = false; - task.error(new Error("Operation not available in offline mode.|stopPublish")); - return; + return task.error(new Error("Operation not available in offline mode.|stopPublish")); } if(connected === true) { - task.chain(); - return; + return task.chain(); } window.delayedFunction = function() { gapi.load("client", function() { @@ -148,8 +146,7 @@ define([ var immediate; function localAuthenticate() { if(authuser > 5) { - task.error(new Error('Unable to authenticate user ' + authorizationMgr.getUserId() + ', please sign in with Google.')); - return; + return task.error(new Error('Unable to authenticate user ' + authorizationMgr.getUserId() + ', please sign in with Google.')); } if(immediate === false) { task.timeout = constants.ASYNC_TASK_LONG_TIMEOUT; @@ -184,8 +181,7 @@ define([ } function oauthRedirect() { if(immediate === true) { - task.chain(localAuthenticate); - return; + return task.chain(localAuthenticate); } utils.redirectConfirm('You are being redirected to Google authorization page.', function() { task.chain(localAuthenticate); @@ -257,7 +253,7 @@ define([ var headers = { 'Content-Type': 'multipart/mixed; boundary="' + boundary + '"', }; - + var base64Data = utils.encodeBase64(content); var multipartRequestBody = [ delimiter, @@ -288,8 +284,7 @@ define([ // Upload success result = response; result.content = content; - task.chain(); - return; + return task.chain(); } var error = response.error; // Handle error @@ -332,8 +327,7 @@ define([ if(response && response.id) { // Rename success result = response; - task.chain(); - return; + return task.chain(); } var error = response.error; // Handle error @@ -380,8 +374,7 @@ define([ request.execute(function(response) { if(!response || !response.largestChangeId) { // Handle error - handleError(response.error, task); - return; + return handleError(response.error, task); } // Retrieve success newChangeId = response.largestChangeId; @@ -419,8 +412,7 @@ define([ task.onRun(function() { function recursiveDownloadMetadata() { if(ids.length === 0) { - task.chain(); - return; + return task.chain(); } var id = ids[0]; var headers = {}; @@ -475,8 +467,7 @@ define([ task.onRun(function() { function recursiveDownloadContent() { if(objects.length === 0) { - task.chain(); - return; + return task.chain(); } var object = objects[0]; result.push(object); @@ -491,8 +482,7 @@ define([ } if(!file) { objects.shift(); - task.chain(recursiveDownloadContent); - return; + return task.chain(recursiveDownloadContent); } var url = file.downloadUrl; // if file is a real time document @@ -604,16 +594,14 @@ define([ errorMsg = "Google error (" + error.code + ": " + error.message + ")."; if(error.code >= 500 && error.code < 600) { // Retry as described in Google's best practices - task.retry(new Error(errorMsg)); - return; + return task.retry(new Error(errorMsg)); } else if(error.code === 401 || error.code === 403 || error.code == "token_refresh_required") { _.each(authorizationMgrMap, function(authorizationMgr) { authorizationMgr.setRefreshFlag(); }); errorMsg = "Access to Google account is not authorized."; - task.retry(new Error(errorMsg), 1); - return; + return task.retry(new Error(errorMsg), 1); } else if(error.code === 0 || error.code === -1) { connected = false; @@ -632,8 +620,7 @@ define([ function loadPicker(task) { task.onRun(function() { if(pickerLoaded === true) { - task.chain(); - return; + return task.chain(); } $.ajax({ url: "//www.google.com/jsapi", diff --git a/public/res/helpers/teamserverHelper.js b/public/res/helpers/teamserverHelper.js new file mode 100644 index 00000000..e205829e --- /dev/null +++ b/public/res/helpers/teamserverHelper.js @@ -0,0 +1,232 @@ +/*global gapi, google */ +define([ + "underscore", + "jquery", + "constants", + "core", + "utils", + "storage", + "logger", + "settings", + "eventMgr", + "classes/AsyncTask" +], function(_, $, constants, core, utils, storage, logger, settings, eventMgr, AsyncTask) { + + var connected = false; + var authenticated = true; + + var teamserverHelper = {}; + + // Listen to offline status changes + var isOffline = false; + eventMgr.addListener("onOfflineChanged", function(isOfflineParam) { + isOffline = isOfflineParam; + }); + + function connect(task) { + task.onRun(function() { + if(isOffline === true) { + connected = false; + return task.error(new Error("Operation not available in offline mode.|stopPublish")); + } + if(connected === true) { + return task.chain(); + } + $.ajax({ + url: settings.teamserverURL + '/ping', + timeout: constants.AJAX_TIMEOUT + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + handleError(error, task); + }); + }); + } + + function authenticate(task) { + task.onRun(function() { + if(authenticated === true) { + return task.chain(); + } + }); + } + + teamserverHelper.upload = function(repo, id, title, content, callback) { + var result; + var task = new AsyncTask(); + connect(task); + authenticate(task); + task.onRun(function() { + var url = settings.teamserverURL + '/repo/' + repo + '/document'; + var type = 'POST'; + if(id) { + url += '/' + id; + type = 'PUT'; + } + $.ajax({ + url: url, + type: type, + data: { + title: title, + content: content + }, + dataType: "json", + timeout: constants.AJAX_TIMEOUT + }).done(function(data) { + result = data; + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + // Handle error + if(error.code === 404) { + error = 'File ID "' + id + '" not found on the Team Server.'; + } + handleError(error, task); + }); + }); + task.onSuccess(function() { + callback(undefined, result); + }); + task.onError(function(error) { + callback(error); + }); + task.enqueue(); + }; + + teamserverHelper.checkChanges = function(repo, lastChangeId, accountId, callback) { + var changes; + var newChangeId = lastChangeId; + var task = new AsyncTask(); + connect(task); + authenticate(task); + task.onRun(function() { + var url = settings.teamserverURL + '/repo/' + repo + '/changes'; + var type = 'GET'; + if(lastChangeId) { + url += lastChangeId; + } + $.ajax({ + url: url, + type: type, + dataType: "json", + timeout: constants.AJAX_TIMEOUT + }).done(function(data) { + newChangeId = data.newChangeId; + changes = data.changes; + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + handleError(error, task); + }); + }); + task.onSuccess(function() { + callback(undefined, changes, newChangeId); + }); + task.onError(function(error) { + callback(error); + }); + task.enqueue(); + }; + + teamserverHelper.download = function(repo, ids, callback) { + var result = []; + var task = new AsyncTask(); + connect(task); + authenticate(task); + task.onRun(function() { + function recursiveDownloadMetadata() { + if(ids.length === 0) { + return task.chain(); + } + var id = ids[0]; + var url = settings.teamserverURL + '/repo/' + repo + '/document/' + id; + $.ajax({ + url: url, + dataType: "json", + timeout: constants.AJAX_TIMEOUT + }).done(function(data) { + result.push(data); + ids.shift(); + task.chain(recursiveDownloadMetadata); + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + if(error.code === 404) { + error = 'File ID "' + id + '" not found on the Team Server.'; + } + handleError(error, task); + }); + } + task.chain(recursiveDownloadMetadata); + }); + task.onSuccess(function() { + callback(undefined, result); + }); + task.onError(function(error) { + callback(error); + }); + task.enqueue(); + }; + + function handleError(error, task) { + var errorMsg; + if(error) { + logger.error(error); + // Try to analyze the error + if(typeof error === "string") { + errorMsg = error; + } + else { + errorMsg = "Google error (" + error.code + ": " + error.message + ")."; + if(error.code >= 500 && error.code < 600) { + // Retry as described in Google's best practices + return task.retry(new Error(errorMsg)); + } + else if(error.code === 401 || error.code === 403) { + authenticated = false; + return task.retry(new Error(errorMsg), 1); + } + else if(error.code === 0 || error.code === -1) { + connected = false; + errorMsg = "|stopPublish"; + } + } + } + task.error(new Error(errorMsg)); + } + + teamserverHelper.picker = function(repo, callback) { + var docs = []; + var picker; + + function hidePicker() { + if(picker !== undefined) { + picker.setVisible(false); + $(".modal-backdrop, .picker").remove(); + } + } + + var task = new AsyncTask(); + // Add some time for user to choose his files + task.timeout = constants.ASYNC_TASK_LONG_TIMEOUT; + connect(task); + task.onSuccess(function() { + callback(undefined, docs); + }); + task.onError(function(error) { + hidePicker(); + callback(error); + }); + task.enqueue(); + }; + + return teamserverHelper; +}); diff --git a/public/res/providers/gdriveProviderBuilder.js b/public/res/providers/gdriveProviderBuilder.js index 84cd5faa..b0f5e575 100644 --- a/public/res/providers/gdriveProviderBuilder.js +++ b/public/res/providers/gdriveProviderBuilder.js @@ -91,8 +91,7 @@ define([ var syncIndex = createSyncIndex(doc.id); var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); if(fileDesc !== undefined) { - eventMgr.onError('"' + fileDesc.title + '" was already imported.'); - return; + return eventMgr.onError('"' + fileDesc.title + '" was already imported.'); } importIds.push(doc.id); }); @@ -109,16 +108,14 @@ define([ var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); if(fileDesc !== undefined) { eventMgr.onError('File ID is already synchronized with "' + fileDesc.title + '".'); - callback(true); - return; + return callback(true); } } var parentId = utils.getInputTextValue('#input-sync-export-' + providerId + '-parentid'); var data = gdriveProvider.serializeContent(content, discussionListJSON); googleHelper.upload(fileId, parentId, title, data, undefined, undefined, accountId, function(error, result) { if(error) { - callback(error); - return; + return callback(error); } var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON); callback(undefined, syncAttributes); @@ -128,7 +125,7 @@ define([ gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) { if( (syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed - (syncAttributes.titleCRC == titleCRC) && // Content CRC hasn't changed + (syncAttributes.titleCRC == titleCRC) && // Title CRC hasn't changed (syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed ) { return callback(undefined, false); @@ -144,8 +141,7 @@ define([ var data = gdriveProvider.serializeContent(content, discussionList); googleHelper.upload(syncAttributes.id, undefined, title, data, undefined, syncAttributes.etag, accountId, function(error, result) { if(error) { - callback(error, true); - return; + return callback(error, true); } syncAttributes.etag = result.etag; // Remove this deprecated flag if any @@ -167,8 +163,7 @@ define([ var lastChangeId = parseInt(storage[accountId + ".gdrive.lastChangeId"], 10); googleHelper.checkChanges(lastChangeId, accountId, function(error, changes, newChangeId) { if(error) { - callback(error); - return; + return callback(error); } var interestingChanges = []; _.each(changes, function(change) { diff --git a/public/res/providers/teamserverProvider.js b/public/res/providers/teamserverProvider.js new file mode 100644 index 00000000..779f3eb5 --- /dev/null +++ b/public/res/providers/teamserverProvider.js @@ -0,0 +1,5 @@ +define([ + "providers/teamserverProviderBuilder" +], function(teamserverProviderBuilder) { + return teamserverProviderBuilder("teamserver", "Team Server"); +}); \ No newline at end of file diff --git a/public/res/providers/teamserverProviderBuilder.js b/public/res/providers/teamserverProviderBuilder.js new file mode 100644 index 00000000..e61dbab0 --- /dev/null +++ b/public/res/providers/teamserverProviderBuilder.js @@ -0,0 +1,200 @@ +define([ + "jquery", + "underscore", + "constants", + "utils", + "storage", + "logger", + "classes/Provider", + "settings", + "eventMgr", + "fileMgr", + "editor", + "helpers/teamserverHelper" +], function($, _, constants, utils, storage, logger, Provider, settings, eventMgr, fileMgr, editor, teamserverHelper) { + + return function(providerId, providerName) { + var repo = 'teamserver'; + + var teamserverProvider = new Provider(providerId, providerName); + + function createSyncIndex(id) { + return "sync." + providerId + "." + id; + } + + var merge = settings.conflictMode == 'merge'; + function createSyncAttributes(id, sha, content, title, discussionListJSON) { + discussionListJSON = discussionListJSON || '{}'; + var syncAttributes = {}; + syncAttributes.provider = teamserverProvider; + syncAttributes.id = id; + syncAttributes.sha = sha; + syncAttributes.contentCRC = utils.crc32(content); + syncAttributes.titleCRC = utils.crc32(title); + syncAttributes.discussionListCRC = utils.crc32(discussionListJSON); + syncAttributes.syncIndex = createSyncIndex(id); + if(merge === true) { + // Need to store the whole content for merge + syncAttributes.content = content; + syncAttributes.title = title; + syncAttributes.discussionList = discussionListJSON; + } + return syncAttributes; + } + + function importFilesFromIds(ids) { + teamserverHelper.download(repo, ids, function(error, result) { + if(error) { + return; + } + var fileDescList = []; + var fileDesc; + _.each(result, function(file) { + var parsedContent = teamserverProvider.parseContent(file.content); + var syncLocations; + var syncAttributes = createSyncAttributes(file.id, file.sha, parsedContent.content, file.title, parsedContent.discussionListJSON); + syncLocations = {}; + syncLocations[syncAttributes.syncIndex] = syncAttributes; + fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionListJSON, syncLocations); + fileDescList.push(fileDesc); + }); + if(fileDesc !== undefined) { + eventMgr.onSyncImportSuccess(fileDescList, teamserverProvider); + fileMgr.selectFile(fileDesc); + } + }); + } + + teamserverProvider.importFiles = function() { + teamserverHelper.picker(repo, function(error, docs) { + if(error || docs.length === 0) { + return; + } + var importIds = []; + _.each(docs, function(doc) { + var syncIndex = createSyncIndex(doc.id); + var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); + if(fileDesc !== undefined) { + eventMgr.onError('"' + fileDesc.title + '" was already imported.'); + return; + } + importIds.push(doc.id); + }); + importFilesFromIds(importIds); + }); + }; + + teamserverProvider.exportFile = function(event, title, content, discussionListJSON, callback) { + var data = teamserverProvider.serializeContent(content, discussionListJSON); + teamserverHelper.upload(repo, undefined, title, data, function(error, result) { + if(error) { + return callback(error); + } + var syncAttributes = createSyncAttributes(result.id, result.sha, content, title, discussionListJSON); + callback(undefined, syncAttributes); + }); + }; + + teamserverProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) { + if( + (syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed + (syncAttributes.titleCRC == titleCRC) && // Title CRC hasn't changed + (syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed + ) { + return callback(undefined, false); + } + + var data = teamserverProvider.serializeContent(content, discussionList); + teamserverHelper.upload(repo, syncAttributes.id, title, data, function(error, result) { + if(error) { + callback(error, true); + return; + } + syncAttributes.etag = result.etag; + if(merge === true) { + // Need to store the whole content for merge + syncAttributes.content = content; + syncAttributes.title = title; + syncAttributes.discussionList = discussionList; + } + syncAttributes.contentCRC = contentCRC; + syncAttributes.titleCRC = titleCRC; + syncAttributes.discussionListCRC = discussionListCRC; + callback(undefined, true); + }); + }; + + teamserverProvider.syncDown = function(callback) { + var lastChangeId = parseInt(storage["teamserver.lastChangeId"], 10); + teamserverHelper.checkChanges(repo, lastChangeId, function(error, changes, newChangeId) { + if(error) { + return callback(error); + } + var interestingChanges = []; + _.each(changes, function(change) { + var syncIndex = createSyncIndex(change.id); + var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); + var syncAttributes = fileDesc && fileDesc.syncLocations[syncIndex]; + if(!syncAttributes) { + return; + } + // Store fileDesc and syncAttributes references to avoid 2 times search + change.fileDesc = fileDesc; + change.syncAttributes = syncAttributes; + // Delete + if(change.deleted === true) { + interestingChanges.push(change); + return; + } + // Modify + if(syncAttributes.sha != change.sha) { + interestingChanges.push(change); + } + }); + teamserverHelper.downloadContent(repo, interestingChanges, function(error, changes) { + if(error) { + return callback(error); + } + function mergeChange() { + if(changes.length === 0) { + storage["teamserver.lastChangeId"] = newChangeId; + return callback(); + } + var change = changes.pop(); + var fileDesc = change.fileDesc; + var syncAttributes = change.syncAttributes; + // File deleted + if(change.deleted === true) { + eventMgr.onError('"' + fileDesc.title + '" has been removed from ' + providerName + '.'); + fileDesc.removeSyncLocation(syncAttributes); + return eventMgr.onSyncRemoved(fileDesc, syncAttributes); + } + var parsedContent = teamserverProvider.parseContent(change.content); + var remoteContent = parsedContent.content; + var remoteTitle = change.title; + var remoteDiscussionListJSON = parsedContent.discussionListJSON; + var remoteDiscussionList = parsedContent.discussionList; + var remoteCRC = teamserverProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON); + + // Update syncAttributes + syncAttributes.sha = change.sha; + if(merge === true) { + // Need to store the whole content for merge + syncAttributes.content = remoteContent; + syncAttributes.title = remoteTitle; + syncAttributes.discussionList = remoteDiscussionListJSON; + } + syncAttributes.contentCRC = remoteCRC.contentCRC; + syncAttributes.titleCRC = remoteCRC.titleCRC; + syncAttributes.discussionListCRC = remoteCRC.discussionListCRC; + utils.storeAttributes(syncAttributes); + setTimeout(mergeChange, 5); + } + setTimeout(mergeChange, 5); + }); + }); + }; + + return teamserverProvider; + }; +}); diff --git a/public/res/settings.js b/public/res/settings.js index c3409cd7..a481b1ec 100644 --- a/public/res/settings.js +++ b/public/res/settings.js @@ -56,6 +56,7 @@ define([ '' ].join(""), pdfPageSize: 'A4', + teamserverURL: constants.TEAM_SERVER_URL, sshProxy: constants.SSH_PROXY_URL, extensionSettings: {} }; diff --git a/public/res/synchronizer.js b/public/res/synchronizer.js index 16591790..4880367a 100644 --- a/public/res/synchronizer.js +++ b/public/res/synchronizer.js @@ -7,6 +7,7 @@ define([ "fileSystem", "fileMgr", "classes/Provider", + "providers/teamserverProvider", "providers/dropboxProvider", "providers/gdriveProvider", "providers/gdrivesecProvider",