From 717eb5c3e91553e00b95a462819a7b98494cf72d Mon Sep 17 00:00:00 2001 From: benweet Date: Sat, 20 Apr 2013 18:40:05 +0100 Subject: [PATCH] New synchronize pattern --- js/blogger-provider.js | 1 - js/config.js | 4 - js/core.js | 8 +- js/dropbox-helper.js | 11 +- js/dropbox-provider.js | 111 ++++++++++++++++++- js/file-manager.js | 18 +--- js/gdrive-provider.js | 131 +++++++++++++++++++++-- js/github-provider.js | 1 - js/google-helper.js | 17 +-- js/publisher.js | 2 +- js/synchronizer.js | 236 +++++++++-------------------------------- 11 files changed, 304 insertions(+), 236 deletions(-) diff --git a/js/blogger-provider.js b/js/blogger-provider.js index bf753417..ffe7444e 100644 --- a/js/blogger-provider.js +++ b/js/blogger-provider.js @@ -4,7 +4,6 @@ define(["jquery", "google-helper"], function($, googleHelper) { var core = undefined; var bloggerProvider = { - providerType: PROVIDER_TYPE_PUBLISH_FLAG, providerId: PROVIDER_BLOGGER, providerName: "Blogger", defaultPublishFormat: "html" diff --git a/js/config.js b/js/config.js index 6e7b4ff9..9b45f613 100644 --- a/js/config.js +++ b/js/config.js @@ -12,10 +12,6 @@ var ASYNC_TASK_DEFAULT_TIMEOUT = 30000; var ASYNC_TASK_LONG_TIMEOUT = 90000; var SYNC_PERIOD = 180000; var USER_IDLE_THRESHOLD = 300000; -var SYNC_PROVIDER_GDRIVE = "sync.gdrive."; -var SYNC_PROVIDER_DROPBOX = "sync.dropbox."; -var PROVIDER_TYPE_SYNC_FLAG = 1; -var PROVIDER_TYPE_PUBLISH_FLAG = 2; var PROVIDER_BLOGGER = "blogger"; var PROVIDER_DROPBOX = "dropbox"; var PROVIDER_GDRIVE = "gdrive"; diff --git a/js/core.js b/js/core.js index e8bcfc2c..cb6c3325 100644 --- a/js/core.js +++ b/js/core.js @@ -177,7 +177,7 @@ define( // Setting management core.settings = { - converterType : "markdown-extra", + converterType : "markdown-extra-prettify", layoutOrientation : "horizontal", editorFontSize : 14, commitMsg : "Published by StackEdit.", @@ -502,6 +502,12 @@ define( // from v1 to v2 if(version == "v1") { + var gdriveLastChangeId = localStorage["sync.gdrive.lastChangeId"]; + localStorage["gdrive.lastChangeId"] = gdriveLastChangeId; + localStorage.removeItem("sync.gdrive.lastChangeId"); + var dropboxLastChangeId = localStorage["sync.dropbox.lastChangeId"]; + localStorage["dropbox.lastChangeId"] = dropboxLastChangeId; + localStorage.removeItem("sync.dropbox.lastChangeId"); var SYNC_PROVIDER_GDRIVE = "sync." + PROVIDER_GDRIVE + "."; var SYNC_PROVIDER_DROPBOX = "sync." + PROVIDER_DROPBOX + "."; diff --git a/js/dropbox-helper.js b/js/dropbox-helper.js index 8f946a23..e0e4f737 100644 --- a/js/dropbox-helper.js +++ b/js/dropbox-helper.js @@ -108,7 +108,7 @@ define(["jquery", "async-runner"], function($, asyncRunner) { asyncRunner.addTask(task); }; - dropboxHelper.checkUpdates = function(lastChangeId, callback) { + dropboxHelper.checkChanges = function(lastChangeId, callback) { callback = callback || core.doNothing; var changes = []; var newChangeId = lastChangeId || 0; @@ -125,14 +125,7 @@ define(["jquery", "async-runner"], function($, asyncRunner) { // Retrieve success newChangeId = pullChanges.cursor(); if(pullChanges.changes !== undefined) { - for(var i=0; i' + result; + }); return result; } @@ -175,8 +169,6 @@ define(["jquery", "google-helper", "dropbox-helper", "github-helper", "synchroni } $("#file-selector").append(li); }); - synchronizer.useGoogleDrive = useGoogleDrive; - synchronizer.useDropbox = useDropbox; }; // Remove a syncIndex (synchronized location) diff --git a/js/gdrive-provider.js b/js/gdrive-provider.js index cd7879b9..e2a3dd75 100644 --- a/js/gdrive-provider.js +++ b/js/gdrive-provider.js @@ -5,10 +5,10 @@ define(["jquery", "google-helper", "underscore"], function($, googleHelper) { var fileManager = undefined; var gdriveProvider = { - providerType: PROVIDER_TYPE_SYNC_FLAG | PROVIDER_TYPE_PUBLISH_FLAG, providerId: PROVIDER_GDRIVE, providerName: "Gdrive", - defaultPublishFormat: "template" + defaultPublishFormat: "template", + useSync: false }; function createSyncAttributes(id, etag, content, title) { @@ -63,7 +63,7 @@ define(["jquery", "google-helper", "underscore"], function($, googleHelper) { }; gdriveProvider.exportFile = function(event, title, content, callback) { - googleHelper.upload(undefined, undefined, title, content, function(error, result) { + googleHelper.upload(undefined, undefined, title, content, undefined, function(error, result) { if (error) { callback(error); return; @@ -87,7 +87,7 @@ define(["jquery", "google-helper", "underscore"], function($, googleHelper) { callback(true); return; } - googleHelper.upload(id, undefined, title, content, function(error, result) { + googleHelper.upload(id, undefined, title, content, undefined, function(error, result) { if (error) { callback(error); return; @@ -97,8 +97,127 @@ define(["jquery", "google-helper", "underscore"], function($, googleHelper) { }); }; + gdriveProvider.syncUp = function(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, callback) { + var syncContentCRC = syncAttributes.contentCRC; + var syncTitleCRC = syncAttributes.titleCRC; + // Skip if CRC has not changed + if(uploadContentCRC == syncContentCRC && uploadTitleCRC == syncTitleCRC) { + callback(undefined, false); + return; + } + googleHelper.upload(syncAttributes.id, undefined, uploadTitle, uploadContent, syncAttributes.etag, function(error, result) { + if(error) { + callback(error, true); + return; + } + syncAttributes.etag = result.etag; + syncAttributes.contentCRC = uploadContentCRC; + syncAttributes.titleCRC = uploadTitleCRC; + callback(undefined, true); + }); + }; + + function syncDown(callback) { + if (gdriveProvider.useSync === false) { + callback(); + return; + } + var lastChangeId = parseInt(localStorage[PROVIDER_GDRIVE + ".lastChangeId"]); + googleHelper.checkChanges(lastChangeId, function(error, changes, newChangeId) { + if (error) { + callback(error); + return; + } + var interestingChanges = []; + _.each(changes, function(change) { + var syncIndex = "sync." + PROVIDER_GDRIVE + "." + file.id; + var serializedAttributes = localStorage[syncIndex]; + if(serializedAttributes === undefined) { + return; + } + // Store syncIndex to avoid 2 times formating + change.syncIndex = syncIndex; + // Delete + if(change.deleted === true) { + interestingChanges.push(change); + return; + } + // Modify + var syncAttributes = JSON.parse(serializedAttributes); + if(syncAttributes.etag != change.file.etag) { + interestingChanges.push(change); + // Store syncAttributes to avoid 2 times parsing + change.syncAttributes = syncAttributes; + } + }); + googleHelper.downloadContent(changes, function(error, changes) { + if (error) { + callback(error); + return; + } + var updateFileTitles = false; + _.each(changes, function(change) { + var syncIndex = change.syncIndex; + var fileIndex = fileManager.getFileIndexFromSync(syncIndex); + // No file corresponding (file may have been deleted locally) + if(fileIndex === undefined) { + fileManager.removeSync(syncIndex); + return; + } + var localTitle = localStorage[fileIndex + ".title"]; + // File deleted + if (change.deleted === true) { + fileManager.removeSync(syncIndex); + updateFileTitles = true; + core.showMessage('"' + localTitle + '" has been removed from Google Drive.'); + return; + } + var syncAttributes = change.syncAttributes; + var localTitleChanged = syncAttributes.titleCRC != core.crc32(localTitle); + var localContent = localStorage[fileIndex + ".content"]; + var localContentChanged = syncAttributes.contentCRC != core.crc32(localContent); + var file = change.file; + var fileTitleChanged = localTitle != file.title; + var fileContentChanged = localContent != file.content; + // Conflict detection + if ((fileTitleChanged === true && localTitleChanged === true) + || (fileContentChanged === true && localContentChanged === true)) { + fileManager.createFile(localTitle + " (backup)", localContent); + updateFileTitles = true; + core.showMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); + } + // If file title changed + if(fileTitleChanged) { + localStorage[fileIndex + ".title"] = file.title; + updateFileTitles = true; + core.showMessage('"' + localTitle + '" has been renamed to "' + file.title + '" on Google Drive.'); + } + // If file content changed + if(fileContentChanged) { + localStorage[fileIndex + ".content"] = file.content; + core.showMessage('"' + file.title + '" has been updated from Google Drive.'); + if(fileManager.isCurrentFileIndex(fileIndex)) { + updateFileTitles = false; // Done by next function + fileManager.selectFile(); // Refresh editor + } + } + // Update syncAttributes + syncAttributes.etag = file.etag; + syncAttributes.contentCRC = core.crc32(file.content); + syncAttributes.titleCRC = core.crc32(file.title); + localStorage[syncIndex] = JSON.stringify(syncAttributes); + }); + if(updateFileTitles) { + fileManager.updateFileTitles(); + } + localStorage[PROVIDER_GDRIVE + ".lastChangeId"] = newChangeId; + callback(); + }); + }); + } + gdriveProvider.publish = function(publishAttributes, title, content, callback) { - googleHelper.upload(publishAttributes.fileId, undefined, title, content, callback); + googleHelper.upload(publishAttributes.fileId, undefined, title, content, undefined, callback); }; gdriveProvider.newPublishAttributes = function(event) { @@ -122,7 +241,7 @@ define(["jquery", "google-helper", "underscore"], function($, googleHelper) { state = JSON.parse(state); if (state.action == "create") { googleHelper.upload(undefined, state.folderId, GDRIVE_DEFAULT_FILE_TITLE, - "", function(error, file) { + "", undefined, function(error, file) { if(error) { return; } diff --git a/js/github-provider.js b/js/github-provider.js index 635cbde9..d434e7a8 100644 --- a/js/github-provider.js +++ b/js/github-provider.js @@ -4,7 +4,6 @@ define(["jquery", "github-helper"], function($, githubHelper) { var core = undefined; var githubProvider = { - providerType: PROVIDER_TYPE_PUBLISH_FLAG, providerId: PROVIDER_GITHUB, providerName: "GitHub" }; diff --git a/js/google-helper.js b/js/google-helper.js index a55e7c41..082952ac 100644 --- a/js/google-helper.js +++ b/js/google-helper.js @@ -76,7 +76,7 @@ define(["jquery", "async-runner"], function($, asyncRunner) { }); } - googleHelper.upload = function(fileId, parentId, title, content, callback) { + googleHelper.upload = function(fileId, parentId, title, content, etag, callback) { callback = callback || core.doNothing; var result = undefined; var task = asyncRunner.createTask(); @@ -95,13 +95,10 @@ define(["jquery", "async-runner"], function($, asyncRunner) { } var path = '/upload/drive/v2/files'; var method = 'POST'; - var etag = undefined; if (fileId !== undefined) { // If it's an update path += "/" + fileId; method = 'PUT'; - etag = localStorage[SYNC_PROVIDER_GDRIVE - + fileId + ".etag"]; } var headers = { 'Content-Type' : 'multipart/mixed; boundary="' + boundary + '"', }; @@ -155,7 +152,7 @@ define(["jquery", "async-runner"], function($, asyncRunner) { asyncRunner.addTask(task); }; - googleHelper.checkUpdates = function(lastChangeId, callback) { + googleHelper.checkChanges = function(lastChangeId, callback) { callback = callback || core.doNothing; var changes = []; var newChangeId = lastChangeId || 0; @@ -185,15 +182,7 @@ define(["jquery", "async-runner"], function($, asyncRunner) { newChangeId = response.largestChangeId; nextPageToken = response.nextPageToken; if (response.items !== undefined) { - for ( var i = 0; i < response.items.length; i++) { - var item = response.items[i]; - var etag = localStorage[SYNC_PROVIDER_GDRIVE - + item.fileId + ".etag"]; - if (etag - && (item.deleted === true || item.file.etag != etag)) { - changes.push(item); - } - } + changes = changes.concat(response.items); } if (nextPageToken !== undefined) { task.chain(retrievePageOfChanges); diff --git a/js/publisher.js b/js/publisher.js index fd97ef95..33ff8ebb 100644 --- a/js/publisher.js +++ b/js/publisher.js @@ -9,7 +9,7 @@ define(["jquery", "github-provider", "blogger-provider", "dropbox-provider", "gd // Create a map with providerName: providerObject var providerMap = _.chain(arguments) .map(function(argument) { - return argument && argument.providerType & PROVIDER_TYPE_PUBLISH_FLAG && [argument.providerId, argument]; + return argument && argument.providerId && [argument.providerId, argument]; }).compact().object().value(); // Used to know if the current file has publications diff --git a/js/synchronizer.js b/js/synchronizer.js index 46b62d0f..bcee91eb 100644 --- a/js/synchronizer.js +++ b/js/synchronizer.js @@ -8,7 +8,7 @@ define(["jquery", "google-helper", "dropbox-helper", "dropbox-provider", "gdrive // Create a map with providerName: providerObject var providerMap = _.chain(arguments) .map(function(argument) { - return argument && argument.providerType & PROVIDER_TYPE_SYNC_FLAG && [argument.providerId, argument]; + return argument && argument.providerId && [argument.providerId, argument]; }).compact().object().value(); // Used to know the providers we are connected to @@ -59,51 +59,32 @@ define(["jquery", "google-helper", "dropbox-helper", "dropbox-provider", "gdrive // Dequeue a synchronized location var syncIndex = uploadFileSyncIndexList.pop(); - var syncAttributes = JSON.parse(localStorage[fileSyncIndex]); - = - var syncContentCRC = localStorage[fileSyncIndex + ".contentCRC"]; - var syncTitleCRC = localStorage[fileSyncIndex + ".titleCRC"]; - // Skip if CRC has not changed - if(uploadContentCRC == syncContentCRC && (syncTitleCRC === undefined || uploadTitleCRC == syncTitleCRC)) { - locationUp(callback); - return; - } - - // If upload is going to run, go for an other upload cycle at the end - uploadCycle = true; - // When page is refreshed, this flag is false but should be true here - uploadPending = true; - - // Try to find the provider - if (fileSyncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) { - var id = fileSyncIndex.substring(SYNC_PROVIDER_GDRIVE.length); - googleHelper.upload(id, undefined, uploadTitle, uploadContent, function(error, result) { + var syncAttributes = JSON.parse(localStorage[syncIndex]); + // Use the specified provider to perform the upload + providerMap[syncAttributes.provider].syncUp( + uploadContent, + uploadContentCRC, + uploadTitle, + uploadTitleCRC, + syncAttributes, + function(error, uploadFlag) { + if(uploadFlag === true) { + // If uploadFlag is true, request another upload cycle + uploadCycle = true; + // When page is refreshed, this flag is false but should be true + uploadPending = true; + } if(error) { - // If error we abort the synchronization (retry later) callback(error); return; } - localStorage[fileSyncIndex + ".contentCRC"] = uploadContentCRC; - localStorage[fileSyncIndex + ".titleCRC"] = uploadTitleCRC; - locationUp(callback); - }); - } else if (fileSyncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) { - var path = fileSyncIndex.substring(SYNC_PROVIDER_DROPBOX.length); - path = decodeURIComponent(path); - dropboxHelper.upload(path, uploadContent, function(error, result) { - if (error) { - // If error we abort the synchronization (retry later) - callback(error); - return; + if(uploadFlag) { + // Update syncAttributes in localStorage + localStorage[syncIndex] = JSON.stringify(syncAttributes); } - localStorage[fileSyncIndex + ".contentCRC"] = uploadContentCRC; locationUp(callback); - }); - } else { - // This should never happen - console.error("Invalid fileSyncIndex: " + fileSyncIndex); - callback("error"); - } + } + ); } // Recursive function to upload multiple files @@ -119,7 +100,7 @@ define(["jquery", "google-helper", "dropbox-helper", "dropbox-provider", "gdrive // Dequeue a fileIndex var fileIndex = uploadFileIndexList.pop(); var fileSyncIndexes = localStorage[fileIndex + ".sync"]; - if(!fileIndex || fileSyncIndexes.length === 1) { + if(fileSyncIndexes.length === 1) { fileUp(callback); return; } @@ -141,7 +122,7 @@ define(["jquery", "google-helper", "dropbox-helper", "dropbox-provider", "gdrive if(uploadCycle === true) { // New upload cycle uploadCycle = false; - uploadFileIndexList = localStorage["file.list"].split(";"); + uploadFileIndexList = _.compact(localStorage["file.list"].split(";")); fileUp(callback); } else { @@ -149,157 +130,25 @@ define(["jquery", "google-helper", "dropbox-helper", "dropbox-provider", "gdrive } } - // Used to download file changes from Google Drive - function syncDownGdrive(callback) { - if (synchronizer.useGoogleDrive === false) { + // Recursive function to download changes from multiple providers + var providerList = []; + function providerDown(callback) { + if(providerList.length === 0) { callback(); - return; } - var lastChangeId = parseInt(localStorage[SYNC_PROVIDER_GDRIVE - + "lastChangeId"]); - googleHelper.checkUpdates(lastChangeId, function(error, changes, newChangeId) { - if (error) { + var provider = providerList.pop(); + provider.syncDown(function(error) { + if(error) { callback(error); return; } - googleHelper.downloadContent(changes, function(error, changes) { - if (error) { - callback(error); - return; - } - var updateFileTitles = false; - for ( var i = 0; i < changes.length; i++) { - var change = changes[i]; - var fileSyncIndex = SYNC_PROVIDER_GDRIVE + change.fileId; - var fileIndex = fileManager.getFileIndexFromSync(fileSyncIndex); - // No file corresponding (file may have been deleted locally) - if(fileIndex === undefined) { - fileManager.removeSync(fileSyncIndex); - continue; - } - var localTitle = localStorage[fileIndex + ".title"]; - // File deleted - if (change.deleted === true) { - fileManager.removeSync(fileSyncIndex); - updateFileTitles = true; - core.showMessage('"' + localTitle + '" has been removed from Google Drive.'); - continue; - } - var localTitleChanged = localStorage[fileSyncIndex + ".titleCRC"] != core.crc32(localTitle); - var localContent = localStorage[fileIndex + ".content"]; - var localContentChanged = localStorage[fileSyncIndex + ".contentCRC"] != core.crc32(localContent); - var file = change.file; - var fileTitleChanged = localTitle != file.title; - var fileContentChanged = localContent != file.content; - // Conflict detection - if ((fileTitleChanged === true && localTitleChanged === true) - || (fileContentChanged === true && localContentChanged === true)) { - fileManager.createFile(localTitle + " (backup)", localContent); - updateFileTitles = true; - core.showMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); - } - // If file title changed - if(fileTitleChanged) { - localStorage[fileIndex + ".title"] = file.title; - updateFileTitles = true; - core.showMessage('"' + localTitle + '" has been renamed to "' + file.title + '" on Google Drive.'); - } - // If file content changed - if(fileContentChanged) { - localStorage[fileIndex + ".content"] = file.content; - core.showMessage('"' + file.title + '" has been updated from Google Drive.'); - if(fileManager.isCurrentFileIndex(fileIndex)) { - updateFileTitles = false; // Done by next function - fileManager.selectFile(); // Refresh editor - } - } - // Update file etag and CRCs - localStorage[fileSyncIndex + ".etag"] = file.etag; - localStorage[fileSyncIndex + ".contentCRC"] = core.crc32(file.content); - localStorage[fileSyncIndex + ".titleCRC"] = core.crc32(file.title); - } - if(updateFileTitles) { - fileManager.updateFileTitles(); - } - localStorage[SYNC_PROVIDER_GDRIVE - + "lastChangeId"] = newChangeId; - callback(); - }); - }); - } - - // Used to download file changes from Dropbox - function syncDownDropbox(callback) { - if (synchronizer.useDropbox === false) { - callback(); - return; - } - var lastChangeId = localStorage[SYNC_PROVIDER_DROPBOX + "lastChangeId"]; - dropboxHelper.checkUpdates(lastChangeId, function(error, changes, newChangeId) { - if (error) { - callback(error); - return; - } - dropboxHelper.downloadContent(changes, function(error, changes) { - if (error) { - callback(error); - return; - } - var updateFileTitles = false; - for ( var i = 0; i < changes.length; i++) { - var change = changes[i]; - var fileSyncIndex = SYNC_PROVIDER_DROPBOX + encodeURIComponent(change.path.toLowerCase()); - var fileIndex = fileManager.getFileIndexFromSync(fileSyncIndex); - // No file corresponding (file may have been deleted locally) - if(fileIndex === undefined) { - fileManager.removeSync(fileSyncIndex); - continue; - } - var localTitle = localStorage[fileIndex + ".title"]; - // File deleted - if (change.wasRemoved === true) { - fileManager.removeSync(fileSyncIndex); - updateFileTitles = true; - core.showMessage('"' + localTitle + '" has been removed from Dropbox.'); - continue; - } - var localContent = localStorage[fileIndex + ".content"]; - var localContentChanged = localStorage[fileSyncIndex + ".contentCRC"] != core.crc32(localContent); - var file = change.stat; - var fileContentChanged = localContent != file.content; - // Conflict detection - if (fileContentChanged === true && localContentChanged === true) { - fileManager.createFile(localTitle + " (backup)", localContent); - updateFileTitles = true; - core.showMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); - } - // If file content changed - if(fileContentChanged) { - localStorage[fileIndex + ".content"] = file.content; - core.showMessage('"' + localTitle + '" has been updated from Dropbox.'); - if(fileManager.isCurrentFileIndex(fileIndex)) { - updateFileTitles = false; // Done by next function - fileManager.selectFile(); // Refresh editor - } - } - // Update file version and CRC - localStorage[fileSyncIndex + ".version"] = file.versionTag; - localStorage[fileSyncIndex + ".contentCRC"] = core.crc32(file.content); - } - if(updateFileTitles) { - fileManager.updateFileTitles(); - } - localStorage[SYNC_PROVIDER_DROPBOX - + "lastChangeId"] = newChangeId; - callback(); - }); + providerDown(callback); }); } function syncDown(callback) { - syncDownGdrive(function() { - syncDownDropbox(callback); - }); + providerList = _.values(providerMap); + providerDown(callback); }; var syncRunning = false; @@ -316,6 +165,7 @@ define(["jquery", "google-helper", "dropbox-helper", "dropbox-provider", "gdrive function isError(error) { if(error !== undefined) { + console.error(error); syncRunning = false; synchronizer.updateSyncButton(); return true; @@ -369,6 +219,24 @@ define(["jquery", "google-helper", "dropbox-helper", "dropbox-provider", "gdrive }); }; + synchronizer.resetSyncFlags = function() { + _.each(providerMap, function(provider) { + provider.useSync = false; + }); + }; + + synchronizer.getSyncProvidersFromFile = function(fileIndex) { + var sync = localStorage[fileIndex + ".sync"]; + var providerIdList = []; + _.each(providerMap, function(provider) { + if (sync.indexOf(";sync." + provider.providerId + ".") !== -1) { + provider.useSync = true; + providerIdList.push(provider.providerId); + } + }); + return providerIdList; + }; + synchronizer.init = function(coreModule, fileManagerModule) { core = coreModule; fileManager = fileManagerModule;