|
|
// 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();
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Name of newly created notebook files.
|
|
|
* @type {string}
|
|
|
*/
|
|
|
ContentManager.NEW_NOTEBOOK_TITLE = 'Untitled';
|
|
|
|
|
|
/**
|
|
|
* Extension for notebook files.
|
|
|
* @type {string}
|
|
|
*/
|
|
|
ContentManager.NOTEBOOK_EXTENSION = 'ipynb';
|
|
|
|
|
|
|
|
|
ContentManager.MULTIPART_BOUNDARY = '-------314159265358979323846';
|
|
|
|
|
|
ContentManager.NOTEBOOK_MIMETYPE = 'application/ipynb';
|
|
|
|
|
|
|
|
|
/**
|
|
|
* low level Google Drive functions
|
|
|
*
|
|
|
* NOTE: these functions should only be called after gapi_ready has been
|
|
|
* resolved, with the excpetion of authorize(), and on_gapi_load() which
|
|
|
* is private and should not be called at all. Typical usage is:
|
|
|
*
|
|
|
* var that = this;
|
|
|
* this.gapi_ready.done(function () {
|
|
|
* that.get_id_for_path(...)
|
|
|
* ...
|
|
|
* });
|
|
|
*/
|
|
|
|
|
|
/*
|
|
|
* Load Google Drive client library
|
|
|
* @private
|
|
|
* @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.
|
|
|
* @method get_id_for_path
|
|
|
* @param {String} path The path
|
|
|
* @param {Function} onSuccess called with the folder Id on success
|
|
|
* @param {Function} onFailure called with the error on Failure
|
|
|
*/
|
|
|
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 + '\''
|
|
|
+ ' and trashed = false');
|
|
|
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());
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 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.
|
|
|
* @method get_id_for_path
|
|
|
* @param {String} folder_id The google Drive folder id to search
|
|
|
* @param {String} filename The filename to find in folder_id
|
|
|
* @param {Function} onSuccess called with a files resource on success (see
|
|
|
* Google Drive API documentation for more information on the files
|
|
|
* resource).
|
|
|
* @param {Function} onFailure called with the error on Failure
|
|
|
*/
|
|
|
ContentManager.prototype.get_resource_for_filename = function(
|
|
|
folder_id,
|
|
|
filename,
|
|
|
onSuccess,
|
|
|
onFailure) {
|
|
|
var query = ('title = \'' + filename + '\''
|
|
|
+ ' and \'' + folder_id + '\' in parents'
|
|
|
+ ' and trashed = false');
|
|
|
var request = gapi.client.drive.files.list({
|
|
|
'q': query
|
|
|
});
|
|
|
request.execute(function(response) {
|
|
|
if (!response || response['error']) {
|
|
|
onFailure(response ? response['error'] : null);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
var files = response['items'];
|
|
|
if (!files) {
|
|
|
// 'directory does not exist' error.
|
|
|
onFailure();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (files.length > 1) {
|
|
|
// 'runtime error' this should not happen
|
|
|
onFailure();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
onSuccess(files[0]);
|
|
|
|
|
|
});
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Uploads a notebook to Drive, either creating a new one or saving an
|
|
|
* existing one.
|
|
|
*
|
|
|
* @method upload_to_drive
|
|
|
* @param {string} data The file contents as a string
|
|
|
* @param {Object} metadata File metadata
|
|
|
* @param {function(gapi.client.drive.files.Resource)} success_callback callback for
|
|
|
* success
|
|
|
* @param {function(?):?} error_callback callback for error, takes response object
|
|
|
* @param {string=} opt_fileId file Id. If false, a new file is created.
|
|
|
* @param {Object?} opt_params a dictionary containing the following keys
|
|
|
* pinned: whether this save should be pinned
|
|
|
*/
|
|
|
ContentManager.prototype.upload_to_drive = function(data, metadata,
|
|
|
success_callback, error_callback, opt_fileId, opt_params) {
|
|
|
var params = opt_params || {};
|
|
|
var delimiter = '\r\n--' + ContentManager.MULTIPART_BOUNDARY + '\r\n';
|
|
|
var close_delim = '\r\n--' + ContentManager.MULTIPART_BOUNDARY + '--';
|
|
|
var body = delimiter +
|
|
|
'Content-Type: application/json\r\n\r\n' +
|
|
|
JSON.stringify(metadata) +
|
|
|
delimiter +
|
|
|
'Content-Type: ' + ContentManager.NOTEBOOK_MIMETYPE + '\r\n' +
|
|
|
'\r\n' +
|
|
|
data +
|
|
|
close_delim;
|
|
|
|
|
|
var path = '/upload/drive/v2/files';
|
|
|
var method = 'POST';
|
|
|
if (opt_fileId) {
|
|
|
path += '/' + opt_fileId;
|
|
|
method = 'PUT';
|
|
|
}
|
|
|
|
|
|
var request = gapi.client.request({
|
|
|
'path': path,
|
|
|
'method': method,
|
|
|
'params': {
|
|
|
'uploadType': 'multipart',
|
|
|
'pinned' : params['pinned']
|
|
|
},
|
|
|
'headers': {
|
|
|
'Content-Type': 'multipart/mixed; boundary="' +
|
|
|
ContentManager.MULTIPART_BOUNDARY + '"'
|
|
|
},
|
|
|
'body': body
|
|
|
});
|
|
|
request.execute(function(response) {
|
|
|
if (!response || response['error']) {
|
|
|
error_callback(response ? response['error'] : null);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
success_callback(response);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Obtains the filename that should be used for a new file in a given folder.
|
|
|
* This is the next file in the series Untitled0, Untitled1, ... in the given
|
|
|
* drive folder. As a fallback, returns Untitled.
|
|
|
*
|
|
|
* @method get_new_filename
|
|
|
* @param {function(string)} callback Called with the name for the new file.
|
|
|
* @param {string} opt_folderId optinal Drive folder Id to search for
|
|
|
* filenames. Uses root, if none is specified.
|
|
|
*/
|
|
|
ContentManager.prototype.get_new_filename = function(callback, opt_folderId) {
|
|
|
/** @type {string} */
|
|
|
var folderId = opt_folderId || 'root';
|
|
|
var query = 'title contains \'' + ContentManager.NEW_NOTEBOOK_TITLE + '\'' +
|
|
|
' and \'' + folderId + '\' in parents' +
|
|
|
' and trashed = false';
|
|
|
var request = gapi.client.drive.files.list({
|
|
|
'maxResults': 1000,
|
|
|
'folderId' : folderId,
|
|
|
'q': query
|
|
|
});
|
|
|
|
|
|
request.execute(function(response) {
|
|
|
// Use 'Untitled.ipynb' as a fallback in case of error
|
|
|
var fallbackFilename = ContentManager.NEW_NOTEBOOK_TITLE + '.' +
|
|
|
ContentManager.NOTEBOOK_EXTENSION;
|
|
|
if (!response || response['error']) {
|
|
|
callback(fallbackFilename);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
var files = response['items'] || [];
|
|
|
var existingFilenames = $.map(files, function(filesResource) {
|
|
|
return filesResource['title'];
|
|
|
});
|
|
|
|
|
|
// Loop over file names Untitled0, ... , UntitledN where N is the number of
|
|
|
// elements in existingFilenames. Select the first file name that does not
|
|
|
// belong to existingFilenames. This is guaranteed to find a file name
|
|
|
// that does not belong to existingFilenames, since there are N + 1 file
|
|
|
// names tried, and existingFilenames contains N elements.
|
|
|
for (var i = 0; i <= existingFilenames.length; i++) {
|
|
|
/** @type {string} */
|
|
|
var filename = ContentManager.NEW_NOTEBOOK_TITLE + i + '.' +
|
|
|
ContentManager.NOTEBOOK_EXTENSION;
|
|
|
if (existingFilenames.indexOf(filename) == -1) {
|
|
|
callback(filename);
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Control should not reach this point, so an error has occured
|
|
|
callback(fallbackFilename);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Notebook Functions
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
* Load a notebook.
|
|
|
*
|
|
|
* Calls success_callback with notebook JSON object (as string), or
|
|
|
* error_callback with error.
|
|
|
*
|
|
|
* @method load_notebook
|
|
|
* @param {String} path
|
|
|
* @param {String} name
|
|
|
* @param {Function} success_callback
|
|
|
* @param {Function} error_callback
|
|
|
*/
|
|
|
ContentManager.prototype.load_notebook = function (path, name, success_callback,
|
|
|
error_callback) {
|
|
|
var that = this;
|
|
|
this.gapi_ready.done(function() {
|
|
|
that.get_id_for_path(path, function(folder_id) {
|
|
|
that.get_resource_for_filename(folder_id, name, function(file_resource) {
|
|
|
// Sends request to load file to drive.
|
|
|
var token = gapi.auth.getToken()['access_token'];
|
|
|
var xhrRequest = new XMLHttpRequest();
|
|
|
xhrRequest.open('GET', file_resource['downloadUrl'], true);
|
|
|
xhrRequest.setRequestHeader('Authorization', 'Bearer ' + token);
|
|
|
xhrRequest.onreadystatechange = function(e) {
|
|
|
if (xhrRequest.readyState == 4) {
|
|
|
if (xhrRequest.status == 200) {
|
|
|
var notebook_contents = xhrRequest.responseText;
|
|
|
//colab.nbformat.convertJsonNotebookToRealtime(
|
|
|
// notebook_contents, model);
|
|
|
var model = JSON.parse(notebook_contents);
|
|
|
|
|
|
success_callback({
|
|
|
content: model,
|
|
|
// A hack to deal with file/memory format conversions
|
|
|
name: model.metadata.name
|
|
|
});
|
|
|
} else {
|
|
|
error_callback(xhrRequest);
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
xhrRequest.send();
|
|
|
}, error_callback)
|
|
|
}, error_callback);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 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 that = this;
|
|
|
this.gapi_ready.done(function() {
|
|
|
that.get_id_for_path(path, function(folder_id) {
|
|
|
that.get_new_filename(function(filename) {
|
|
|
var data = {
|
|
|
'worksheets': [{
|
|
|
'cells' : [{
|
|
|
'cell_type': 'code',
|
|
|
'input': '',
|
|
|
'outputs': [],
|
|
|
'language': 'python'
|
|
|
}],
|
|
|
}],
|
|
|
'metadata': {
|
|
|
'name': filename,
|
|
|
},
|
|
|
'nbformat': 3,
|
|
|
'nbformat_minor': 0
|
|
|
};
|
|
|
var metadata = {
|
|
|
'parents' : [{'id' : folder_id}],
|
|
|
'title' : filename,
|
|
|
'description': 'IP[y] file',
|
|
|
'mimeType': ContentManager.NOTEBOOK_MIMETYPE
|
|
|
}
|
|
|
that.upload_to_drive(JSON.stringify(data), metadata, function (data, status, xhr) {
|
|
|
var notebook_name = data.name;
|
|
|
window.open(
|
|
|
utils.url_join_encode(
|
|
|
that.base_url,
|
|
|
'contents',
|
|
|
path,
|
|
|
filename
|
|
|
),
|
|
|
'_blank'
|
|
|
);
|
|
|
}, function(){});
|
|
|
}, folder_id);
|
|
|
})
|
|
|
});
|
|
|
};
|
|
|
|
|
|
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/contents',
|
|
|
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/contents',
|
|
|
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/contents',
|
|
|
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/contents',
|
|
|
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/contents',
|
|
|
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'
|
|
|
+ ' and trashed = false');
|
|
|
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};
|
|
|
});
|
|
|
|