##// END OF EJS Templates
DEV: Tweaks from PR feedback....
Scott Sanderson -
Show More
@@ -1,318 +1,322 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
29 29 def validate_model(model, expect_content):
30 30 """
31 31 Validate a model returned by a ContentsManager method.
32 32
33 33 If expect_content is True, then we expect non-null entries for 'content'
34 34 and 'format'.
35 35 """
36 36 required_keys = {
37 "name"
38 , "path"
39 , "type"
40 , "writable"
41 , "created"
42 # Note: This key is specified as just 'modified' in IPEP-27
43 , "last_modified"
44 , "mimetype"
45 , "content"
46 , "format"
37 "name",
38 "path",
39 "type",
40 "writable",
41 "created",
42 "last_modified",
43 "mimetype",
44 "content",
45 "format",
47 46 }
48 47 missing = required_keys - set(model.keys())
49 48 if missing:
50 49 raise web.HTTPError(
51 50 500,
52 51 u"Missing Model Keys: {missing}".format(missing=missing),
53 52 )
54 53
55 # Note: Per IPEP-27, 'mimetype' should be present in this list.
56 54 maybe_none_keys = ['content', 'format']
55 if model['type'] == 'file':
56 # mimetype should be populated only for file models
57 maybe_none_keys.append('mimetype')
57 58 if expect_content:
58 59 errors = [key for key in maybe_none_keys if model[key] is None]
59 60 if errors:
60 61 raise web.HTTPError(
61 62 500,
62 63 u"Keys unexpectedly None: {keys}".format(keys=errors),
63 64 )
64
65 65 else:
66 errors = [key for key in maybe_none_keys if model[key] is not None]
66 errors = {
67 key: model[key]
68 for key in maybe_none_keys
69 if model[key] is not None
70 }
67 71 if errors:
68 72 raise web.HTTPError(
69 73 500,
70 74 u"Keys unexpectedly not None: {keys}".format(keys=errors),
71 75 )
72 76
73 77
74 78 class ContentsHandler(IPythonHandler):
75 79
76 80 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
77 81
78 82 def location_url(self, path):
79 83 """Return the full URL location of a file.
80 84
81 85 Parameters
82 86 ----------
83 87 path : unicode
84 88 The API path of the file, such as "foo/bar.txt".
85 89 """
86 90 return url_escape(url_path_join(
87 91 self.base_url, 'api', 'contents', path
88 92 ))
89 93
90 94 def _finish_model(self, model, location=True):
91 95 """Finish a JSON request with a model, setting relevant headers, etc."""
92 96 if location:
93 97 location = self.location_url(model['path'])
94 98 self.set_header('Location', location)
95 99 self.set_header('Last-Modified', model['last_modified'])
96 100 self.set_header('Content-Type', 'application/json')
97 101 self.finish(json.dumps(model, default=date_default))
98 102
99 103 @web.authenticated
100 104 @json_errors
101 105 def get(self, path=''):
102 106 """Return a model for a file or directory.
103 107
104 108 A directory model contains a list of models (without content)
105 109 of the files and directories it contains.
106 110 """
107 111 path = path or ''
108 112 type_ = self.get_query_argument('type', default=None)
109 113 if type_ not in {None, 'directory', 'file', 'notebook'}:
110 114 raise web.HTTPError(400, u'Type %r is invalid' % type_)
111 115
112 116 format = self.get_query_argument('format', default=None)
113 117 if format not in {None, 'text', 'base64'}:
114 118 raise web.HTTPError(400, u'Format %r is invalid' % format)
115 119
116 120 model = self.contents_manager.get(path=path, type_=type_, format=format)
117 121 if model['type'] == 'directory':
118 122 # group listing by type, then by name (case-insensitive)
119 123 # FIXME: sorting should be done in the frontends
120 124 model['content'].sort(key=sort_key)
121 125 validate_model(model, expect_content=True)
122 126 self._finish_model(model, location=False)
123 127
124 128 @web.authenticated
125 129 @json_errors
126 130 def patch(self, path=''):
127 131 """PATCH renames a file or directory without re-uploading content."""
128 132 cm = self.contents_manager
129 133 model = self.get_json_body()
130 134 if model is None:
131 135 raise web.HTTPError(400, u'JSON body missing')
132 136 model = cm.update(model, path)
133 137 validate_model(model, expect_content=False)
134 138 self._finish_model(model)
135 139
136 140 def _copy(self, copy_from, copy_to=None):
137 141 """Copy a file, optionally specifying a target directory."""
138 142 self.log.info(u"Copying {copy_from} to {copy_to}".format(
139 143 copy_from=copy_from,
140 144 copy_to=copy_to or '',
141 145 ))
142 146 model = self.contents_manager.copy(copy_from, copy_to)
143 147 self.set_status(201)
144 148 validate_model(model, expect_content=False)
145 149 self._finish_model(model)
146 150
147 151 def _upload(self, model, path):
148 152 """Handle upload of a new file to path"""
149 153 self.log.info(u"Uploading file to %s", path)
150 154 model = self.contents_manager.new(model, path)
151 155 self.set_status(201)
152 156 validate_model(model, expect_content=False)
153 157 self._finish_model(model)
154 158
155 159 def _new_untitled(self, path, type='', ext=''):
156 160 """Create a new, empty untitled entity"""
157 161 self.log.info(u"Creating new %s in %s", type or 'file', path)
158 162 model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)
159 163 self.set_status(201)
160 164 validate_model(model, expect_content=False)
161 165 self._finish_model(model)
162 166
163 167 def _save(self, model, path):
164 168 """Save an existing file."""
165 169 self.log.info(u"Saving file at %s", path)
166 170 model = self.contents_manager.save(model, path)
167 171 validate_model(model, expect_content=False)
168 172 self._finish_model(model)
169 173
170 174 @web.authenticated
171 175 @json_errors
172 176 def post(self, path=''):
173 177 """Create a new file in the specified path.
174 178
175 179 POST creates new files. The server always decides on the name.
176 180
177 181 POST /api/contents/path
178 182 New untitled, empty file or directory.
179 183 POST /api/contents/path
180 184 with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
181 185 New copy of OtherNotebook in path
182 186 """
183 187
184 188 cm = self.contents_manager
185 189
186 190 if cm.file_exists(path):
187 191 raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
188 192
189 193 if not cm.dir_exists(path):
190 194 raise web.HTTPError(404, "No such directory: %s" % path)
191 195
192 196 model = self.get_json_body()
193 197
194 198 if model is not None:
195 199 copy_from = model.get('copy_from')
196 200 ext = model.get('ext', '')
197 201 type = model.get('type', '')
198 202 if copy_from:
199 203 self._copy(copy_from, path)
200 204 else:
201 205 self._new_untitled(path, type=type, ext=ext)
202 206 else:
203 207 self._new_untitled(path)
204 208
205 209 @web.authenticated
206 210 @json_errors
207 211 def put(self, path=''):
208 212 """Saves the file in the location specified by name and path.
209 213
210 214 PUT is very similar to POST, but the requester specifies the name,
211 215 whereas with POST, the server picks the name.
212 216
213 217 PUT /api/contents/path/Name.ipynb
214 218 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
215 219 in `content` key of JSON request body. If content is not specified,
216 220 create a new empty notebook.
217 221 """
218 222 model = self.get_json_body()
219 223 if model:
220 224 if model.get('copy_from'):
221 225 raise web.HTTPError(400, "Cannot copy with PUT, only POST")
222 226 if self.contents_manager.file_exists(path):
223 227 self._save(model, path)
224 228 else:
225 229 self._upload(model, path)
226 230 else:
227 231 self._new_untitled(path)
228 232
229 233 @web.authenticated
230 234 @json_errors
231 235 def delete(self, path=''):
232 236 """delete a file in the given path"""
233 237 cm = self.contents_manager
234 238 self.log.warn('delete %s', path)
235 239 cm.delete(path)
236 240 self.set_status(204)
237 241 self.finish()
238 242
239 243
240 244 class CheckpointsHandler(IPythonHandler):
241 245
242 246 SUPPORTED_METHODS = ('GET', 'POST')
243 247
244 248 @web.authenticated
245 249 @json_errors
246 250 def get(self, path=''):
247 251 """get lists checkpoints for a file"""
248 252 cm = self.contents_manager
249 253 checkpoints = cm.list_checkpoints(path)
250 254 data = json.dumps(checkpoints, default=date_default)
251 255 self.finish(data)
252 256
253 257 @web.authenticated
254 258 @json_errors
255 259 def post(self, path=''):
256 260 """post creates a new checkpoint"""
257 261 cm = self.contents_manager
258 262 checkpoint = cm.create_checkpoint(path)
259 263 data = json.dumps(checkpoint, default=date_default)
260 264 location = url_path_join(self.base_url, 'api/contents',
261 265 path, 'checkpoints', checkpoint['id'])
262 266 self.set_header('Location', url_escape(location))
263 267 self.set_status(201)
264 268 self.finish(data)
265 269
266 270
267 271 class ModifyCheckpointsHandler(IPythonHandler):
268 272
269 273 SUPPORTED_METHODS = ('POST', 'DELETE')
270 274
271 275 @web.authenticated
272 276 @json_errors
273 277 def post(self, path, checkpoint_id):
274 278 """post restores a file from a checkpoint"""
275 279 cm = self.contents_manager
276 280 cm.restore_checkpoint(checkpoint_id, path)
277 281 self.set_status(204)
278 282 self.finish()
279 283
280 284 @web.authenticated
281 285 @json_errors
282 286 def delete(self, path, checkpoint_id):
283 287 """delete clears a checkpoint for a given file"""
284 288 cm = self.contents_manager
285 289 cm.delete_checkpoint(checkpoint_id, path)
286 290 self.set_status(204)
287 291 self.finish()
288 292
289 293
290 294 class NotebooksRedirectHandler(IPythonHandler):
291 295 """Redirect /api/notebooks to /api/contents"""
292 296 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
293 297
294 298 def get(self, path):
295 299 self.log.warn("/api/notebooks is deprecated, use /api/contents")
296 300 self.redirect(url_path_join(
297 301 self.base_url,
298 302 'api/contents',
299 303 path
300 304 ))
301 305
302 306 put = patch = post = delete = get
303 307
304 308
305 309 #-----------------------------------------------------------------------------
306 310 # URL to handler mappings
307 311 #-----------------------------------------------------------------------------
308 312
309 313
310 314 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
311 315
312 316 default_handlers = [
313 317 (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
314 318 (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
315 319 ModifyCheckpointsHandler),
316 320 (r"/api/contents%s" % path_regex, ContentsHandler),
317 321 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
318 322 ]
General Comments 0
You need to be logged in to leave comments. Login now