##// END OF EJS Templates
redirect /api/notebooks to /api/contents...
MinRK -
Show More
@@ -1,286 +1,303 b''
1 """Tornado handlers for the contents web service."""
1 """Tornado handlers for the contents web service."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import json
6 import json
7
7
8 from tornado import web
8 from tornado import web
9
9
10 from IPython.html.utils import url_path_join, url_escape
10 from IPython.html.utils import url_path_join, url_escape
11 from IPython.utils.jsonutil import date_default
11 from IPython.utils.jsonutil import date_default
12
12
13 from IPython.html.base.handlers import (IPythonHandler, json_errors,
13 from IPython.html.base.handlers import (IPythonHandler, json_errors,
14 file_path_regex, path_regex,
14 file_path_regex, path_regex,
15 file_name_regex)
15 file_name_regex)
16
16
17
17
18 def sort_key(model):
18 def sort_key(model):
19 """key function for case-insensitive sort by name and type"""
19 """key function for case-insensitive sort by name and type"""
20 iname = model['name'].lower()
20 iname = model['name'].lower()
21 type_key = {
21 type_key = {
22 'directory' : '0',
22 'directory' : '0',
23 'notebook' : '1',
23 'notebook' : '1',
24 'file' : '2',
24 'file' : '2',
25 }.get(model['type'], '9')
25 }.get(model['type'], '9')
26 return u'%s%s' % (type_key, iname)
26 return u'%s%s' % (type_key, iname)
27
27
28 class ContentsHandler(IPythonHandler):
28 class ContentsHandler(IPythonHandler):
29
29
30 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
30 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
31
31
32 def location_url(self, name, path):
32 def location_url(self, name, path):
33 """Return the full URL location of a file.
33 """Return the full URL location of a file.
34
34
35 Parameters
35 Parameters
36 ----------
36 ----------
37 name : unicode
37 name : unicode
38 The base name of the file, such as "foo.ipynb".
38 The base name of the file, such as "foo.ipynb".
39 path : unicode
39 path : unicode
40 The API path of the file, such as "foo/bar".
40 The API path of the file, such as "foo/bar".
41 """
41 """
42 return url_escape(url_path_join(
42 return url_escape(url_path_join(
43 self.base_url, 'api', 'contents', path, name
43 self.base_url, 'api', 'contents', path, name
44 ))
44 ))
45
45
46 def _finish_model(self, model, location=True):
46 def _finish_model(self, model, location=True):
47 """Finish a JSON request with a model, setting relevant headers, etc."""
47 """Finish a JSON request with a model, setting relevant headers, etc."""
48 if location:
48 if location:
49 location = self.location_url(model['name'], model['path'])
49 location = self.location_url(model['name'], model['path'])
50 self.set_header('Location', location)
50 self.set_header('Location', location)
51 self.set_header('Last-Modified', model['last_modified'])
51 self.set_header('Last-Modified', model['last_modified'])
52 self.finish(json.dumps(model, default=date_default))
52 self.finish(json.dumps(model, default=date_default))
53
53
54 @web.authenticated
54 @web.authenticated
55 @json_errors
55 @json_errors
56 def get(self, path='', name=None):
56 def get(self, path='', name=None):
57 """Return a model for a file or directory.
57 """Return a model for a file or directory.
58
58
59 A directory model contains a list of models (without content)
59 A directory model contains a list of models (without content)
60 of the files and directories it contains.
60 of the files and directories it contains.
61 """
61 """
62 path = path or ''
62 path = path or ''
63 model = self.contents_manager.get_model(name=name, path=path)
63 model = self.contents_manager.get_model(name=name, path=path)
64 if model['type'] == 'directory':
64 if model['type'] == 'directory':
65 # group listing by type, then by name (case-insensitive)
65 # group listing by type, then by name (case-insensitive)
66 # FIXME: sorting should be done in the frontends
66 # FIXME: sorting should be done in the frontends
67 model['content'].sort(key=sort_key)
67 model['content'].sort(key=sort_key)
68 self._finish_model(model, location=False)
68 self._finish_model(model, location=False)
69
69
70 @web.authenticated
70 @web.authenticated
71 @json_errors
71 @json_errors
72 def patch(self, path='', name=None):
72 def patch(self, path='', name=None):
73 """PATCH renames a notebook without re-uploading content."""
73 """PATCH renames a notebook without re-uploading content."""
74 cm = self.contents_manager
74 cm = self.contents_manager
75 if name is None:
75 if name is None:
76 raise web.HTTPError(400, u'Filename missing')
76 raise web.HTTPError(400, u'Filename missing')
77 model = self.get_json_body()
77 model = self.get_json_body()
78 if model is None:
78 if model is None:
79 raise web.HTTPError(400, u'JSON body missing')
79 raise web.HTTPError(400, u'JSON body missing')
80 model = cm.update(model, name, path)
80 model = cm.update(model, name, path)
81 self._finish_model(model)
81 self._finish_model(model)
82
82
83 def _copy(self, copy_from, path, copy_to=None):
83 def _copy(self, copy_from, path, copy_to=None):
84 """Copy a file, optionally specifying the new name.
84 """Copy a file, optionally specifying the new name.
85 """
85 """
86 self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format(
86 self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format(
87 copy_from=copy_from,
87 copy_from=copy_from,
88 path=path,
88 path=path,
89 copy_to=copy_to or '',
89 copy_to=copy_to or '',
90 ))
90 ))
91 model = self.contents_manager.copy(copy_from, copy_to, path)
91 model = self.contents_manager.copy(copy_from, copy_to, path)
92 self.set_status(201)
92 self.set_status(201)
93 self._finish_model(model)
93 self._finish_model(model)
94
94
95 def _upload(self, model, path, name=None):
95 def _upload(self, model, path, name=None):
96 """Handle upload of a new file
96 """Handle upload of a new file
97
97
98 If name specified, create it in path/name,
98 If name specified, create it in path/name,
99 otherwise create a new untitled file in path.
99 otherwise create a new untitled file in path.
100 """
100 """
101 self.log.info(u"Uploading file to %s/%s", path, name or '')
101 self.log.info(u"Uploading file to %s/%s", path, name or '')
102 if name:
102 if name:
103 model['name'] = name
103 model['name'] = name
104
104
105 model = self.contents_manager.create_file(model, path)
105 model = self.contents_manager.create_file(model, path)
106 self.set_status(201)
106 self.set_status(201)
107 self._finish_model(model)
107 self._finish_model(model)
108
108
109 def _create_empty_file(self, path, name=None, ext='.ipynb'):
109 def _create_empty_file(self, path, name=None, ext='.ipynb'):
110 """Create an empty file in path
110 """Create an empty file in path
111
111
112 If name specified, create it in path/name.
112 If name specified, create it in path/name.
113 """
113 """
114 self.log.info(u"Creating new file in %s/%s", path, name or '')
114 self.log.info(u"Creating new file in %s/%s", path, name or '')
115 model = {}
115 model = {}
116 if name:
116 if name:
117 model['name'] = name
117 model['name'] = name
118 model = self.contents_manager.create_file(model, path=path, ext=ext)
118 model = self.contents_manager.create_file(model, path=path, ext=ext)
119 self.set_status(201)
119 self.set_status(201)
120 self._finish_model(model)
120 self._finish_model(model)
121
121
122 def _save(self, model, path, name):
122 def _save(self, model, path, name):
123 """Save an existing file."""
123 """Save an existing file."""
124 self.log.info(u"Saving file at %s/%s", path, name)
124 self.log.info(u"Saving file at %s/%s", path, name)
125 model = self.contents_manager.save(model, name, path)
125 model = self.contents_manager.save(model, name, path)
126 if model['path'] != path.strip('/') or model['name'] != name:
126 if model['path'] != path.strip('/') or model['name'] != name:
127 # a rename happened, set Location header
127 # a rename happened, set Location header
128 location = True
128 location = True
129 else:
129 else:
130 location = False
130 location = False
131 self._finish_model(model, location)
131 self._finish_model(model, location)
132
132
133 @web.authenticated
133 @web.authenticated
134 @json_errors
134 @json_errors
135 def post(self, path='', name=None):
135 def post(self, path='', name=None):
136 """Create a new file or directory in the specified path.
136 """Create a new file or directory in the specified path.
137
137
138 POST creates new files or directories. The server always decides on the name.
138 POST creates new files or directories. The server always decides on the name.
139
139
140 POST /api/contents/path
140 POST /api/contents/path
141 New untitled notebook in path. If content specified, upload a
141 New untitled notebook in path. If content specified, upload a
142 notebook, otherwise start empty.
142 notebook, otherwise start empty.
143 POST /api/contents/path
143 POST /api/contents/path
144 with body {"copy_from" : "OtherNotebook.ipynb"}
144 with body {"copy_from" : "OtherNotebook.ipynb"}
145 New copy of OtherNotebook in path
145 New copy of OtherNotebook in path
146 """
146 """
147
147
148 if name is not None:
148 if name is not None:
149 path = u'{}/{}'.format(path, name)
149 path = u'{}/{}'.format(path, name)
150
150
151 cm = self.contents_manager
151 cm = self.contents_manager
152
152
153 if cm.file_exists(path):
153 if cm.file_exists(path):
154 raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.")
154 raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.")
155
155
156 if not cm.path_exists(path):
156 if not cm.path_exists(path):
157 raise web.HTTPError(404, "No such directory: %s" % path)
157 raise web.HTTPError(404, "No such directory: %s" % path)
158
158
159 model = self.get_json_body()
159 model = self.get_json_body()
160
160
161 if model is not None:
161 if model is not None:
162 copy_from = model.get('copy_from')
162 copy_from = model.get('copy_from')
163 ext = model.get('ext', '.ipynb')
163 ext = model.get('ext', '.ipynb')
164 if model.get('content') is not None:
164 if model.get('content') is not None:
165 if copy_from:
165 if copy_from:
166 raise web.HTTPError(400, "Can't upload and copy at the same time.")
166 raise web.HTTPError(400, "Can't upload and copy at the same time.")
167 self._upload(model, path)
167 self._upload(model, path)
168 elif copy_from:
168 elif copy_from:
169 self._copy(copy_from, path)
169 self._copy(copy_from, path)
170 else:
170 else:
171 self._create_empty_file(path, ext=ext)
171 self._create_empty_file(path, ext=ext)
172 else:
172 else:
173 self._create_empty_file(path)
173 self._create_empty_file(path)
174
174
175 @web.authenticated
175 @web.authenticated
176 @json_errors
176 @json_errors
177 def put(self, path='', name=None):
177 def put(self, path='', name=None):
178 """Saves the file in the location specified by name and path.
178 """Saves the file in the location specified by name and path.
179
179
180 PUT is very similar to POST, but the requester specifies the name,
180 PUT is very similar to POST, but the requester specifies the name,
181 whereas with POST, the server picks the name.
181 whereas with POST, the server picks the name.
182
182
183 PUT /api/contents/path/Name.ipynb
183 PUT /api/contents/path/Name.ipynb
184 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
184 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
185 in `content` key of JSON request body. If content is not specified,
185 in `content` key of JSON request body. If content is not specified,
186 create a new empty notebook.
186 create a new empty notebook.
187 PUT /api/contents/path/Name.ipynb
187 PUT /api/contents/path/Name.ipynb
188 with JSON body::
188 with JSON body::
189
189
190 {
190 {
191 "copy_from" : "[path/to/]OtherNotebook.ipynb"
191 "copy_from" : "[path/to/]OtherNotebook.ipynb"
192 }
192 }
193
193
194 Copy OtherNotebook to Name
194 Copy OtherNotebook to Name
195 """
195 """
196 if name is None:
196 if name is None:
197 raise web.HTTPError(400, "name must be specified with PUT.")
197 raise web.HTTPError(400, "name must be specified with PUT.")
198
198
199 model = self.get_json_body()
199 model = self.get_json_body()
200 if model:
200 if model:
201 copy_from = model.get('copy_from')
201 copy_from = model.get('copy_from')
202 if copy_from:
202 if copy_from:
203 if model.get('content'):
203 if model.get('content'):
204 raise web.HTTPError(400, "Can't upload and copy at the same time.")
204 raise web.HTTPError(400, "Can't upload and copy at the same time.")
205 self._copy(copy_from, path, name)
205 self._copy(copy_from, path, name)
206 elif self.contents_manager.file_exists(name, path):
206 elif self.contents_manager.file_exists(name, path):
207 self._save(model, path, name)
207 self._save(model, path, name)
208 else:
208 else:
209 self._upload(model, path, name)
209 self._upload(model, path, name)
210 else:
210 else:
211 self._create_empty_file(path, name)
211 self._create_empty_file(path, name)
212
212
213 @web.authenticated
213 @web.authenticated
214 @json_errors
214 @json_errors
215 def delete(self, path='', name=None):
215 def delete(self, path='', name=None):
216 """delete a file in the given path"""
216 """delete a file in the given path"""
217 cm = self.contents_manager
217 cm = self.contents_manager
218 self.log.warn('delete %s:%s', path, name)
218 self.log.warn('delete %s:%s', path, name)
219 cm.delete(name, path)
219 cm.delete(name, path)
220 self.set_status(204)
220 self.set_status(204)
221 self.finish()
221 self.finish()
222
222
223
223
224 class CheckpointsHandler(IPythonHandler):
224 class CheckpointsHandler(IPythonHandler):
225
225
226 SUPPORTED_METHODS = ('GET', 'POST')
226 SUPPORTED_METHODS = ('GET', 'POST')
227
227
228 @web.authenticated
228 @web.authenticated
229 @json_errors
229 @json_errors
230 def get(self, path='', name=None):
230 def get(self, path='', name=None):
231 """get lists checkpoints for a file"""
231 """get lists checkpoints for a file"""
232 cm = self.contents_manager
232 cm = self.contents_manager
233 checkpoints = cm.list_checkpoints(name, path)
233 checkpoints = cm.list_checkpoints(name, path)
234 data = json.dumps(checkpoints, default=date_default)
234 data = json.dumps(checkpoints, default=date_default)
235 self.finish(data)
235 self.finish(data)
236
236
237 @web.authenticated
237 @web.authenticated
238 @json_errors
238 @json_errors
239 def post(self, path='', name=None):
239 def post(self, path='', name=None):
240 """post creates a new checkpoint"""
240 """post creates a new checkpoint"""
241 cm = self.contents_manager
241 cm = self.contents_manager
242 checkpoint = cm.create_checkpoint(name, path)
242 checkpoint = cm.create_checkpoint(name, path)
243 data = json.dumps(checkpoint, default=date_default)
243 data = json.dumps(checkpoint, default=date_default)
244 location = url_path_join(self.base_url, 'api/contents',
244 location = url_path_join(self.base_url, 'api/contents',
245 path, name, 'checkpoints', checkpoint['id'])
245 path, name, 'checkpoints', checkpoint['id'])
246 self.set_header('Location', url_escape(location))
246 self.set_header('Location', url_escape(location))
247 self.set_status(201)
247 self.set_status(201)
248 self.finish(data)
248 self.finish(data)
249
249
250
250
251 class ModifyCheckpointsHandler(IPythonHandler):
251 class ModifyCheckpointsHandler(IPythonHandler):
252
252
253 SUPPORTED_METHODS = ('POST', 'DELETE')
253 SUPPORTED_METHODS = ('POST', 'DELETE')
254
254
255 @web.authenticated
255 @web.authenticated
256 @json_errors
256 @json_errors
257 def post(self, path, name, checkpoint_id):
257 def post(self, path, name, checkpoint_id):
258 """post restores a file from a checkpoint"""
258 """post restores a file from a checkpoint"""
259 cm = self.contents_manager
259 cm = self.contents_manager
260 cm.restore_checkpoint(checkpoint_id, name, path)
260 cm.restore_checkpoint(checkpoint_id, name, path)
261 self.set_status(204)
261 self.set_status(204)
262 self.finish()
262 self.finish()
263
263
264 @web.authenticated
264 @web.authenticated
265 @json_errors
265 @json_errors
266 def delete(self, path, name, checkpoint_id):
266 def delete(self, path, name, checkpoint_id):
267 """delete clears a checkpoint for a given file"""
267 """delete clears a checkpoint for a given file"""
268 cm = self.contents_manager
268 cm = self.contents_manager
269 cm.delete_checkpoint(checkpoint_id, name, path)
269 cm.delete_checkpoint(checkpoint_id, name, path)
270 self.set_status(204)
270 self.set_status(204)
271 self.finish()
271 self.finish()
272
272
273
274 class NotebooksRedirectHandler(IPythonHandler):
275 """Redirect /api/notebooks to /api/contents"""
276 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
277
278 def get(self, path):
279 self.log.warn("/api/notebooks is deprecated, use /api/contents")
280 self.redirect(url_path_join(
281 self.base_url,
282 'api/contents',
283 path
284 ))
285
286 put = patch = post = delete = get
287
288
273 #-----------------------------------------------------------------------------
289 #-----------------------------------------------------------------------------
274 # URL to handler mappings
290 # URL to handler mappings
275 #-----------------------------------------------------------------------------
291 #-----------------------------------------------------------------------------
276
292
277
293
278 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
294 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
279
295
280 default_handlers = [
296 default_handlers = [
281 (r"/api/contents%s/checkpoints" % file_path_regex, CheckpointsHandler),
297 (r"/api/contents%s/checkpoints" % file_path_regex, CheckpointsHandler),
282 (r"/api/contents%s/checkpoints/%s" % (file_path_regex, _checkpoint_id_regex),
298 (r"/api/contents%s/checkpoints/%s" % (file_path_regex, _checkpoint_id_regex),
283 ModifyCheckpointsHandler),
299 ModifyCheckpointsHandler),
284 (r"/api/contents%s" % file_path_regex, ContentsHandler),
300 (r"/api/contents%s" % file_path_regex, ContentsHandler),
285 (r"/api/contents%s" % path_regex, ContentsHandler),
301 (r"/api/contents%s" % path_regex, ContentsHandler),
302 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
286 ]
303 ]
General Comments 0
You need to be logged in to leave comments. Login now