##// END OF EJS Templates
Minor cleanups in the contents API....
Scott Sanderson -
Show More
@@ -1,265 +1,265 b''
1 1 """Tornado handlers for the contents web service."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7
8 8 from tornado import web
9 9
10 10 from IPython.html.utils import url_path_join, url_escape
11 11 from IPython.utils.jsonutil import date_default
12 12
13 13 from IPython.html.base.handlers import (
14 14 IPythonHandler, json_errors, path_regex,
15 15 )
16 16
17 17
18 18 def sort_key(model):
19 19 """key function for case-insensitive sort by name and type"""
20 20 iname = model['name'].lower()
21 21 type_key = {
22 22 'directory' : '0',
23 23 'notebook' : '1',
24 24 'file' : '2',
25 25 }.get(model['type'], '9')
26 26 return u'%s%s' % (type_key, iname)
27 27
28 28 class ContentsHandler(IPythonHandler):
29 29
30 30 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
31 31
32 32 def location_url(self, path):
33 33 """Return the full URL location of a file.
34 34
35 35 Parameters
36 36 ----------
37 37 path : unicode
38 38 The API path of the file, such as "foo/bar.txt".
39 39 """
40 40 return url_escape(url_path_join(
41 41 self.base_url, 'api', 'contents', path
42 42 ))
43 43
44 44 def _finish_model(self, model, location=True):
45 45 """Finish a JSON request with a model, setting relevant headers, etc."""
46 46 if location:
47 47 location = self.location_url(model['path'])
48 48 self.set_header('Location', location)
49 49 self.set_header('Last-Modified', model['last_modified'])
50 50 self.finish(json.dumps(model, default=date_default))
51 51
52 52 @web.authenticated
53 53 @json_errors
54 54 def get(self, path=''):
55 55 """Return a model for a file or directory.
56 56
57 57 A directory model contains a list of models (without content)
58 58 of the files and directories it contains.
59 59 """
60 60 path = path or ''
61 61 type_ = self.get_query_argument('type', default=None)
62 62 if type_ not in {None, 'directory', 'file', 'notebook'}:
63 63 raise web.HTTPError(400, u'Type %r is invalid' % type_)
64 64
65 format = self.get_query_argument('format', default=None)#
65 format = self.get_query_argument('format', default=None)
66 66 if format not in {None, 'text', 'base64'}:
67 67 raise web.HTTPError(400, u'Format %r is invalid' % format)
68 68
69 69 model = self.contents_manager.get(path=path, type_=type_, format=format)
70 70 if model['type'] == 'directory':
71 71 # group listing by type, then by name (case-insensitive)
72 72 # FIXME: sorting should be done in the frontends
73 73 model['content'].sort(key=sort_key)
74 74 self._finish_model(model, location=False)
75 75
76 76 @web.authenticated
77 77 @json_errors
78 78 def patch(self, path=''):
79 79 """PATCH renames a file or directory without re-uploading content."""
80 80 cm = self.contents_manager
81 81 model = self.get_json_body()
82 82 if model is None:
83 83 raise web.HTTPError(400, u'JSON body missing')
84 84 model = cm.update(model, path)
85 85 self._finish_model(model)
86 86
87 87 def _copy(self, copy_from, copy_to=None):
88 88 """Copy a file, optionally specifying a target directory."""
89 89 self.log.info(u"Copying {copy_from} to {copy_to}".format(
90 90 copy_from=copy_from,
91 91 copy_to=copy_to or '',
92 92 ))
93 93 model = self.contents_manager.copy(copy_from, copy_to)
94 94 self.set_status(201)
95 95 self._finish_model(model)
96 96
97 97 def _upload(self, model, path):
98 98 """Handle upload of a new file to path"""
99 99 self.log.info(u"Uploading file to %s", path)
100 100 model = self.contents_manager.new(model, path)
101 101 self.set_status(201)
102 102 self._finish_model(model)
103 103
104 104 def _new_untitled(self, path, type='', ext=''):
105 105 """Create a new, empty untitled entity"""
106 106 self.log.info(u"Creating new %s in %s", type or 'file', path)
107 107 model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)
108 108 self.set_status(201)
109 109 self._finish_model(model)
110 110
111 111 def _save(self, model, path):
112 112 """Save an existing file."""
113 113 self.log.info(u"Saving file at %s", path)
114 114 model = self.contents_manager.save(model, path)
115 115 self._finish_model(model)
116 116
117 117 @web.authenticated
118 118 @json_errors
119 119 def post(self, path=''):
120 120 """Create a new file in the specified path.
121 121
122 122 POST creates new files. The server always decides on the name.
123 123
124 124 POST /api/contents/path
125 125 New untitled, empty file or directory.
126 126 POST /api/contents/path
127 127 with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
128 128 New copy of OtherNotebook in path
129 129 """
130 130
131 131 cm = self.contents_manager
132 132
133 133 if cm.file_exists(path):
134 134 raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
135 135
136 136 if not cm.dir_exists(path):
137 137 raise web.HTTPError(404, "No such directory: %s" % path)
138 138
139 139 model = self.get_json_body()
140 140
141 141 if model is not None:
142 142 copy_from = model.get('copy_from')
143 143 ext = model.get('ext', '')
144 144 type = model.get('type', '')
145 145 if copy_from:
146 146 self._copy(copy_from, path)
147 147 else:
148 148 self._new_untitled(path, type=type, ext=ext)
149 149 else:
150 150 self._new_untitled(path)
151 151
152 152 @web.authenticated
153 153 @json_errors
154 154 def put(self, path=''):
155 155 """Saves the file in the location specified by name and path.
156 156
157 157 PUT is very similar to POST, but the requester specifies the name,
158 158 whereas with POST, the server picks the name.
159 159
160 160 PUT /api/contents/path/Name.ipynb
161 161 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
162 162 in `content` key of JSON request body. If content is not specified,
163 163 create a new empty notebook.
164 164 """
165 165 model = self.get_json_body()
166 166 if model:
167 167 if model.get('copy_from'):
168 168 raise web.HTTPError(400, "Cannot copy with PUT, only POST")
169 169 if self.contents_manager.file_exists(path):
170 170 self._save(model, path)
171 171 else:
172 172 self._upload(model, path)
173 173 else:
174 174 self._new_untitled(path)
175 175
176 176 @web.authenticated
177 177 @json_errors
178 178 def delete(self, path=''):
179 179 """delete a file in the given path"""
180 180 cm = self.contents_manager
181 181 self.log.warn('delete %s', path)
182 182 cm.delete(path)
183 183 self.set_status(204)
184 184 self.finish()
185 185
186 186
187 187 class CheckpointsHandler(IPythonHandler):
188 188
189 189 SUPPORTED_METHODS = ('GET', 'POST')
190 190
191 191 @web.authenticated
192 192 @json_errors
193 193 def get(self, path=''):
194 194 """get lists checkpoints for a file"""
195 195 cm = self.contents_manager
196 196 checkpoints = cm.list_checkpoints(path)
197 197 data = json.dumps(checkpoints, default=date_default)
198 198 self.finish(data)
199 199
200 200 @web.authenticated
201 201 @json_errors
202 202 def post(self, path=''):
203 203 """post creates a new checkpoint"""
204 204 cm = self.contents_manager
205 205 checkpoint = cm.create_checkpoint(path)
206 206 data = json.dumps(checkpoint, default=date_default)
207 207 location = url_path_join(self.base_url, 'api/contents',
208 208 path, 'checkpoints', checkpoint['id'])
209 209 self.set_header('Location', url_escape(location))
210 210 self.set_status(201)
211 211 self.finish(data)
212 212
213 213
214 214 class ModifyCheckpointsHandler(IPythonHandler):
215 215
216 216 SUPPORTED_METHODS = ('POST', 'DELETE')
217 217
218 218 @web.authenticated
219 219 @json_errors
220 220 def post(self, path, checkpoint_id):
221 221 """post restores a file from a checkpoint"""
222 222 cm = self.contents_manager
223 223 cm.restore_checkpoint(checkpoint_id, path)
224 224 self.set_status(204)
225 225 self.finish()
226 226
227 227 @web.authenticated
228 228 @json_errors
229 229 def delete(self, path, checkpoint_id):
230 230 """delete clears a checkpoint for a given file"""
231 231 cm = self.contents_manager
232 232 cm.delete_checkpoint(checkpoint_id, path)
233 233 self.set_status(204)
234 234 self.finish()
235 235
236 236
237 237 class NotebooksRedirectHandler(IPythonHandler):
238 238 """Redirect /api/notebooks to /api/contents"""
239 239 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
240 240
241 241 def get(self, path):
242 242 self.log.warn("/api/notebooks is deprecated, use /api/contents")
243 243 self.redirect(url_path_join(
244 244 self.base_url,
245 245 'api/contents',
246 246 path
247 247 ))
248 248
249 249 put = patch = post = delete = get
250 250
251 251
252 252 #-----------------------------------------------------------------------------
253 253 # URL to handler mappings
254 254 #-----------------------------------------------------------------------------
255 255
256 256
257 257 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
258 258
259 259 default_handlers = [
260 260 (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
261 261 (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
262 262 ModifyCheckpointsHandler),
263 263 (r"/api/contents%s" % path_regex, ContentsHandler),
264 264 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
265 265 ]
@@ -1,67 +1,68 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'jquery',
6 6 'base/js/utils',
7 7 ],
8 8 function($, utils) {
9 "use strict";
9 10 var ConfigSection = function(section_name, options) {
10 11 this.section_name = section_name;
11 12 this.base_url = options.base_url;
12 13 this.data = {};
13 14
14 15 var that = this;
15 16
16 17 /* .loaded is a promise, fulfilled the first time the config is loaded
17 18 * from the server. Code can do:
18 19 * conf.loaded.then(function() { ... using conf.data ... });
19 20 */
20 21 this._one_load_finished = false;
21 22 this.loaded = new Promise(function(resolve, reject) {
22 23 that._finish_firstload = resolve;
23 24 });
24 25 };
25 26
26 27 ConfigSection.prototype.api_url = function() {
27 28 return utils.url_join_encode(this.base_url, 'api/config', this.section_name);
28 29 };
29 30
30 31 ConfigSection.prototype._load_done = function() {
31 32 if (!this._one_load_finished) {
32 33 this._one_load_finished = true;
33 34 this._finish_firstload();
34 35 }
35 36 };
36 37
37 38 ConfigSection.prototype.load = function() {
38 39 var that = this;
39 40 return utils.promising_ajax(this.api_url(), {
40 41 cache: false,
41 42 type: "GET",
42 43 dataType: "json",
43 44 }).then(function(data) {
44 45 that.data = data;
45 46 that._load_done();
46 47 return data;
47 48 });
48 49 };
49 50
50 51 ConfigSection.prototype.update = function(newdata) {
51 52 var that = this;
52 53 return utils.promising_ajax(this.api_url(), {
53 54 processData: false,
54 55 type : "PATCH",
55 56 data: JSON.stringify(newdata),
56 57 dataType : "json",
57 58 contentType: 'application/json',
58 59 }).then(function(data) {
59 60 that.data = data;
60 61 that._load_done();
61 62 return data;
62 63 });
63 64 };
64 65
65 66 return {ConfigSection: ConfigSection};
66 67
67 68 });
@@ -1,250 +1,251 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 ], function(IPython, $, utils) {
9 "use strict";
9 10 var Contents = function(options) {
10 11 /**
11 12 * Constructor
12 13 *
13 14 * A contents handles passing file operations
14 15 * to the back-end. This includes checkpointing
15 16 * with the normal file operations.
16 17 *
17 18 * Parameters:
18 19 * options: dictionary
19 20 * Dictionary of keyword arguments.
20 21 * base_url: string
21 22 */
22 23 this.base_url = options.base_url;
23 24 };
24 25
25 26 /** Error type */
26 27 Contents.DIRECTORY_NOT_EMPTY_ERROR = 'DirectoryNotEmptyError';
27 28
28 29 Contents.DirectoryNotEmptyError = function() {
29 30 // Constructor
30 31 //
31 32 // An error representing the result of attempting to delete a non-empty
32 33 // directory.
33 34 this.message = 'A directory must be empty before being deleted.';
34 35 };
35 36
36 37 Contents.DirectoryNotEmptyError.prototype = Object.create(Error.prototype);
37 38 Contents.DirectoryNotEmptyError.prototype.name =
38 39 Contents.DIRECTORY_NOT_EMPTY_ERROR;
39 40
40 41
41 42 Contents.prototype.api_url = function() {
42 43 var url_parts = [this.base_url, 'api/contents'].concat(
43 44 Array.prototype.slice.apply(arguments));
44 45 return utils.url_join_encode.apply(null, url_parts);
45 46 };
46 47
47 48 /**
48 49 * Creates a basic error handler that wraps a jqXHR error as an Error.
49 50 *
50 51 * Takes a callback that accepts an Error, and returns a callback that can
51 52 * be passed directly to $.ajax, which will wrap the error from jQuery
52 53 * as an Error, and pass that to the original callback.
53 54 *
54 55 * @method create_basic_error_handler
55 56 * @param{Function} callback
56 57 * @return{Function}
57 58 */
58 59 Contents.prototype.create_basic_error_handler = function(callback) {
59 60 if (!callback) {
60 61 return utils.log_ajax_error;
61 62 }
62 63 return function(xhr, status, error) {
63 64 callback(utils.wrap_ajax_error(xhr, status, error));
64 65 };
65 66 };
66 67
67 68 /**
68 69 * File Functions (including notebook operations)
69 70 */
70 71
71 72 /**
72 73 * Get a file.
73 74 *
74 75 * Calls success with file JSON model, or error with error.
75 76 *
76 77 * @method get
77 78 * @param {String} path
78 79 * @param {Object} options
79 80 * type : 'notebook', 'file', or 'directory'
80 81 * format: 'text' or 'base64'; only relevant for type: 'file'
81 82 */
82 83 Contents.prototype.get = function (path, options) {
83 84 /**
84 85 * We do the call with settings so we can set cache to false.
85 86 */
86 87 var settings = {
87 88 processData : false,
88 89 cache : false,
89 90 type : "GET",
90 91 dataType : "json",
91 92 };
92 93 var url = this.api_url(path);
93 params = {};
94 var params = {};
94 95 if (options.type) { params.type = options.type; }
95 96 if (options.format) { params.format = options.format; }
96 97 return utils.promising_ajax(url + '?' + $.param(params), settings);
97 98 };
98 99
99 100
100 101 /**
101 102 * Creates a new untitled file or directory in the specified directory path.
102 103 *
103 104 * @method new
104 105 * @param {String} path: the directory in which to create the new file/directory
105 106 * @param {Object} options:
106 107 * ext: file extension to use
107 108 * type: model type to create ('notebook', 'file', or 'directory')
108 109 */
109 110 Contents.prototype.new_untitled = function(path, options) {
110 111 var data = JSON.stringify({
111 112 ext: options.ext,
112 113 type: options.type
113 114 });
114 115
115 116 var settings = {
116 117 processData : false,
117 118 type : "POST",
118 119 data: data,
119 120 dataType : "json",
120 121 };
121 122 return utils.promising_ajax(this.api_url(path), settings);
122 123 };
123 124
124 125 Contents.prototype.delete = function(path) {
125 126 var settings = {
126 127 processData : false,
127 128 type : "DELETE",
128 129 dataType : "json",
129 130 };
130 131 var url = this.api_url(path);
131 132 return utils.promising_ajax(url, settings).catch(
132 133 // Translate certain errors to more specific ones.
133 134 function(error) {
134 135 // TODO: update IPEP27 to specify errors more precisely, so
135 136 // that error types can be detected here with certainty.
136 137 if (error.xhr.status === 400) {
137 138 throw new Contents.DirectoryNotEmptyError();
138 139 }
139 140 throw error;
140 141 }
141 142 );
142 143 };
143 144
144 145 Contents.prototype.rename = function(path, new_path) {
145 146 var data = {path: new_path};
146 147 var settings = {
147 148 processData : false,
148 149 type : "PATCH",
149 150 data : JSON.stringify(data),
150 151 dataType: "json",
151 152 contentType: 'application/json',
152 153 };
153 154 var url = this.api_url(path);
154 155 return utils.promising_ajax(url, settings);
155 156 };
156 157
157 158 Contents.prototype.save = function(path, model) {
158 159 /**
159 160 * We do the call with settings so we can set cache to false.
160 161 */
161 162 var settings = {
162 163 processData : false,
163 164 type : "PUT",
164 165 data : JSON.stringify(model),
165 166 contentType: 'application/json',
166 167 };
167 168 var url = this.api_url(path);
168 169 return utils.promising_ajax(url, settings);
169 170 };
170 171
171 172 Contents.prototype.copy = function(from_file, to_dir) {
172 173 /**
173 174 * Copy a file into a given directory via POST
174 175 * The server will select the name of the copied file
175 176 */
176 177 var url = this.api_url(to_dir);
177 178
178 179 var settings = {
179 180 processData : false,
180 181 type: "POST",
181 182 data: JSON.stringify({copy_from: from_file}),
182 183 dataType : "json",
183 184 };
184 185 return utils.promising_ajax(url, settings);
185 186 };
186 187
187 188 /**
188 189 * Checkpointing Functions
189 190 */
190 191
191 192 Contents.prototype.create_checkpoint = function(path) {
192 193 var url = this.api_url(path, 'checkpoints');
193 194 var settings = {
194 195 type : "POST",
195 196 dataType : "json",
196 197 };
197 198 return utils.promising_ajax(url, settings);
198 199 };
199 200
200 201 Contents.prototype.list_checkpoints = function(path) {
201 202 var url = this.api_url(path, 'checkpoints');
202 203 var settings = {
203 204 type : "GET",
204 205 cache: false,
205 206 dataType: "json",
206 207 };
207 208 return utils.promising_ajax(url, settings);
208 209 };
209 210
210 211 Contents.prototype.restore_checkpoint = function(path, checkpoint_id) {
211 212 var url = this.api_url(path, 'checkpoints', checkpoint_id);
212 213 var settings = {
213 214 type : "POST",
214 215 };
215 216 return utils.promising_ajax(url, settings);
216 217 };
217 218
218 219 Contents.prototype.delete_checkpoint = function(path, checkpoint_id) {
219 220 var url = this.api_url(path, 'checkpoints', checkpoint_id);
220 221 var settings = {
221 222 type : "DELETE",
222 223 };
223 224 return utils.promising_ajax(url, settings);
224 225 };
225 226
226 227 /**
227 228 * File management functions
228 229 */
229 230
230 231 /**
231 232 * List notebooks and directories at a given path
232 233 *
233 234 * On success, load_callback is called with an array of dictionaries
234 235 * representing individual files or directories. Each dictionary has
235 236 * the keys:
236 237 * type: "notebook" or "directory"
237 238 * created: created date
238 239 * last_modified: last modified dat
239 240 * @method list_notebooks
240 241 * @param {String} path The path to list notebooks in
241 242 */
242 243 Contents.prototype.list_contents = function(path) {
243 244 return this.get(path, {type: 'directory'});
244 245 };
245 246
246 247
247 248 IPython.Contents = Contents;
248 249
249 250 return {'Contents': Contents};
250 251 });
General Comments 0
You need to be logged in to leave comments. Login now