##// END OF EJS Templates
Adds Google Drive version of ContentManager
KesterTong -
Show More
@@ -0,0 +1,420 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 define([
5 'base/js/namespace',
6 'jquery',
7 'base/js/utils',
8 'base/js/dialog',
9 ], function(IPython, $, utils, dialog) {
10 var FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder';
11
12 var ContentManager = function(options) {
13 // Constructor
14 //
15 // A contentmanager handles passing file operations
16 // to the back-end. This includes checkpointing
17 // with the normal file operations.
18 //
19 // Parameters:
20 // options: dictionary
21 // Dictionary of keyword arguments.
22 // events: $(Events) instance
23 // base_url: string
24 var that = this;
25 this.version = 0.1;
26 this.events = options.events;
27 this.base_url = options.base_url;
28 this.gapi_ready = $.Deferred();
29
30 this.gapi_ready.fail(function(){
31 // TODO: display a dialog
32 console.log('failed to load Google API');
33 });
34
35 // load Google API
36 $.getScript('https://apis.google.com/js/client.js');
37 function poll_for_gapi_load() {
38 if (window.gapi && gapi.client) {
39 that.on_gapi_load();
40 } else {
41 setTimeout(poll_for_gapi_load, 100);
42 }
43 }
44 poll_for_gapi_load();
45 };
46
47 /**
48 * low level Google Drive functions
49 */
50
51 /*
52 * Load Google Drive client library
53 * @method on_gapi_load
54 */
55 ContentManager.prototype.on_gapi_load = function() {
56 var that = this;
57 gapi.load('auth:client,drive-realtime,drive-share', function() {
58 gapi.client.load('drive', 'v2', function() {
59 that.authorize(false);
60 });
61 });
62 };
63
64 /**
65 * Authorize using Google OAuth API.
66 * @method authorize
67 * @param {boolean} opt_withPopup If true, display popup without first
68 * trying to authorize without a popup.
69 */
70 ContentManager.prototype.authorize = function(opt_withPopup) {
71 var that = this;
72 var doAuthorize = function() {
73 gapi.auth.authorize({
74 'client_id': '911569945122-tlvi6ucbj137ifhitpqpdikf3qo1mh9d.apps.googleusercontent.com',
75 'scope': ['https://www.googleapis.com/auth/drive'],
76 'immediate': !opt_withPopup
77 }, function(response) {
78 if (!response || response['error']) {
79 if (opt_withPopup) {
80 that.gapi_ready.reject(response ? response['error'] : null);
81 } else {
82 that.authorize(true);
83 }
84 return;
85 }
86 that.gapi_ready.resolve();
87 });
88 };
89
90 // if no popup, calls the authorization function immediately
91 if (!opt_withPopup) {
92 doAuthorize();
93 return;
94 }
95
96 // Gets user to initiate the authorization with a dialog,
97 // to prevent popup blockers.
98 var options = {
99 title: 'Authentication needed',
100 body: ('Accessing Google Drive requires authentication. Click'
101 + ' ok to proceed.'),
102 buttons: {
103 'ok': { click : doAuthorize },
104 'cancel': { click : that.gapi_ready.reject }
105 }
106 }
107 dialog.modal(options);
108 };
109
110 /**
111 * Gets the Google Drive folder ID corresponding to a path. Since
112 * the Google Drive API doesn't expose a path structure, it is necessary
113 * to manually walk the path from root.
114 */
115 ContentManager.prototype.get_id_for_path = function(path, onSuccess, onFailure) {
116 // Use recursive strategy, with helper function
117 // get_id_for_relative_path.
118
119 // calls callbacks with the id for the sepcified path, treated as
120 // a relative path with base given by base_id.
121 function get_id_for_relative_path(base_id, path_components) {
122 if (path_components.length == 0) {
123 onSuccess(base_id);
124 return;
125 }
126
127 var this_component = path_components.pop();
128
129 // Treat the empty string as a special case, and ignore it.
130 // This will result in ignoring leading and trailing slashes.
131 if (this_component == "") {
132 get_id_for_relative_path(base_id, path_components);
133 return;
134 }
135
136 var query = ('mimeType = \'' + FOLDER_MIME_TYPE + '\''
137 + ' and title = \'' + this_component + '\'');
138 var request = gapi.client.drive.children.list({
139 'folderId': base_id,
140 'q': query
141 });
142 request.execute(function(response) {
143 if (!response || response['error']) {
144 onFailure(response ? response['error'] : null);
145 return;
146 }
147
148 var child_folders = response['items'];
149 if (!child_folders) {
150 // 'directory does not exist' error.
151 onFailure();
152 return;
153 }
154
155 if (child_folders.length > 1) {
156 // 'runtime error' this should not happen
157 onFailure();
158 return;
159 }
160
161 get_id_for_relative_path(child_folders[0]['id'],
162 path_components);
163 });
164 };
165 get_id_for_relative_path('root', path.split('/').reverse());
166 }
167
168
169 /**
170 * Notebook Functions
171 */
172
173 /**
174 * Creates a new notebook file at the specified path, and
175 * opens that notebook in a new window.
176 *
177 * @method scroll_to_cell
178 * @param {String} path The path to create the new notebook at
179 */
180 ContentManager.prototype.new_notebook = function(path) {
181 var base_url = this.base_url;
182 var settings = {
183 processData : false,
184 cache : false,
185 type : "POST",
186 dataType : "json",
187 async : false,
188 success : function (data, status, xhr){
189 var notebook_name = data.name;
190 window.open(
191 utils.url_join_encode(
192 base_url,
193 'notebooks',
194 path,
195 notebook_name
196 ),
197 '_blank'
198 );
199 },
200 error : function(xhr, status, error) {
201 utils.log_ajax_error(xhr, status, error);
202 var msg;
203 if (xhr.responseJSON && xhr.responseJSON.message) {
204 msg = xhr.responseJSON.message;
205 } else {
206 msg = xhr.statusText;
207 }
208 dialog.modal({
209 title : 'Creating Notebook Failed',
210 body : "The error was: " + msg,
211 buttons : {'OK' : {'class' : 'btn-primary'}}
212 });
213 }
214 };
215 var url = utils.url_join_encode(
216 base_url,
217 'api/notebooks',
218 path
219 );
220 $.ajax(url,settings);
221 };
222
223 ContentManager.prototype.delete_notebook = function(name, path) {
224 var settings = {
225 processData : false,
226 cache : false,
227 type : "DELETE",
228 dataType : "json",
229 success : $.proxy(this.events.trigger, this.events,
230 'notebook_deleted.ContentManager',
231 {
232 name: name,
233 path: path
234 }),
235 error : utils.log_ajax_error
236 };
237 var url = utils.url_join_encode(
238 this.base_url,
239 'api/notebooks',
240 path,
241 name
242 );
243 $.ajax(url, settings);
244 };
245
246 ContentManager.prototype.rename_notebook = function(path, name, new_name) {
247 var that = this;
248 var data = {name: new_name};
249 var settings = {
250 processData : false,
251 cache : false,
252 type : "PATCH",
253 data : JSON.stringify(data),
254 dataType: "json",
255 headers : {'Content-Type': 'application/json'},
256 success : function (json, status, xhr) {
257 that.events.trigger('notebook_rename_success.ContentManager',
258 json);
259 },
260 error : function (xhr, status, error) {
261 that.events.trigger('notebook_rename_error.ContentManager',
262 [xhr, status, error]);
263 }
264 }
265 var url = utils.url_join_encode(
266 this.base_url,
267 'api/notebooks',
268 path,
269 name
270 );
271 $.ajax(url, settings);
272 };
273
274 ContentManager.prototype.save_notebook = function(path, name, content,
275 extra_settings) {
276 var that = notebook;
277 // Create a JSON model to be sent to the server.
278 var model = {
279 name : name,
280 path : path,
281 content : content
282 };
283 // time the ajax call for autosave tuning purposes.
284 var start = new Date().getTime();
285 // We do the call with settings so we can set cache to false.
286 var settings = {
287 processData : false,
288 cache : false,
289 type : "PUT",
290 data : JSON.stringify(model),
291 headers : {'Content-Type': 'application/json'},
292 success : $.proxy(this.events.trigger, this.events,
293 'notebook_save_success.ContentManager',
294 $.extend(model, { start : start })),
295 error : function (xhr, status, error) {
296 that.events.trigger('notebook_save_error.ContentManager',
297 [xhr, status, error, model]);
298 }
299 };
300 if (extra_settings) {
301 for (var key in extra_settings) {
302 settings[key] = extra_settings[key];
303 }
304 }
305 var url = utils.url_join_encode(
306 this.base_url,
307 'api/notebooks',
308 path,
309 name
310 );
311 $.ajax(url, settings);
312 };
313
314 /**
315 * Checkpointing Functions
316 */
317
318 ContentManager.prototype.save_checkpoint = function() {
319 // This is not necessary - integrated into save
320 };
321
322 ContentManager.prototype.restore_checkpoint = function(notebook, id) {
323 that = notebook;
324 this.events.trigger('notebook_restoring.Notebook', checkpoint);
325 var url = utils.url_join_encode(
326 this.base_url,
327 'api/notebooks',
328 this.notebook_path,
329 this.notebook_name,
330 'checkpoints',
331 checkpoint
332 );
333 $.post(url).done(
334 $.proxy(that.restore_checkpoint_success, that)
335 ).fail(
336 $.proxy(that.restore_checkpoint_error, that)
337 );
338 };
339
340 ContentManager.prototype.list_checkpoints = function(notebook) {
341 that = notebook;
342 var url = utils.url_join_encode(
343 that.base_url,
344 'api/notebooks',
345 that.notebook_path,
346 that.notebook_name,
347 'checkpoints'
348 );
349 $.get(url).done(
350 $.proxy(that.list_checkpoints_success, that)
351 ).fail(
352 $.proxy(that.list_checkpoints_error, that)
353 );
354 };
355
356 /**
357 * File management functions
358 */
359
360 /**
361 * List notebooks and directories at a given path
362 *
363 * On success, load_callback is called with an array of dictionaries
364 * representing individual files or directories. Each dictionary has
365 * the keys:
366 * type: "notebook" or "directory"
367 * name: the name of the file or directory
368 * created: created date
369 * last_modified: last modified dat
370 * path: the path
371 * @method list_notebooks
372 * @param {String} path The path to list notebooks in
373 * @param {Function} load_callback called with list of notebooks on success
374 * @param {Function} error_callback called with ajax results on error
375 */
376 ContentManager.prototype.list_contents = function(path, load_callback,
377 error_callback) {
378 var that = this;
379 this.gapi_ready.done(function() {
380 that.get_id_for_path(path, function(folder_id) {
381 query = ('(fileExtension = \'ipynb\' or'
382 + ' mimeType = \'' + FOLDER_MIME_TYPE + '\')'
383 + ' and \'' + folder_id + '\' in parents');
384 var request = gapi.client.drive.files.list({
385 'maxResults' : 1000,
386 'q' : query
387 });
388 request.execute(function(response) {
389 // On a drive API error, call error_callback
390 if (!response || response['error']) {
391 error_callback(response ? response['error'] : null);
392 return;
393 }
394
395 // Convert this list to the format that is passed to
396 // load_callback. Note that a files resource can represent
397 // a file or a directory.
398 // TODO: check that date formats are the same, and either
399 // convert to the IPython format, or document the difference.
400 var list = $.map(response['items'], function(files_resource) {
401 var type = files_resource['mimeType'] == FOLDER_MIME_TYPE ? 'directory' : 'notebook';
402 return {
403 type: type,
404 name: files_resource['title'],
405 path: path,
406 created: files_resource['createdDate'],
407 last_modified: files_resource['modifiedDate']
408 };
409 });
410 load_callback(list);
411 });
412 }, error_callback);
413 });
414 };
415
416
417 IPython.ContentManager = ContentManager;
418
419 return {'ContentManager': ContentManager};
420 });
General Comments 0
You need to be logged in to leave comments. Login now