From 32b9ac2c30a3226c5b5a2c52c014b0da96873465 2014-11-03 18:17:12 From: KesterTong Date: 2014-11-03 18:17:12 Subject: [PATCH] Adds Google Drive version of ContentManager --- diff --git a/IPython/html/static/base/js/drive_contentmanager.js b/IPython/html/static/base/js/drive_contentmanager.js new file mode 100644 index 0000000..f1e774d --- /dev/null +++ b/IPython/html/static/base/js/drive_contentmanager.js @@ -0,0 +1,420 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +define([ + 'base/js/namespace', + 'jquery', + 'base/js/utils', + 'base/js/dialog', +], function(IPython, $, utils, dialog) { + var FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder'; + + var ContentManager = function(options) { + // Constructor + // + // A contentmanager handles passing file operations + // to the back-end. This includes checkpointing + // with the normal file operations. + // + // Parameters: + // options: dictionary + // Dictionary of keyword arguments. + // events: $(Events) instance + // base_url: string + var that = this; + this.version = 0.1; + this.events = options.events; + this.base_url = options.base_url; + this.gapi_ready = $.Deferred(); + + this.gapi_ready.fail(function(){ + // TODO: display a dialog + console.log('failed to load Google API'); + }); + + // load Google API + $.getScript('https://apis.google.com/js/client.js'); + function poll_for_gapi_load() { + if (window.gapi && gapi.client) { + that.on_gapi_load(); + } else { + setTimeout(poll_for_gapi_load, 100); + } + } + poll_for_gapi_load(); + }; + + /** + * low level Google Drive functions + */ + + /* + * Load Google Drive client library + * @method on_gapi_load + */ + ContentManager.prototype.on_gapi_load = function() { + var that = this; + gapi.load('auth:client,drive-realtime,drive-share', function() { + gapi.client.load('drive', 'v2', function() { + that.authorize(false); + }); + }); + }; + + /** + * Authorize using Google OAuth API. + * @method authorize + * @param {boolean} opt_withPopup If true, display popup without first + * trying to authorize without a popup. + */ + ContentManager.prototype.authorize = function(opt_withPopup) { + var that = this; + var doAuthorize = function() { + gapi.auth.authorize({ + 'client_id': '911569945122-tlvi6ucbj137ifhitpqpdikf3qo1mh9d.apps.googleusercontent.com', + 'scope': ['https://www.googleapis.com/auth/drive'], + 'immediate': !opt_withPopup + }, function(response) { + if (!response || response['error']) { + if (opt_withPopup) { + that.gapi_ready.reject(response ? response['error'] : null); + } else { + that.authorize(true); + } + return; + } + that.gapi_ready.resolve(); + }); + }; + + // if no popup, calls the authorization function immediately + if (!opt_withPopup) { + doAuthorize(); + return; + } + + // Gets user to initiate the authorization with a dialog, + // to prevent popup blockers. + var options = { + title: 'Authentication needed', + body: ('Accessing Google Drive requires authentication. Click' + + ' ok to proceed.'), + buttons: { + 'ok': { click : doAuthorize }, + 'cancel': { click : that.gapi_ready.reject } + } + } + dialog.modal(options); + }; + + /** + * Gets the Google Drive folder ID corresponding to a path. Since + * the Google Drive API doesn't expose a path structure, it is necessary + * to manually walk the path from root. + */ + ContentManager.prototype.get_id_for_path = function(path, onSuccess, onFailure) { + // Use recursive strategy, with helper function + // get_id_for_relative_path. + + // calls callbacks with the id for the sepcified path, treated as + // a relative path with base given by base_id. + function get_id_for_relative_path(base_id, path_components) { + if (path_components.length == 0) { + onSuccess(base_id); + return; + } + + var this_component = path_components.pop(); + + // Treat the empty string as a special case, and ignore it. + // This will result in ignoring leading and trailing slashes. + if (this_component == "") { + get_id_for_relative_path(base_id, path_components); + return; + } + + var query = ('mimeType = \'' + FOLDER_MIME_TYPE + '\'' + + ' and title = \'' + this_component + '\''); + var request = gapi.client.drive.children.list({ + 'folderId': base_id, + 'q': query + }); + request.execute(function(response) { + if (!response || response['error']) { + onFailure(response ? response['error'] : null); + return; + } + + var child_folders = response['items']; + if (!child_folders) { + // 'directory does not exist' error. + onFailure(); + return; + } + + if (child_folders.length > 1) { + // 'runtime error' this should not happen + onFailure(); + return; + } + + get_id_for_relative_path(child_folders[0]['id'], + path_components); + }); + }; + get_id_for_relative_path('root', path.split('/').reverse()); + } + + + /** + * Notebook Functions + */ + + /** + * Creates a new notebook file at the specified path, and + * opens that notebook in a new window. + * + * @method scroll_to_cell + * @param {String} path The path to create the new notebook at + */ + ContentManager.prototype.new_notebook = function(path) { + var base_url = this.base_url; + var settings = { + processData : false, + cache : false, + type : "POST", + dataType : "json", + async : false, + success : function (data, status, xhr){ + var notebook_name = data.name; + window.open( + utils.url_join_encode( + base_url, + 'notebooks', + path, + notebook_name + ), + '_blank' + ); + }, + error : function(xhr, status, error) { + utils.log_ajax_error(xhr, status, error); + var msg; + if (xhr.responseJSON && xhr.responseJSON.message) { + msg = xhr.responseJSON.message; + } else { + msg = xhr.statusText; + } + dialog.modal({ + title : 'Creating Notebook Failed', + body : "The error was: " + msg, + buttons : {'OK' : {'class' : 'btn-primary'}} + }); + } + }; + var url = utils.url_join_encode( + base_url, + 'api/notebooks', + path + ); + $.ajax(url,settings); + }; + + ContentManager.prototype.delete_notebook = function(name, path) { + var settings = { + processData : false, + cache : false, + type : "DELETE", + dataType : "json", + success : $.proxy(this.events.trigger, this.events, + 'notebook_deleted.ContentManager', + { + name: name, + path: path + }), + error : utils.log_ajax_error + }; + var url = utils.url_join_encode( + this.base_url, + 'api/notebooks', + path, + name + ); + $.ajax(url, settings); + }; + + ContentManager.prototype.rename_notebook = function(path, name, new_name) { + var that = this; + var data = {name: new_name}; + var settings = { + processData : false, + cache : false, + type : "PATCH", + data : JSON.stringify(data), + dataType: "json", + headers : {'Content-Type': 'application/json'}, + success : function (json, status, xhr) { + that.events.trigger('notebook_rename_success.ContentManager', + json); + }, + error : function (xhr, status, error) { + that.events.trigger('notebook_rename_error.ContentManager', + [xhr, status, error]); + } + } + var url = utils.url_join_encode( + this.base_url, + 'api/notebooks', + path, + name + ); + $.ajax(url, settings); + }; + + ContentManager.prototype.save_notebook = function(path, name, content, + extra_settings) { + var that = notebook; + // Create a JSON model to be sent to the server. + var model = { + name : name, + path : path, + content : content + }; + // time the ajax call for autosave tuning purposes. + var start = new Date().getTime(); + // We do the call with settings so we can set cache to false. + var settings = { + processData : false, + cache : false, + type : "PUT", + data : JSON.stringify(model), + headers : {'Content-Type': 'application/json'}, + success : $.proxy(this.events.trigger, this.events, + 'notebook_save_success.ContentManager', + $.extend(model, { start : start })), + error : function (xhr, status, error) { + that.events.trigger('notebook_save_error.ContentManager', + [xhr, status, error, model]); + } + }; + if (extra_settings) { + for (var key in extra_settings) { + settings[key] = extra_settings[key]; + } + } + var url = utils.url_join_encode( + this.base_url, + 'api/notebooks', + path, + name + ); + $.ajax(url, settings); + }; + + /** + * Checkpointing Functions + */ + + ContentManager.prototype.save_checkpoint = function() { + // This is not necessary - integrated into save + }; + + ContentManager.prototype.restore_checkpoint = function(notebook, id) { + that = notebook; + this.events.trigger('notebook_restoring.Notebook', checkpoint); + var url = utils.url_join_encode( + this.base_url, + 'api/notebooks', + this.notebook_path, + this.notebook_name, + 'checkpoints', + checkpoint + ); + $.post(url).done( + $.proxy(that.restore_checkpoint_success, that) + ).fail( + $.proxy(that.restore_checkpoint_error, that) + ); + }; + + ContentManager.prototype.list_checkpoints = function(notebook) { + that = notebook; + var url = utils.url_join_encode( + that.base_url, + 'api/notebooks', + that.notebook_path, + that.notebook_name, + 'checkpoints' + ); + $.get(url).done( + $.proxy(that.list_checkpoints_success, that) + ).fail( + $.proxy(that.list_checkpoints_error, that) + ); + }; + + /** + * File management functions + */ + + /** + * List notebooks and directories at a given path + * + * On success, load_callback is called with an array of dictionaries + * representing individual files or directories. Each dictionary has + * the keys: + * type: "notebook" or "directory" + * name: the name of the file or directory + * created: created date + * last_modified: last modified dat + * path: the path + * @method list_notebooks + * @param {String} path The path to list notebooks in + * @param {Function} load_callback called with list of notebooks on success + * @param {Function} error_callback called with ajax results on error + */ + ContentManager.prototype.list_contents = function(path, load_callback, + error_callback) { + var that = this; + this.gapi_ready.done(function() { + that.get_id_for_path(path, function(folder_id) { + query = ('(fileExtension = \'ipynb\' or' + + ' mimeType = \'' + FOLDER_MIME_TYPE + '\')' + + ' and \'' + folder_id + '\' in parents'); + var request = gapi.client.drive.files.list({ + 'maxResults' : 1000, + 'q' : query + }); + request.execute(function(response) { + // On a drive API error, call error_callback + if (!response || response['error']) { + error_callback(response ? response['error'] : null); + return; + } + + // Convert this list to the format that is passed to + // load_callback. Note that a files resource can represent + // a file or a directory. + // TODO: check that date formats are the same, and either + // convert to the IPython format, or document the difference. + var list = $.map(response['items'], function(files_resource) { + var type = files_resource['mimeType'] == FOLDER_MIME_TYPE ? 'directory' : 'notebook'; + return { + type: type, + name: files_resource['title'], + path: path, + created: files_resource['createdDate'], + last_modified: files_resource['modifiedDate'] + }; + }); + load_callback(list); + }); + }, error_callback); + }); + }; + + + IPython.ContentManager = ContentManager; + + return {'ContentManager': ContentManager}; +});