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