##// END OF EJS Templates
unicode!
Min RK -
Show More
@@ -1,337 +1,337 b''
1 """A base class for contents managers."""
1 """A base class for contents managers."""
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 from fnmatch import fnmatch
6 from fnmatch import fnmatch
7 import itertools
7 import itertools
8 import json
8 import json
9 import os
9 import os
10
10
11 from tornado.web import HTTPError
11 from tornado.web import HTTPError
12
12
13 from IPython.config.configurable import LoggingConfigurable
13 from IPython.config.configurable import LoggingConfigurable
14 from IPython.nbformat import sign, validate, ValidationError
14 from IPython.nbformat import sign, validate, ValidationError
15 from IPython.nbformat.v4 import new_notebook
15 from IPython.nbformat.v4 import new_notebook
16 from IPython.utils.traitlets import Instance, Unicode, List
16 from IPython.utils.traitlets import Instance, Unicode, List
17
17
18
18
19 class ContentsManager(LoggingConfigurable):
19 class ContentsManager(LoggingConfigurable):
20 """Base class for serving files and directories.
20 """Base class for serving files and directories.
21
21
22 This serves any text or binary file,
22 This serves any text or binary file,
23 as well as directories,
23 as well as directories,
24 with special handling for JSON notebook documents.
24 with special handling for JSON notebook documents.
25
25
26 Most APIs take a path argument,
26 Most APIs take a path argument,
27 which is always an API-style unicode path,
27 which is always an API-style unicode path,
28 and always refers to a directory.
28 and always refers to a directory.
29
29
30 - unicode, not url-escaped
30 - unicode, not url-escaped
31 - '/'-separated
31 - '/'-separated
32 - leading and trailing '/' will be stripped
32 - leading and trailing '/' will be stripped
33 - if unspecified, path defaults to '',
33 - if unspecified, path defaults to '',
34 indicating the root path.
34 indicating the root path.
35
35
36 """
36 """
37
37
38 notary = Instance(sign.NotebookNotary)
38 notary = Instance(sign.NotebookNotary)
39 def _notary_default(self):
39 def _notary_default(self):
40 return sign.NotebookNotary(parent=self)
40 return sign.NotebookNotary(parent=self)
41
41
42 hide_globs = List(Unicode, [
42 hide_globs = List(Unicode, [
43 u'__pycache__', '*.pyc', '*.pyo',
43 u'__pycache__', '*.pyc', '*.pyo',
44 '.DS_Store', '*.so', '*.dylib', '*~',
44 '.DS_Store', '*.so', '*.dylib', '*~',
45 ], config=True, help="""
45 ], config=True, help="""
46 Glob patterns to hide in file and directory listings.
46 Glob patterns to hide in file and directory listings.
47 """)
47 """)
48
48
49 untitled_notebook = Unicode("Untitled", config=True,
49 untitled_notebook = Unicode("Untitled", config=True,
50 help="The base name used when creating untitled notebooks."
50 help="The base name used when creating untitled notebooks."
51 )
51 )
52
52
53 untitled_file = Unicode("untitled", config=True,
53 untitled_file = Unicode("untitled", config=True,
54 help="The base name used when creating untitled files."
54 help="The base name used when creating untitled files."
55 )
55 )
56
56
57 untitled_directory = Unicode("Untitled Folder", config=True,
57 untitled_directory = Unicode("Untitled Folder", config=True,
58 help="The base name used when creating untitled directories."
58 help="The base name used when creating untitled directories."
59 )
59 )
60
60
61 # ContentsManager API part 1: methods that must be
61 # ContentsManager API part 1: methods that must be
62 # implemented in subclasses.
62 # implemented in subclasses.
63
63
64 def dir_exists(self, path):
64 def dir_exists(self, path):
65 """Does the API-style path (directory) actually exist?
65 """Does the API-style path (directory) actually exist?
66
66
67 Like os.path.isdir
67 Like os.path.isdir
68
68
69 Override this method in subclasses.
69 Override this method in subclasses.
70
70
71 Parameters
71 Parameters
72 ----------
72 ----------
73 path : string
73 path : string
74 The path to check
74 The path to check
75
75
76 Returns
76 Returns
77 -------
77 -------
78 exists : bool
78 exists : bool
79 Whether the path does indeed exist.
79 Whether the path does indeed exist.
80 """
80 """
81 raise NotImplementedError
81 raise NotImplementedError
82
82
83 def is_hidden(self, path):
83 def is_hidden(self, path):
84 """Does the API style path correspond to a hidden directory or file?
84 """Does the API style path correspond to a hidden directory or file?
85
85
86 Parameters
86 Parameters
87 ----------
87 ----------
88 path : string
88 path : string
89 The path to check. This is an API path (`/` separated,
89 The path to check. This is an API path (`/` separated,
90 relative to root dir).
90 relative to root dir).
91
91
92 Returns
92 Returns
93 -------
93 -------
94 hidden : bool
94 hidden : bool
95 Whether the path is hidden.
95 Whether the path is hidden.
96
96
97 """
97 """
98 raise NotImplementedError
98 raise NotImplementedError
99
99
100 def file_exists(self, path=''):
100 def file_exists(self, path=''):
101 """Does a file exist at the given path?
101 """Does a file exist at the given path?
102
102
103 Like os.path.isfile
103 Like os.path.isfile
104
104
105 Override this method in subclasses.
105 Override this method in subclasses.
106
106
107 Parameters
107 Parameters
108 ----------
108 ----------
109 name : string
109 name : string
110 The name of the file you are checking.
110 The name of the file you are checking.
111 path : string
111 path : string
112 The relative path to the file's directory (with '/' as separator)
112 The relative path to the file's directory (with '/' as separator)
113
113
114 Returns
114 Returns
115 -------
115 -------
116 exists : bool
116 exists : bool
117 Whether the file exists.
117 Whether the file exists.
118 """
118 """
119 raise NotImplementedError('must be implemented in a subclass')
119 raise NotImplementedError('must be implemented in a subclass')
120
120
121 def exists(self, path):
121 def exists(self, path):
122 """Does a file or directory exist at the given name and path?
122 """Does a file or directory exist at the given name and path?
123
123
124 Like os.path.exists
124 Like os.path.exists
125
125
126 Parameters
126 Parameters
127 ----------
127 ----------
128 path : string
128 path : string
129 The relative path to the file's directory (with '/' as separator)
129 The relative path to the file's directory (with '/' as separator)
130
130
131 Returns
131 Returns
132 -------
132 -------
133 exists : bool
133 exists : bool
134 Whether the target exists.
134 Whether the target exists.
135 """
135 """
136 return self.file_exists(path) or self.dir_exists(path)
136 return self.file_exists(path) or self.dir_exists(path)
137
137
138 def get_model(self, path, content=True):
138 def get_model(self, path, content=True):
139 """Get the model of a file or directory with or without content."""
139 """Get the model of a file or directory with or without content."""
140 raise NotImplementedError('must be implemented in a subclass')
140 raise NotImplementedError('must be implemented in a subclass')
141
141
142 def save(self, model, path):
142 def save(self, model, path):
143 """Save the file or directory and return the model with no content."""
143 """Save the file or directory and return the model with no content."""
144 raise NotImplementedError('must be implemented in a subclass')
144 raise NotImplementedError('must be implemented in a subclass')
145
145
146 def update(self, model, path):
146 def update(self, model, path):
147 """Update the file or directory and return the model with no content.
147 """Update the file or directory and return the model with no content.
148
148
149 For use in PATCH requests, to enable renaming a file without
149 For use in PATCH requests, to enable renaming a file without
150 re-uploading its contents. Only used for renaming at the moment.
150 re-uploading its contents. Only used for renaming at the moment.
151 """
151 """
152 raise NotImplementedError('must be implemented in a subclass')
152 raise NotImplementedError('must be implemented in a subclass')
153
153
154 def delete(self, path):
154 def delete(self, path):
155 """Delete file or directory by path."""
155 """Delete file or directory by path."""
156 raise NotImplementedError('must be implemented in a subclass')
156 raise NotImplementedError('must be implemented in a subclass')
157
157
158 def create_checkpoint(self, path):
158 def create_checkpoint(self, path):
159 """Create a checkpoint of the current state of a file
159 """Create a checkpoint of the current state of a file
160
160
161 Returns a checkpoint_id for the new checkpoint.
161 Returns a checkpoint_id for the new checkpoint.
162 """
162 """
163 raise NotImplementedError("must be implemented in a subclass")
163 raise NotImplementedError("must be implemented in a subclass")
164
164
165 def list_checkpoints(self, path):
165 def list_checkpoints(self, path):
166 """Return a list of checkpoints for a given file"""
166 """Return a list of checkpoints for a given file"""
167 return []
167 return []
168
168
169 def restore_checkpoint(self, checkpoint_id, path):
169 def restore_checkpoint(self, checkpoint_id, path):
170 """Restore a file from one of its checkpoints"""
170 """Restore a file from one of its checkpoints"""
171 raise NotImplementedError("must be implemented in a subclass")
171 raise NotImplementedError("must be implemented in a subclass")
172
172
173 def delete_checkpoint(self, checkpoint_id, path):
173 def delete_checkpoint(self, checkpoint_id, path):
174 """delete a checkpoint for a file"""
174 """delete a checkpoint for a file"""
175 raise NotImplementedError("must be implemented in a subclass")
175 raise NotImplementedError("must be implemented in a subclass")
176
176
177 # ContentsManager API part 2: methods that have useable default
177 # ContentsManager API part 2: methods that have useable default
178 # implementations, but can be overridden in subclasses.
178 # implementations, but can be overridden in subclasses.
179
179
180 def info_string(self):
180 def info_string(self):
181 return "Serving contents"
181 return "Serving contents"
182
182
183 def get_kernel_path(self, path, model=None):
183 def get_kernel_path(self, path, model=None):
184 """ Return the path to start kernel in """
184 """ Return the path to start kernel in """
185 return path
185 return path
186
186
187 def increment_filename(self, filename, path=''):
187 def increment_filename(self, filename, path=''):
188 """Increment a filename until it is unique.
188 """Increment a filename until it is unique.
189
189
190 Parameters
190 Parameters
191 ----------
191 ----------
192 filename : unicode
192 filename : unicode
193 The name of a file, including extension
193 The name of a file, including extension
194 path : unicode
194 path : unicode
195 The API path of the target's directory
195 The API path of the target's directory
196
196
197 Returns
197 Returns
198 -------
198 -------
199 name : unicode
199 name : unicode
200 A filename that is unique, based on the input filename.
200 A filename that is unique, based on the input filename.
201 """
201 """
202 path = path.strip('/')
202 path = path.strip('/')
203 basename, ext = os.path.splitext(filename)
203 basename, ext = os.path.splitext(filename)
204 for i in itertools.count():
204 for i in itertools.count():
205 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
205 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
206 ext=ext)
206 ext=ext)
207 if not self.file_exists('{}/{}'.format(path, name)):
207 if not self.file_exists(u'{}/{}'.format(path, name)):
208 break
208 break
209 return name
209 return name
210
210
211 def validate_notebook_model(self, model):
211 def validate_notebook_model(self, model):
212 """Add failed-validation message to model"""
212 """Add failed-validation message to model"""
213 try:
213 try:
214 validate(model['content'])
214 validate(model['content'])
215 except ValidationError as e:
215 except ValidationError as e:
216 model['message'] = 'Notebook Validation failed: {}:\n{}'.format(
216 model['message'] = u'Notebook Validation failed: {}:\n{}'.format(
217 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
217 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
218 )
218 )
219 return model
219 return model
220
220
221 def new(self, model=None, path='', ext='.ipynb'):
221 def new(self, model=None, path='', ext='.ipynb'):
222 """Create a new file or directory and return its model with no content."""
222 """Create a new file or directory and return its model with no content."""
223 path = path.strip('/')
223 path = path.strip('/')
224 if model is None:
224 if model is None:
225 model = {}
225 model = {}
226 else:
226 else:
227 model.pop('path', None)
227 model.pop('path', None)
228 if 'content' not in model and model.get('type', None) != 'directory':
228 if 'content' not in model and model.get('type', None) != 'directory':
229 if ext == '.ipynb':
229 if ext == '.ipynb':
230 model['content'] = new_notebook()
230 model['content'] = new_notebook()
231 model['type'] = 'notebook'
231 model['type'] = 'notebook'
232 model['format'] = 'json'
232 model['format'] = 'json'
233 else:
233 else:
234 model['content'] = ''
234 model['content'] = ''
235 model['type'] = 'file'
235 model['type'] = 'file'
236 model['format'] = 'text'
236 model['format'] = 'text'
237 if self.dir_exists(path):
237 if self.dir_exists(path):
238 if model['type'] == 'directory':
238 if model['type'] == 'directory':
239 untitled = self.untitled_directory
239 untitled = self.untitled_directory
240 elif model['type'] == 'notebook':
240 elif model['type'] == 'notebook':
241 untitled = self.untitled_notebook
241 untitled = self.untitled_notebook
242 elif model['type'] == 'file':
242 elif model['type'] == 'file':
243 untitled = self.untitled_file
243 untitled = self.untitled_file
244 else:
244 else:
245 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
245 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
246
246
247 name = self.increment_filename(untitled + ext, path)
247 name = self.increment_filename(untitled + ext, path)
248 path = '{0}/{1}'.format(path, name)
248 path = u'{0}/{1}'.format(path, name)
249 model = self.save(model, path)
249 model = self.save(model, path)
250 return model
250 return model
251
251
252 def copy(self, from_path, to_path=None):
252 def copy(self, from_path, to_path=None):
253 """Copy an existing file and return its new model.
253 """Copy an existing file and return its new model.
254
254
255 If to_name not specified, increment `from_name-Copy#.ext`.
255 If to_name not specified, increment `from_name-Copy#.ext`.
256
256
257 copy_from can be a full path to a file,
257 copy_from can be a full path to a file,
258 or just a base name. If a base name, `path` is used.
258 or just a base name. If a base name, `path` is used.
259 """
259 """
260 path = from_path.strip('/')
260 path = from_path.strip('/')
261 if '/' in path:
261 if '/' in path:
262 from_dir, from_name = path.rsplit('/', 1)
262 from_dir, from_name = path.rsplit('/', 1)
263 else:
263 else:
264 from_dir = ''
264 from_dir = ''
265 from_name = path
265 from_name = path
266
266
267 model = self.get_model(path)
267 model = self.get_model(path)
268 model.pop('path', None)
268 model.pop('path', None)
269 model.pop('name', None)
269 model.pop('name', None)
270 if model['type'] == 'directory':
270 if model['type'] == 'directory':
271 raise HTTPError(400, "Can't copy directories")
271 raise HTTPError(400, "Can't copy directories")
272
272
273 if not to_path:
273 if not to_path:
274 to_path = from_dir
274 to_path = from_dir
275 if self.dir_exists(to_path):
275 if self.dir_exists(to_path):
276 base, ext = os.path.splitext(from_name)
276 base, ext = os.path.splitext(from_name)
277 copy_name = u'{0}-Copy{1}'.format(base, ext)
277 copy_name = u'{0}-Copy{1}'.format(base, ext)
278 to_name = self.increment_filename(copy_name, to_path)
278 to_name = self.increment_filename(copy_name, to_path)
279 to_path = '{0}/{1}'.format(to_path, to_name)
279 to_path = u'{0}/{1}'.format(to_path, to_name)
280
280
281 model = self.save(model, to_path)
281 model = self.save(model, to_path)
282 return model
282 return model
283
283
284 def log_info(self):
284 def log_info(self):
285 self.log.info(self.info_string())
285 self.log.info(self.info_string())
286
286
287 def trust_notebook(self, path):
287 def trust_notebook(self, path):
288 """Explicitly trust a notebook
288 """Explicitly trust a notebook
289
289
290 Parameters
290 Parameters
291 ----------
291 ----------
292 path : string
292 path : string
293 The path of a notebook
293 The path of a notebook
294 """
294 """
295 model = self.get_model(path)
295 model = self.get_model(path)
296 nb = model['content']
296 nb = model['content']
297 self.log.warn("Trusting notebook %s", path)
297 self.log.warn("Trusting notebook %s", path)
298 self.notary.mark_cells(nb, True)
298 self.notary.mark_cells(nb, True)
299 self.save(model, path)
299 self.save(model, path)
300
300
301 def check_and_sign(self, nb, path=''):
301 def check_and_sign(self, nb, path=''):
302 """Check for trusted cells, and sign the notebook.
302 """Check for trusted cells, and sign the notebook.
303
303
304 Called as a part of saving notebooks.
304 Called as a part of saving notebooks.
305
305
306 Parameters
306 Parameters
307 ----------
307 ----------
308 nb : dict
308 nb : dict
309 The notebook dict
309 The notebook dict
310 path : string
310 path : string
311 The notebook's path (for logging)
311 The notebook's path (for logging)
312 """
312 """
313 if self.notary.check_cells(nb):
313 if self.notary.check_cells(nb):
314 self.notary.sign(nb)
314 self.notary.sign(nb)
315 else:
315 else:
316 self.log.warn("Saving untrusted notebook %s", path)
316 self.log.warn("Saving untrusted notebook %s", path)
317
317
318 def mark_trusted_cells(self, nb, path=''):
318 def mark_trusted_cells(self, nb, path=''):
319 """Mark cells as trusted if the notebook signature matches.
319 """Mark cells as trusted if the notebook signature matches.
320
320
321 Called as a part of loading notebooks.
321 Called as a part of loading notebooks.
322
322
323 Parameters
323 Parameters
324 ----------
324 ----------
325 nb : dict
325 nb : dict
326 The notebook object (in current nbformat)
326 The notebook object (in current nbformat)
327 path : string
327 path : string
328 The notebook's directory (for logging)
328 The notebook's directory (for logging)
329 """
329 """
330 trusted = self.notary.check_signature(nb)
330 trusted = self.notary.check_signature(nb)
331 if not trusted:
331 if not trusted:
332 self.log.warn("Notebook %s is not trusted", path)
332 self.log.warn("Notebook %s is not trusted", path)
333 self.notary.mark_cells(nb, trusted)
333 self.notary.mark_cells(nb, trusted)
334
334
335 def should_list(self, name):
335 def should_list(self, name):
336 """Should this file/directory name be displayed in a listing?"""
336 """Should this file/directory name be displayed in a listing?"""
337 return not any(fnmatch(name, glob) for glob in self.hide_globs)
337 return not any(fnmatch(name, glob) for glob in self.hide_globs)
General Comments 0
You need to be logged in to leave comments. Login now