Added Dropbox synchronization

This commit is contained in:
Benoit Schweblin 2013-04-07 16:22:13 +01:00
parent 2b04af3610
commit 6983b74acd
13 changed files with 754 additions and 115 deletions

View File

@ -1 +1 @@
CACHE MANIFEST # v32 CACHE: index.html css/bootstrap.css css/jgrowl.css css/main.css js/async-runner.js js/bootstrap.js js/config.js js/custo.github.js js/gdrive.js js/jgrowl.js js/jquery.js js/jquery-ui.js js/layout.js js/main.js js/Markdown.Converter.js js/Markdown.Editor.js js/Markdown.Sanitizer.js js/require.js js/synchronizer.js img/ajax-loader.gif img/dropbox.png img/gdrive.png img/glyphicons-halflings.png img/glyphicons-halflings-white.png img/stackedit-16.png img/stackedit-32.ico NETWORK: * CACHE MANIFEST # v32 CACHE: index.html css/bootstrap.css css/jgrowl.css css/main.css js/async-runner.js js/bootstrap.js js/config.js js/custo.github.js js/dropbox.js js/gdrive.js js/jgrowl.js js/jquery.js js/jquery-ui.js js/layout.js js/main.js js/Markdown.Converter.js js/Markdown.Editor.js js/Markdown.Sanitizer.js js/require.js js/synchronizer.js img/ajax-loader.gif img/dropbox.png img/gdrive.png img/glyphicons-halflings.png img/glyphicons-halflings-white.png img/stackedit-16.png img/stackedit-32.ico NETWORK: *

View File

@ -9,7 +9,7 @@ body {
cursor: progress; cursor: progress;
} }
.btn { .btn, .dropdown-menu {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
@ -35,6 +35,7 @@ div, span, a, ul, li, textarea, input, button {
.dropdown-menu { .dropdown-menu {
border: 1px solid #e2e2e2 !important; border: 1px solid #e2e2e2 !important;
text-align: left;
} }
.input-prepend input, .input-prepend input,
@ -84,7 +85,6 @@ input.error {
} }
input[disabled], select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly], .input-append .add-on { input[disabled], select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly], .input-append .add-on {
cursor: not-allowed;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
@ -93,7 +93,9 @@ input[disabled], select[disabled], textarea[disabled], input[readonly], select[r
.btn-primary:active, .btn-primary:active,
.btn-primary.active, .btn-primary.active,
.btn-primary.disabled, .btn-primary.disabled,
.btn-primary[disabled] { .btn-primary[disabled],
.btn-group.open .btn.btn-primary.dropdown-toggle {
color: #fff;
background-color: #888; background-color: #888;
} }

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="js/dropbox.min.js"></script>
<script type="text/javascript">
Dropbox.Drivers.Popup.oauthReceiver();
</script>
</head>
<body>
</body>
</html>

View File

@ -5,6 +5,9 @@
<link rel="icon" href="img/stackedit-32.ico" type="image/x-icon"> <link rel="icon" href="img/stackedit-32.ico" type="image/x-icon">
<link rel="shortcut icon" href="img/stackedit-32.ico" <link rel="shortcut icon" href="img/stackedit-32.ico"
type="image/x-icon"> type="image/x-icon">
<meta name="keywords" content="Markdown, Editor, PageDown, Stack Overflow, Stack Exchange">
<meta name="description" content="StackEdit is a free, open-source Markdown editor based on PageDown, the Markdown library used by Stack Overflow and the other Stack Exchange sites.">
<meta name="author" content="Benoit Schweblin">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="css/main.css" rel="stylesheet" media="screen"> <link href="css/main.css" rel="stylesheet" media="screen">
<script data-main="js/main" src="js/require.js"></script> <script data-main="js/main" src="js/require.js"></script>
@ -47,18 +50,18 @@
<li class="dropdown-submenu"><a href="#"><i <li class="dropdown-submenu"><a href="#"><i
class="icon-gdrive"></i> Google Drive</a> class="icon-gdrive"></i> Google Drive</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#" class="action-upload-gdrive">Export to <li><a href="#" class="action-download-gdrive">Import
Google Drive</a></li> from Google Drive</a></li>
<li><a href="#" class="action-download-gdrive">Import from Google <li><a href="#" data-toggle="modal"
Drive</a></li> data-target="#modal-upload-gdrive">Export to Google Drive</a></li>
</ul></li> </ul></li>
<li class="dropdown-submenu"><a href="#"><i <li class="dropdown-submenu"><a href="#"><i
class="icon-dropbox"></i> Dropbox</a> class="icon-dropbox"></i> Dropbox</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="action-upload-dropbox" href="#">Export to
Dropbox</a></li>
<li><a class="action-download-dropbox" href="#">Import <li><a class="action-download-dropbox" href="#">Import
from Dropbox</a></li> from Dropbox</a></li>
<li><a href="#" data-toggle="modal"
data-target="#modal-upload-dropbox">Export to Dropbox</a></li>
</ul></li> </ul></li>
<li><a href="#" data-toggle="modal" <li><a href="#" data-toggle="modal"
data-target="#modal-manage-sync"><i class="icon-refresh"></i> data-target="#modal-manage-sync"><i class="icon-refresh"></i>
@ -87,39 +90,43 @@
<div id="modal-insert-link" class="modal hide"> <div id="modal-insert-link" class="modal hide">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close action-close-insert-link" data-dismiss="modal" <button type="button" class="close action-close-insert-link"
aria-hidden="true">&times;</button> data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Hyperlink</h3> <h3>Hyperlink</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Please provide the link URL and an optional title:</p> <p>Please provide the link URL and an optional title:</p>
<div class="input-prepend"> <div class="input-prepend">
<span class="add-on"><i class="icon-globe"></i></span><input <span class="add-on"><i class="icon-globe"></i></span><input
id="input-insert-link" type="text" class="span5" placeholder='http://example.com/ "optional title"'></input> id="input-insert-link" type="text" class="span5"
placeholder='http://example.com/ "optional title"'></input>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="#" class="btn action-close-insert-link" data-dismiss="modal">Cancel</a> <a href="#" <a href="#" class="btn action-close-insert-link" data-dismiss="modal">Cancel</a>
class="btn btn-primary action-insert-link" data-dismiss="modal">OK</a> <a href="#" class="btn btn-primary action-insert-link"
data-dismiss="modal">OK</a>
</div> </div>
</div> </div>
<div id="modal-insert-image" class="modal hide"> <div id="modal-insert-image" class="modal hide">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close action-close-insert-link" data-dismiss="modal" <button type="button" class="close action-close-insert-link"
aria-hidden="true">&times;</button> data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Image</h3> <h3>Image</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Please provide the image URL and an optional title:</p> <p>Please provide the image URL and an optional title:</p>
<div class="input-prepend"> <div class="input-prepend">
<span class="add-on"><i class="icon-picture"></i></span><input <span class="add-on"><i class="icon-picture"></i></span><input
id="input-insert-image" type="text" class="span5" placeholder='http://example.com/image.jpg "optional title"'></input> id="input-insert-image" type="text" class="span5"
placeholder='http://example.com/image.jpg "optional title"'></input>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="#" class="btn action-close-insert-link" data-dismiss="modal">Cancel</a> <a href="#" <a href="#" class="btn action-close-insert-link" data-dismiss="modal">Cancel</a>
class="btn btn-primary action-insert-image" data-dismiss="modal">OK</a> <a href="#" class="btn btn-primary action-insert-image"
data-dismiss="modal">OK</a>
</div> </div>
</div> </div>
@ -141,6 +148,55 @@
</div> </div>
</div> </div>
<div id="modal-upload-gdrive" class="modal hide">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">&times;</button>
<h3>Export</h3>
</div>
<div class="modal-body">
<p>This will upload the current document into your Google Drive
root folder and keep it synchronized.</p>
<p class="muted"><b>NOTE:</b> You can move or rename the file
within Google Drive afterwards.</p>
</div>
<div class="modal-footer">
<a href="#" class="btn" data-dismiss="modal">Cancel</a> <a href="#"
data-dismiss="modal"
class="btn btn-primary action-upload-gdrive-root">OK</a>
</div>
</div>
<div id="modal-upload-dropbox" class="modal hide">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">&times;</button>
<h3>Export</h3>
</div>
<div class="modal-body">
<p>This will upload the current document to your Dropbox account
and keep it synchronized.</p>
<p>Please specify a file path for "<span class="file-title"></span>":
</p>
<div class="input-prepend">
<span class="add-on"><i class="icon-dropbox"></i></span><input
id="upload-dropbox-path" type="text" class="span5"
placeholder="/path/to/My Document.md"></input>
</div>
<br /> <br /> <b class="muted">NOTE:</b>
<ul class="muted">
<li>Dropbox file path does not depend on document title.</li>
<li>The title of your document will not be synchronized.</li>
<li>Destination folder must exist.</li>
<li>Any existing file at this location will be overwritten.</li>
</ul>
</div>
<div class="modal-footer">
<a href="#" class="btn" data-dismiss="modal">Cancel</a> <a href="#"
data-dismiss="modal" class="btn btn-primary action-upload-dropbox">OK</a>
</div>
</div>
<div id="modal-manage-sync" class="modal hide"> <div id="modal-manage-sync" class="modal hide">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" <button type="button" class="close" data-dismiss="modal"
@ -148,11 +204,10 @@
<h3>Synchronization</h3> <h3>Synchronization</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="manage-sync-list">
<p class="msg-sync-list hide">"<span class="file-title"></span>" <p class="msg-sync-list hide">"<span class="file-title"></span>"
is synchronized with these locations: is synchronized with these locations:
</p> </p>
</div> <div id="manage-sync-list"></div>
<p class="msg-sync-list hide muted"><b>NOTE:</b> Removing a <p class="msg-sync-list hide muted"><b>NOTE:</b> Removing a
synchronized location will not delete any file.</p> synchronized location will not delete any file.</p>
<p class="msg-no-sync hide">"<span class="file-title"></span>" is <p class="msg-no-sync hide">"<span class="file-title"></span>" is
@ -161,12 +216,21 @@
<p>Add a synchronized location manually:</p> <p>Add a synchronized location manually:</p>
<div class="input-prepend input-append"> <div class="input-prepend input-append">
<span class="add-on"><i class="icon-gdrive"></i></span><input <span class="add-on"><i class="icon-gdrive"></i></span><input
id="manual-gdrive-fileid" type="text" class="span5" placeholder="Google Drive file ID"></input> id="manual-gdrive-fileid" type="text" class="span5"
<a class="btn action-manual-gdrive" data-dismiss="modal"><i class="icon-ok"></i></a> placeholder="Google Drive file ID"></input> <a
class="btn action-manual-gdrive" title="Add this location"
data-dismiss="modal"><i class="icon-ok"></i></a>
</div>
<div class="input-prepend input-append">
<span class="add-on"><i class="icon-dropbox"></i></span><input
id="manual-dropbox-path" type="text" class="span5"
placeholder="Dropbox file path"></input> <a
class="btn action-manual-dropbox" title="Add this location"
data-dismiss="modal"><i class="icon-ok"></i></a>
</div> </div>
<p class="muted"><b>NOTE:</b> Adding a synchronized location will <p class="muted"><b>NOTE:</b> Adding a synchronized location will
first upload the local document and overwrite the existing file on the upload the local document firstly and overwrite the existing file on
server.</p> the server.</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="#" class="btn btn-primary" data-dismiss="modal">Close</a> <a href="#" class="btn btn-primary" data-dismiss="modal">Close</a>
@ -181,7 +245,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>Layout orientation</dt> <dt>Layout orientation:</dt>
<dd> <dd>
<label class="radio"> <input type="radio" <label class="radio"> <input type="radio"
name="radio-layout-orientation" value="horizontal"> name="radio-layout-orientation" value="horizontal">
@ -252,8 +316,8 @@
<a target="_blank" href="http://jquery.com/">jQuery</a> <a target="_blank" href="http://jquery.com/">jQuery</a>
</dd> </dd>
<dd> <dd>
<a target="_blank" href="http://layout.jquery-dev.net/">jQuery UI <a target="_blank" href="http://layout.jquery-dev.net/">jQuery
Layout</a> UI Layout</a>
</dd> </dd>
<dd> <dd>
<a target="_blank" href="https://code.google.com/p/pagedown/">PageDown</a> <a target="_blank" href="https://code.google.com/p/pagedown/">PageDown</a>
@ -272,5 +336,6 @@
</div> </div>
</div> </div>
<div id="dropboxjs" data-app-key="x0k2l8puemfvg0o"></div>
</body> </body>
</html> </html>

View File

@ -84,7 +84,7 @@ define(["core"], function(core) {
}; };
function runSafe(func) { function runSafe(func) {
if(currentTask.finished === true) { if(currentTask === undefined || currentTask.finished === true) {
return; return;
} }
try { try {
@ -107,6 +107,7 @@ define(["core"], function(core) {
// Add a task in the queue // Add a task in the queue
asyncTaskRunner.addTask = function(asyncTask) { asyncTaskRunner.addTask = function(asyncTask) {
asyncTaskQueue.push(asyncTask); asyncTaskQueue.push(asyncTask);
asyncTaskRunner.runTask();
}; };
return asyncTaskRunner; return asyncTaskRunner;

View File

@ -1,14 +1,17 @@
var GOOGLE_SCOPES = [ 'https://www.googleapis.com/auth/drive.install', var GOOGLE_SCOPES = [ 'https://www.googleapis.com/auth/drive.install',
'https://www.googleapis.com/auth/drive' ]; 'https://www.googleapis.com/auth/drive' ];
var GOOGLE_DRIVE_APP_ID = "241271498917"; var GOOGLE_DRIVE_APP_ID = "241271498917";
var DROPBOX_APP_KEY = "lq6mwopab8wskas";
var DROPBOX_APP_SECRET = "851fgnucpezy84t";
var DEFAULT_FILE_TITLE = "Title"; var DEFAULT_FILE_TITLE = "Title";
var GDRIVE_DEFAULT_FILE_TITLE = "New Markdown document"; var GDRIVE_DEFAULT_FILE_TITLE = "New Markdown document";
var CHECK_ONLINE_PERIOD = 60000; var CHECK_ONLINE_PERIOD = 60000;
var AJAX_TIMEOUT = 10000; var AJAX_TIMEOUT = 10000;
var ASYNC_TASK_DEFAULT_TIMEOUT = 30000; var ASYNC_TASK_DEFAULT_TIMEOUT = 30000;
var AUTH_POPUP_TIMEOUT = 90000; var AUTH_POPUP_TIMEOUT = 90000;
var SYNC_PERIOD = 60000; var SYNC_PERIOD = 180000;
var SYNC_PROVIDER_GDRIVE = "sync.gdrive."; var SYNC_PROVIDER_GDRIVE = "sync.gdrive.";
var SYNC_PROVIDER_DROPBOX = "sync.dropbox.";
// Use by Google's client.js // Use by Google's client.js
var delayedFunction = undefined; var delayedFunction = undefined;

View File

@ -45,7 +45,7 @@ define(["jquery", "bootstrap", "jgrowl", "layout", "Markdown.Editor"], function(
core.showMessage = function(msg, iconClass, options) { core.showMessage = function(msg, iconClass, options) {
options = options || {}; options = options || {};
iconClass = iconClass || "icon-info-sign"; iconClass = iconClass || "icon-info-sign";
$.jGrowl("<i class='icon-white " + iconClass + "'></i> " + msg, options); $.jGrowl("<i class='icon-white " + iconClass + "'></i> " + $("<div>").text(msg).html(), options);
}; };
// Used to show an error message // Used to show an error message

389
js/dropbox.js Normal file
View File

@ -0,0 +1,389 @@
define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
// Dependencies
var fileManager = undefined;
var client = undefined;
var authenticated = false;
var dropbox = {};
// Try to connect dropbox by downloading client.js
function connect(callback) {
callback = callback || core.doNothing;
var asyncTask = {};
asyncTask.run = function() {
if(core.isOffline === true) {
client = undefined;
core.showMessage("Operation not available in offline mode.");
asyncTask.error();
return;
}
if (client !== undefined) {
asyncTask.success();
return;
}
$.ajax({
url : "js/dropbox.min.js",
dataType : "script", timeout : AJAX_TIMEOUT
}).done(function() {
asyncTask.success();
}).fail(function() {
asyncTask.error();
});
};
asyncTask.onSuccess = function() {
client = new Dropbox.Client({
key: DROPBOX_APP_KEY,
secret: DROPBOX_APP_SECRET
});
client.authDriver(new Dropbox.Drivers.Popup({
receiverUrl: "http://localhost/dropbox-oauth-receiver.html",
rememberUser: true
}));
callback();
};
asyncTask.onError = function() {
core.setOffline();
callback();
};
asyncTaskRunner.addTask(asyncTask);
}
// Try to authenticate with Oauth
function authenticate(callback, immediate) {
callback = callback || core.doNothing;
if (immediate === undefined) {
immediate = true;
}
connect(function() {
if (client === undefined) {
callback();
return;
}
var asyncTask = {};
asyncTask.run = function() {
if (authenticated === true) {
asyncTask.success();
return;
}
if (immediate === false) {
core.showMessage("Please make sure the Dropbox authorization popup is not blocked by your browser.");
}
client.authenticate({interactive: !immediate}, function(error, client) {
if (client.authState !== Dropbox.Client.DONE) {
// Handle error
asyncTask.error();
return;
}
asyncTask.success();
});
};
asyncTask.onSuccess = function() {
callback();
};
asyncTask.onError = function() {
// If immediate did not work retry without immediate flag
if (client !== undefined && immediate === true) {
authenticate(callback, false);
return;
}
callback();
};
asyncTaskRunner.addTask(asyncTask);
});
}
dropbox.upload = function(path, content, callback) {
callback = callback || core.doNothing;
authenticate(function() {
if (client === undefined) {
callback();
return;
}
var fileSyncIndex = undefined;
var asyncTask = {};
asyncTask.run = function() {
client.writeFile(path, content, function(error, stat) {
if (!error) {
fileSyncIndex = SYNC_PROVIDER_DROPBOX + encodeURIComponent(stat.path.toLowerCase());
localStorage[fileSyncIndex + ".version"] = stat.versionTag;
asyncTask.success();
return;
}
// Handle error
if(error.status === Dropbox.ApiError.INVALID_PARAM) {
error = 'Could not upload document into path "' + path + '".';
}
handleError(error, asyncTask, callback);
});
};
asyncTask.onSuccess = function() {
callback(fileSyncIndex);
};
asyncTaskRunner.addTask(asyncTask);
});
};
dropbox.checkUpdates = function(lastChangeId, callback) {
callback = callback || core.doNothing;
authenticate(function() {
if (client === undefined) {
callback();
return;
}
var changes = [];
var newChangeId = lastChangeId || 0;
function retrievePageOfChanges(changeId) {
var shouldPullAgain = false;
var asyncTask = {};
asyncTask.run = function() {
client.pullChanges(changeId, function(error, pullChanges) {
if (pullChanges && pullChanges.cursorTag) {
// Retrieve success
newChangeId = pullChanges.cursor();
shouldPullAgain = pullChanges.shouldPullAgain;
if(pullChanges.changes !== undefined) {
for(var i=0; i<pullChanges.changes.length; i++) {
var item = pullChanges.changes[i];
var version = localStorage[SYNC_PROVIDER_DROPBOX
+ encodeURIComponent(item.path.toLowerCase()) + ".version"];
if(version && (item.wasRemoved || item.stat.versionTag != version)) {
changes.push(item);
}
}
}
asyncTask.success();
return;
}
// Handle error
handleError(error, asyncTask, callback);
});
};
asyncTask.onSuccess = function() {
if (shouldPullAgain === true) {
retrievePageOfChanges(newChangeId);
} else {
callback(changes, newChangeId);
}
};
asyncTaskRunner.addTask(asyncTask);
}
retrievePageOfChanges(newChangeId);
});
};
dropbox.downloadMetadata = function(paths, callback, result) {
callback = callback || core.doNothing;
result = result || [];
if(paths.length === 0) {
callback(result);
return;
}
authenticate(function() {
if (client === undefined) {
callback();
return;
}
var path = paths.pop();
var asyncTask = {};
asyncTask.run = function() {
client.stat(path, function(error, stat) {
if(stat) {
result.push(stat);
asyncTask.success();
return;
}
handleError(error, asyncTask, callback);
});
};
asyncTask.onSuccess = function() {
dropbox.downloadMetadata(paths, callback, result);
};
asyncTaskRunner.addTask(asyncTask);
});
};
dropbox.downloadContent = function(objects, callback, result) {
callback = callback || core.doNothing;
result = result || [];
if(objects.length === 0) {
callback(result);
return;
}
var object = objects.pop();
result.push(object);
var file = undefined;
// object may be a file
if(object.isFile === true) {
file = object;
}
// object may be a change
else if(object.wasRemoved !== undefined) {
file = object.stat;
}
if(!file) {
this.downloadContent(objects, callback, result);
return;
}
authenticate(function() {
if (client === undefined) {
callback();
return;
}
var asyncTask = {};
asyncTask.run = function() {
client.readFile(file.path, function(error, data) {
if(data) {
file.content = data;
asyncTask.success();
return;
}
handleError(error, asyncTask, callback);
});
};
asyncTask.onSuccess = function() {
dropbox.downloadContent(objects, callback, result);
};
asyncTaskRunner.addTask(asyncTask);
});
};
function handleError(error, asyncTask, callback) {
var errorMsg = undefined;
asyncTask.onError = function() {
if (errorMsg !== undefined) {
core.showError(errorMsg);
}
callback();
};
if (error) {
// Try to analyze the error
if (typeof error === "string") {
errorMsg = error;
} else if (error.status === Dropbox.ApiError.INVALID_TOKEN
|| error.status === Dropbox.ApiError.OAUTH_ERROR) {
authenticated = false;
errorMsg = "Access to Dropbox is not authorized.";
} else if (error.status === Dropbox.ApiError.NETWORK_ERROR) {
client = undefined;
authenticated = false;
core.setOffline();
} else {
errorMsg = "Dropbox error ("
+ error.status + ").";
}
}
asyncTask.error();
}
var pickerLoaded = false;
function loadPicker(callback) {
connect(function() {
if (client === undefined) {
pickerLoaded = false;
callback();
return;
}
var asyncTask = {};
asyncTask.run = function() {
if (pickerLoaded === true) {
asyncTask.success();
return;
}
$.ajax({
url : "https://www.dropbox.com/static/api/1/dropbox.js",
dataType : "script", timeout : AJAX_TIMEOUT
}).done(function() {
asyncTask.success();
}).fail(function() {
asyncTask.error();
});
};
asyncTask.onSuccess = function() {
pickerLoaded = true;
callback();
};
asyncTask.onError = function() {
core.setOffline();
callback();
};
asyncTaskRunner.addTask(asyncTask);
});
}
dropbox.picker = function(callback) {
callback = callback || core.doNothing;
loadPicker(function() {
if (pickerLoaded === false) {
callback();
return;
}
var options = {};
options.multiselect = true;
options.linkType = "direct";
options.success = function(files) {
var paths = [];
for(var i=0; i<files.length; i++) {
var path = files[i].link;
path = path.replace(/.*\/view\/[^\/]*/, "");
paths.push(decodeURI(path));
}
callback(paths);
};
options.cancel = function() {
callback();
};
Dropbox.choose(options);
core.showMessage("Please make sure the Dropbox chooser popup is not blocked by your browser.");
});
};
dropbox.importFiles = function(paths) {
dropbox.downloadMetadata(paths, function(result) {
if(result === undefined) {
return;
}
dropbox.downloadContent(result, function(result) {
if(result === undefined) {
return;
}
for(var i=0; i<result.length; i++) {
var file = result[i];
fileSyncIndex = SYNC_PROVIDER_DROPBOX + encodeURIComponent(file.path.toLowerCase());
localStorage[fileSyncIndex + ".version"] = file.versionTag;
var fileIndex = fileManager.createFile(file.name, file.content, [fileSyncIndex]);
fileManager.selectFile(fileIndex);
core.showMessage('"' + file.name + '" imported successfully from Dropbox.');
}
});
});
};
dropbox.init = function(fileManagerModule) {
fileManager = fileManagerModule;
};
dropbox.checkPath = function(path) {
if(!path.match(/^[^\\<>:"\|?\*]+$/)) {
core.showError('"' + path + '" contains invalid characters.');
return undefined;
}
if(path.indexOf("/") !== 0) {
return "/" + path;
}
return path;
};
return dropbox;
});

5
js/dropbox.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,11 @@
define(["jquery", "core", "gdrive", "synchronizer", "async-runner"], function($, core, gdrive, synchronizer, asyncTaskRunner) { define(["jquery", "core", "gdrive", "dropbox", "synchronizer", "async-runner"],
function($, core, gdrive, dropbox, synchronizer, asyncTaskRunner) {
var fileManager = {}; var fileManager = {};
fileManager.init = function() { fileManager.init = function() {
gdrive.init(fileManager); gdrive.init(fileManager);
dropbox.init(fileManager);
var changeSyncButtonState = function() { var changeSyncButtonState = function() {
if(synchronizer.isRunning() || synchronizer.isQueueEmpty() || core.isOffline) { if(synchronizer.isRunning() || synchronizer.isQueueEmpty() || core.isOffline) {
@ -75,7 +77,18 @@ define(["jquery", "core", "gdrive", "synchronizer", "async-runner"], function($,
+ core.encodeBase64(content); + core.encodeBase64(content);
window.open(uriContent, 'file'); window.open(uriContent, 'file');
}); });
$(".action-upload-gdrive").click(uploadGdrive); $(".action-upload-gdrive-root").click(function() {
uploadGdrive();
});
$(".action-upload-gdrive-select").click(function() {
// This action is not available because picker does not support
// folder selection yet
gdrive.picker(function(ids) {
if(ids !== undefined && ids.length !== 0) {
uploadGdrive(ids[0]);
}
}, true);
});
$(".action-download-gdrive").click(function() { $(".action-download-gdrive").click(function() {
gdrive.picker(importGdrive); gdrive.picker(importGdrive);
}); });
@ -84,10 +97,15 @@ define(["jquery", "core", "gdrive", "synchronizer", "async-runner"], function($,
manualGdrive(fileId); manualGdrive(fileId);
}); });
$(".action-download-dropbox").click(function() { $(".action-download-dropbox").click(function() {
core.showMessage("Sorry, Dropbox synchronization is not yet available."); dropbox.picker(importDropbox);
}); });
$(".action-upload-dropbox").click(function() { $(".action-upload-dropbox").click(function(event) {
core.showMessage("Sorry, Dropbox synchronization is not yet available."); var path = core.getInputValue($("#upload-dropbox-path"), event);
manualDropbox(path);
});
$(".action-manual-dropbox").click(function(event) {
var path = core.getInputValue($("#manual-dropbox-path"), event);
manualDropbox(path);
}); });
}; };
@ -209,9 +227,14 @@ define(["jquery", "core", "gdrive", "synchronizer", "async-runner"], function($,
} }
var useGoogleDrive = false; var useGoogleDrive = false;
var useDropbox = false;
function composeTitle(fileIndex) { function composeTitle(fileIndex) {
var result = localStorage[fileIndex + ".title"]; var result = " " + localStorage[fileIndex + ".title"];
var sync = localStorage[fileIndex + ".sync"]; var sync = localStorage[fileIndex + ".sync"];
if (sync.indexOf(";" + SYNC_PROVIDER_DROPBOX) !== -1) {
useDropbox = true;
result = '<i class="icon-dropbox"></i>' + result;
}
if (sync.indexOf(";" + SYNC_PROVIDER_GDRIVE) !== -1) { if (sync.indexOf(";" + SYNC_PROVIDER_GDRIVE) !== -1) {
useGoogleDrive = true; useGoogleDrive = true;
result = '<i class="icon-gdrive"></i>' + result; result = '<i class="icon-gdrive"></i>' + result;
@ -245,6 +268,7 @@ define(["jquery", "core", "gdrive", "synchronizer", "async-runner"], function($,
$("#file-selector").append(li); $("#file-selector").append(li);
} }
synchronizer.useGoogleDrive = useGoogleDrive; synchronizer.useGoogleDrive = useGoogleDrive;
synchronizer.useDropbox = useDropbox;
}; };
// Remove a synchronized location // Remove a synchronized location
@ -258,8 +282,10 @@ define(["jquery", "core", "gdrive", "synchronizer", "async-runner"], function($,
refreshManageSync(); refreshManageSync();
} }
} }
// Remove etag // Remove Google Drive etag
localStorage.removeItem(fileSyncIndex + ".etag"); localStorage.removeItem(fileSyncIndex + ".etag");
// Remove Dropbox version
localStorage.removeItem(fileSyncIndex + ".version");
}; };
// Look for local file associated to a synchronized location // Look for local file associated to a synchronized location
@ -277,11 +303,11 @@ define(["jquery", "core", "gdrive", "synchronizer", "async-runner"], function($,
return fileIndex; return fileIndex;
}; };
function uploadGdrive() { function uploadGdrive(folderId) {
var fileIndex = localStorage["file.current"]; var fileIndex = localStorage["file.current"];
var content = localStorage[fileIndex + ".content"]; var content = localStorage[fileIndex + ".content"];
var title = localStorage[fileIndex + ".title"]; var title = localStorage[fileIndex + ".title"];
gdrive.createFile(title, content, function(fileSyncIndex) { gdrive.upload(undefined, folderId, title, content, function(fileSyncIndex) {
if (fileSyncIndex === undefined) { if (fileSyncIndex === undefined) {
return; return;
} }
@ -342,6 +368,56 @@ define(["jquery", "core", "gdrive", "synchronizer", "async-runner"], function($,
}); });
} }
function manualDropbox(path) {
if(!path) {
return;
}
path = dropbox.checkPath(path);
if(path === undefined) {
return;
}
// Check that file is not synchronized with an other one
var fileSyncIndex = SYNC_PROVIDER_DROPBOX + encodeURIComponent(path.toLowerCase());
var fileIndex = fileManager.getFileIndexFromSync(fileSyncIndex);
if(fileIndex !== undefined) {
var title = localStorage[fileIndex + ".title"];
core.showError('Path "' + path + '" is already synchronized with "' + title + '"');
return;
}
var fileIndex = localStorage["file.current"];
var content = localStorage[fileIndex + ".content"];
var title = localStorage[fileIndex + ".title"];
dropbox.upload(path, content, function(fileSyncIndex) {
if (fileSyncIndex === undefined) {
return;
}
localStorage[fileIndex + ".sync"] += fileSyncIndex + ";";
refreshManageSync();
fileManager.updateFileTitles();
core.showMessage('"' + title
+ '" will now be synchronized on Dropbox.');
});
}
function importDropbox(paths) {
if(paths === undefined) {
return;
}
var importPaths = [];
for(var i=0; i<paths.length; i++) {
var filePath = paths[i];
var fileSyncIndex = SYNC_PROVIDER_DROPBOX + encodeURIComponent(filePath.toLowerCase());
var fileIndex = fileManager.getFileIndexFromSync(fileSyncIndex);
if(fileIndex !== undefined) {
var title = localStorage[fileIndex + ".title"];
core.showError('"' + title + '" was already imported');
continue;
}
importPaths.push(filePath);
}
dropbox.importFiles(importPaths);
}
function refreshManageSync() { function refreshManageSync() {
var fileIndex = localStorage["file.current"]; var fileIndex = localStorage["file.current"];
var fileSyncIndexList = localStorage[fileIndex + ".sync"].split(";"); var fileSyncIndexList = localStorage[fileIndex + ".sync"].split(";");
@ -361,12 +437,18 @@ define(["jquery", "core", "gdrive", "synchronizer", "async-runner"], function($,
'<i class="icon-gdrive"></i>')); '<i class="icon-gdrive"></i>'));
line.append($("<input>").prop("type", "text").prop( line.append($("<input>").prop("type", "text").prop(
"disabled", true).addClass("span5").val( "disabled", true).addClass("span5").val(
"ID=" fileSyncIndex.substring(SYNC_PROVIDER_GDRIVE.length)));
+ fileSyncIndex.substring(SYNC_PROVIDER_GDRIVE.length))); }
if (fileSyncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) {
line.append($("<span>").addClass("add-on").html(
'<i class="icon-dropbox"></i>'));
line.append($("<input>").prop("type", "text").prop(
"disabled", true).addClass("span5").val(
decodeURIComponent(fileSyncIndex.substring(SYNC_PROVIDER_DROPBOX.length))));
} }
line.append($("<a>").addClass("btn").html( line.append($("<a>").addClass("btn").html(
'<i class="icon-trash"></i>').prop("title", '<i class="icon-trash"></i>').prop("title",
"Remove this synchronized location").click(function() { "Remove this location").click(function() {
fileManager.removeSync(fileSyncIndex); fileManager.removeSync(fileSyncIndex);
fileManager.updateFileTitles(); fileManager.updateFileTitles();
})); }));

View File

@ -34,12 +34,10 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
}); });
}; };
asyncTask.onSuccess = function() { asyncTask.onSuccess = function() {
delayedFunction = undefined;
connected = true; connected = true;
callback(); callback();
}; };
asyncTask.onError = function() { asyncTask.onError = function() {
delayedFunction = undefined;
core.setOffline(); core.setOffline();
callback(); callback();
}; };
@ -99,7 +97,7 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
}); });
} }
function upload(fileId, parentId, title, content, callback) { gdrive.upload = function(fileId, parentId, title, content, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
authenticate(function() { authenticate(function() {
if (connected === false) { if (connected === false) {
@ -172,7 +170,7 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
}; };
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
}); });
} };
gdrive.checkUpdates = function(lastChangeId, callback) { gdrive.checkUpdates = function(lastChangeId, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
@ -245,10 +243,13 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
var id = ids.pop(); var id = ids.pop();
var asyncTask = {}; var asyncTask = {};
asyncTask.run = function() { asyncTask.run = function() {
var accessToken = gapi.auth.getToken().access_token; var token = gapi.auth.getToken();
var headers = {
Authorization : token ? "Bearer " + token.access_token: null
};
$.ajax({ $.ajax({
url : "https://www.googleapis.com/drive/v2/files/" + id, url : "https://www.googleapis.com/drive/v2/files/" + id,
headers : { "Authorization" : "Bearer " + accessToken }, headers : headers,
dataType : "json", dataType : "json",
timeout : AJAX_TIMEOUT timeout : AJAX_TIMEOUT
}).done(function(data, textStatus, jqXHR) { }).done(function(data, textStatus, jqXHR) {
@ -261,7 +262,7 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
}; };
// Handle error // Handle error
if(error.code === 404) { if(error.code === 404) {
error = "File is not available."; error = 'File ID "' + id + '" does not exist on Google Drive.';
} }
handleError(error, asyncTask, callback); handleError(error, asyncTask, callback);
}); });
@ -292,7 +293,7 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
else if(object.kind == "drive#change") { else if(object.kind == "drive#change") {
file = object.file; file = object.file;
} }
if(file === undefined) { if(!file) {
this.downloadContent(objects, callback, result); this.downloadContent(objects, callback, result);
return; return;
} }
@ -305,10 +306,13 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
var asyncTask = {}; var asyncTask = {};
asyncTask.run = function() { asyncTask.run = function() {
var accessToken = gapi.auth.getToken().access_token; var token = gapi.auth.getToken();
var headers = {
Authorization : token ? "Bearer " + token.access_token: null
};
$.ajax({ $.ajax({
url : file.downloadUrl, url : file.downloadUrl,
headers : { "Authorization" : "Bearer " + accessToken }, headers : headers,
dataType : "text", dataType : "text",
timeout : AJAX_TIMEOUT timeout : AJAX_TIMEOUT
}).done(function(data, textStatus, jqXHR) { }).done(function(data, textStatus, jqXHR) {
@ -365,7 +369,7 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
var pickerLoaded = false; var pickerLoaded = false;
function loadPicker(callback) { function loadPicker(callback) {
authenticate(function() { connect(function() {
if (connected === false) { if (connected === false) {
pickerLoaded = false; pickerLoaded = false;
callback(); callback();
@ -407,7 +411,6 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
callback(); callback();
return; return;
} }
var view = new google.picker.View(google.picker.ViewId.DOCS); var view = new google.picker.View(google.picker.ViewId.DOCS);
view.setMimeTypes("text/x-markdown,text/plain"); view.setMimeTypes("text/x-markdown,text/plain");
var pickerBuilder = new google.picker.PickerBuilder(); var pickerBuilder = new google.picker.PickerBuilder();
@ -443,14 +446,6 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
}); });
}; };
gdrive.createFile = function(title, content, callback) {
upload(undefined, undefined, title, content, callback);
};
gdrive.updateFile = function(id, title, content, callback) {
upload(id, undefined, title, content, callback);
};
gdrive.importFiles = function(ids) { gdrive.importFiles = function(ids) {
gdrive.downloadMetadata(ids, function(result) { gdrive.downloadMetadata(ids, function(result) {
if(result === undefined) { if(result === undefined) {
@ -481,7 +476,7 @@ define(["jquery", "core", "async-runner"], function($, core, asyncTaskRunner) {
localStorage.removeItem("sync.gdrive.state"); localStorage.removeItem("sync.gdrive.state");
state = JSON.parse(state); state = JSON.parse(state);
if (state.action == "create") { if (state.action == "create") {
upload(undefined, state.folderId, GDRIVE_DEFAULT_FILE_TITLE, gdrive.upload(undefined, state.folderId, GDRIVE_DEFAULT_FILE_TITLE,
"", function(fileSyncIndex) { "", function(fileSyncIndex) {
if(fileSyncIndex === undefined) { if(fileSyncIndex === undefined) {
return; return;

View File

@ -6,6 +6,7 @@ if(location.hostname.indexOf("benweet.github.") === 0) {
// RequireJS configuration // RequireJS configuration
requirejs.config({ requirejs.config({
waitSeconds: 0,
paths: configPaths, paths: configPaths,
shim: { shim: {
'jquery-ui': ['jquery'], 'jquery-ui': ['jquery'],

View File

@ -1,4 +1,4 @@
define(["jquery", "core", "gdrive"], function($, core, gdrive) { define(["jquery", "core", "gdrive", "dropbox"], function($, core, gdrive, dropbox) {
var synchronizer = {}; var synchronizer = {};
// Dependencies // Dependencies
@ -6,6 +6,7 @@ define(["jquery", "core", "gdrive"], function($, core, gdrive) {
// Used to know the providers we are connected to // Used to know the providers we are connected to
synchronizer.useGoogleDrive = false; synchronizer.useGoogleDrive = false;
synchronizer.useDropbox = false;
var onSyncBegin = undefined; var onSyncBegin = undefined;
var onSyncEnd = undefined; var onSyncEnd = undefined;
@ -60,7 +61,21 @@ define(["jquery", "core", "gdrive"], function($, core, gdrive) {
// Try to find the provider // Try to find the provider
if (fileSyncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) { if (fileSyncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) {
var id = fileSyncIndex.substring(SYNC_PROVIDER_GDRIVE.length); var id = fileSyncIndex.substring(SYNC_PROVIDER_GDRIVE.length);
gdrive.updateFile(id, title, content, function(result) { gdrive.upload(id, undefined, title, content, function(result) {
if (result !== undefined) {
fileUp(fileSyncIndexList, content, title, callback);
return;
}
// If error we put the fileIndex back in the queue
synchronizer.addFileForUpload(localStorage["sync.current"]);
localStorage.removeItem("sync.current");
callback();
return;
});
} else if (fileSyncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) {
var path = fileSyncIndex.substring(SYNC_PROVIDER_DROPBOX.length);
path = decodeURIComponent(path);
dropbox.upload(path, content, function(result) {
if (result !== undefined) { if (result !== undefined) {
fileUp(fileSyncIndexList, content, title, callback); fileUp(fileSyncIndexList, content, title, callback);
return; return;
@ -157,7 +172,7 @@ define(["jquery", "core", "gdrive"], function($, core, gdrive) {
core.showMessage('"' + file.title + '" has been updated from Google Drive.'); core.showMessage('"' + file.title + '" has been updated from Google Drive.');
if(fileIndex == localStorage["file.current"]) { if(fileIndex == localStorage["file.current"]) {
updateFileTitles = false; // Done by next function updateFileTitles = false; // Done by next function
fileManager.selectFile(); fileManager.selectFile(); // Refresh editor
} }
} }
// Update file etag // Update file etag
@ -175,8 +190,78 @@ define(["jquery", "core", "gdrive"], function($, core, gdrive) {
}); });
} }
function syncDownDropbox(callback) {
if (synchronizer.useDropbox === false) {
callback();
return;
}
var lastChangeId = localStorage[SYNC_PROVIDER_DROPBOX + "lastChangeId"];
dropbox.checkUpdates(lastChangeId, function(changes, newChangeId) {
if (changes === undefined) {
callback();
return;
}
dropbox.downloadContent(changes, function(changes) {
if (changes === undefined) {
callback();
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 (this should never happen...)
if(fileIndex === undefined) {
// We can remove the stored version
localStorage.removeItem(fileSyncIndex + ".version");
continue;
}
var title = localStorage[fileIndex + ".title"];
// File deleted
if (change.wasRemoved === true) {
fileManager.removeSync(fileSyncIndex);
updateFileTitles = true;
core.showMessage('"' + title + '" has been removed from Dropbox.');
continue;
}
var content = localStorage[fileIndex + ".content"];
var file = change.stat;
var contentChanged = content != file.content;
// If file is in the upload queue we have a conflict
if (contentChanged && syncUpQueue.indexOf(";" + fileIndex + ";") !== -1) {
fileManager.createFile(title + " (backup)", content);
updateFileTitles = true;
core.showMessage('Conflict detected on "' + title + '". A backup has been created locally.');
}
// If file content changed
if(contentChanged) {
localStorage[fileIndex + ".content"] = file.content;
core.showMessage('"' + title + '" has been updated from Dropbox.');
if(fileIndex == localStorage["file.current"]) {
updateFileTitles = false; // Done by next function
fileManager.selectFile(); // Refresh editor
}
}
// Update file version
localStorage[fileSyncIndex + ".version"] = file.versionTag;
// Synchronize file to others locations
synchronizer.addFileForUpload(fileIndex);
}
if(updateFileTitles) {
fileManager.updateFileTitles();
}
localStorage[SYNC_PROVIDER_DROPBOX
+ "lastChangeId"] = newChangeId;
callback();
});
});
}
function syncDown(callback) { function syncDown(callback) {
syncDownGdrive(callback); syncDownGdrive(function() {
syncDownDropbox(callback);
});
}; };
var syncRunning = false; var syncRunning = false;