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