Show More
@@ -4,10 +4,11 b'' | |||||
4 | # Distributed under the terms of the Modified BSD License. |
|
4 | # Distributed under the terms of the Modified BSD License. | |
5 |
|
5 | |||
6 | import base64 |
|
6 | import base64 | |
|
7 | import errno | |||
7 | import io |
|
8 | import io | |
8 | import os |
|
9 | import os | |
9 | import glob |
|
|||
10 | import shutil |
|
10 | import shutil | |
|
11 | from contextlib import contextmanager | |||
11 |
|
12 | |||
12 | from tornado import web |
|
13 | from tornado import web | |
13 |
|
14 | |||
@@ -16,9 +17,9 b' from IPython import nbformat' | |||||
16 | from IPython.utils.io import atomic_writing |
|
17 | from IPython.utils.io import atomic_writing | |
17 | from IPython.utils.path import ensure_dir_exists |
|
18 | from IPython.utils.path import ensure_dir_exists | |
18 | from IPython.utils.traitlets import Unicode, Bool, TraitError |
|
19 | from IPython.utils.traitlets import Unicode, Bool, TraitError | |
19 | from IPython.utils.py3compat import getcwd |
|
20 | from IPython.utils.py3compat import getcwd, str_to_unicode | |
20 | from IPython.utils import tz |
|
21 | from IPython.utils import tz | |
21 |
from IPython.html.utils import is_hidden, to_os_path, |
|
22 | from IPython.html.utils import is_hidden, to_os_path, to_api_path | |
22 |
|
23 | |||
23 |
|
24 | |||
24 | class FileContentsManager(ContentsManager): |
|
25 | class FileContentsManager(ContentsManager): | |
@@ -30,7 +31,38 b' class FileContentsManager(ContentsManager):' | |||||
30 | return self.parent.notebook_dir |
|
31 | return self.parent.notebook_dir | |
31 | except AttributeError: |
|
32 | except AttributeError: | |
32 | return getcwd() |
|
33 | return getcwd() | |
33 |
|
34 | |||
|
35 | @contextmanager | |||
|
36 | def perm_to_403(self, os_path=''): | |||
|
37 | """context manager for turning permission errors into 403""" | |||
|
38 | try: | |||
|
39 | yield | |||
|
40 | except OSError as e: | |||
|
41 | if e.errno in {errno.EPERM, errno.EACCES}: | |||
|
42 | # make 403 error message without root prefix | |||
|
43 | # this may not work perfectly on unicode paths on Python 2, | |||
|
44 | # but nobody should be doing that anyway. | |||
|
45 | if not os_path: | |||
|
46 | os_path = str_to_unicode(e.filename or 'unknown file') | |||
|
47 | path = to_api_path(os_path, self.root_dir) | |||
|
48 | raise web.HTTPError(403, u'Permission denied: %s' % path) | |||
|
49 | else: | |||
|
50 | raise | |||
|
51 | ||||
|
52 | @contextmanager | |||
|
53 | def open(self, os_path, *args, **kwargs): | |||
|
54 | """wrapper around io.open that turns permission errors into 403""" | |||
|
55 | with self.perm_to_403(os_path): | |||
|
56 | with io.open(os_path, *args, **kwargs) as f: | |||
|
57 | yield f | |||
|
58 | ||||
|
59 | @contextmanager | |||
|
60 | def atomic_writing(self, os_path, *args, **kwargs): | |||
|
61 | """wrapper around atomic_writing that turns permission errors into 403""" | |||
|
62 | with self.perm_to_403(os_path): | |||
|
63 | with atomic_writing(os_path, *args, **kwargs) as f: | |||
|
64 | yield f | |||
|
65 | ||||
34 | save_script = Bool(False, config=True, help='DEPRECATED, IGNORED') |
|
66 | save_script = Bool(False, config=True, help='DEPRECATED, IGNORED') | |
35 | def _save_script_changed(self): |
|
67 | def _save_script_changed(self): | |
36 | self.log.warn(""" |
|
68 | self.log.warn(""" | |
@@ -172,6 +204,11 b' class FileContentsManager(ContentsManager):' | |||||
172 | model['created'] = created |
|
204 | model['created'] = created | |
173 | model['content'] = None |
|
205 | model['content'] = None | |
174 | model['format'] = None |
|
206 | model['format'] = None | |
|
207 | try: | |||
|
208 | model['writable'] = os.access(os_path, os.W_OK) | |||
|
209 | except OSError: | |||
|
210 | self.log.error("Failed to check write permissions on %s", os_path) | |||
|
211 | model['writable'] = False | |||
175 | return model |
|
212 | return model | |
176 |
|
213 | |||
177 | def _dir_model(self, path, content=True): |
|
214 | def _dir_model(self, path, content=True): | |
@@ -181,7 +218,7 b' class FileContentsManager(ContentsManager):' | |||||
181 | """ |
|
218 | """ | |
182 | os_path = self._get_os_path(path) |
|
219 | os_path = self._get_os_path(path) | |
183 |
|
220 | |||
184 |
four_o_four = u'directory does not exist: %r' % |
|
221 | four_o_four = u'directory does not exist: %r' % path | |
185 |
|
222 | |||
186 | if not os.path.isdir(os_path): |
|
223 | if not os.path.isdir(os_path): | |
187 | raise web.HTTPError(404, four_o_four) |
|
224 | raise web.HTTPError(404, four_o_four) | |
@@ -232,7 +269,7 b' class FileContentsManager(ContentsManager):' | |||||
232 | if not os.path.isfile(os_path): |
|
269 | if not os.path.isfile(os_path): | |
233 | # could be FIFO |
|
270 | # could be FIFO | |
234 | raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path) |
|
271 | raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path) | |
235 |
with |
|
272 | with self.open(os_path, 'rb') as f: | |
236 | bcontent = f.read() |
|
273 | bcontent = f.read() | |
237 |
|
274 | |||
238 | if format != 'base64': |
|
275 | if format != 'base64': | |
@@ -261,7 +298,7 b' class FileContentsManager(ContentsManager):' | |||||
261 | model['type'] = 'notebook' |
|
298 | model['type'] = 'notebook' | |
262 | if content: |
|
299 | if content: | |
263 | os_path = self._get_os_path(path) |
|
300 | os_path = self._get_os_path(path) | |
264 |
with |
|
301 | with self.open(os_path, 'r', encoding='utf-8') as f: | |
265 | try: |
|
302 | try: | |
266 | nb = nbformat.read(f, as_version=4) |
|
303 | nb = nbformat.read(f, as_version=4) | |
267 | except Exception as e: |
|
304 | except Exception as e: | |
@@ -321,7 +358,7 b' class FileContentsManager(ContentsManager):' | |||||
321 |
|
358 | |||
322 | self.check_and_sign(nb, path) |
|
359 | self.check_and_sign(nb, path) | |
323 |
|
360 | |||
324 | with atomic_writing(os_path, encoding='utf-8') as f: |
|
361 | with self.atomic_writing(os_path, encoding='utf-8') as f: | |
325 | nbformat.write(nb, f, version=nbformat.NO_CONVERT) |
|
362 | nbformat.write(nb, f, version=nbformat.NO_CONVERT) | |
326 |
|
363 | |||
327 | def _save_file(self, os_path, model, path=''): |
|
364 | def _save_file(self, os_path, model, path=''): | |
@@ -338,7 +375,7 b' class FileContentsManager(ContentsManager):' | |||||
338 | bcontent = base64.decodestring(b64_bytes) |
|
375 | bcontent = base64.decodestring(b64_bytes) | |
339 | except Exception as e: |
|
376 | except Exception as e: | |
340 | raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e)) |
|
377 | raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e)) | |
341 | with atomic_writing(os_path, text=False) as f: |
|
378 | with self.atomic_writing(os_path, text=False) as f: | |
342 | f.write(bcontent) |
|
379 | f.write(bcontent) | |
343 |
|
380 | |||
344 | def _save_directory(self, os_path, model, path=''): |
|
381 | def _save_directory(self, os_path, model, path=''): | |
@@ -346,7 +383,8 b' class FileContentsManager(ContentsManager):' | |||||
346 | if is_hidden(os_path, self.root_dir): |
|
383 | if is_hidden(os_path, self.root_dir): | |
347 | raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path) |
|
384 | raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path) | |
348 | if not os.path.exists(os_path): |
|
385 | if not os.path.exists(os_path): | |
349 | os.mkdir(os_path) |
|
386 | with self.perm_to_403(): | |
|
387 | os.mkdir(os_path) | |||
350 | elif not os.path.isdir(os_path): |
|
388 | elif not os.path.isdir(os_path): | |
351 | raise web.HTTPError(400, u'Not a directory: %s' % (os_path)) |
|
389 | raise web.HTTPError(400, u'Not a directory: %s' % (os_path)) | |
352 | else: |
|
390 | else: | |
@@ -379,7 +417,8 b' class FileContentsManager(ContentsManager):' | |||||
379 | except web.HTTPError: |
|
417 | except web.HTTPError: | |
380 | raise |
|
418 | raise | |
381 | except Exception as e: |
|
419 | except Exception as e: | |
382 |
|
|
420 | self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True) | |
|
421 | raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e)) | |||
383 |
|
422 | |||
384 | validation_message = None |
|
423 | validation_message = None | |
385 | if model['type'] == 'notebook': |
|
424 | if model['type'] == 'notebook': | |
@@ -423,14 +462,17 b' class FileContentsManager(ContentsManager):' | |||||
423 | cp_path = self.get_checkpoint_path(checkpoint_id, path) |
|
462 | cp_path = self.get_checkpoint_path(checkpoint_id, path) | |
424 | if os.path.isfile(cp_path): |
|
463 | if os.path.isfile(cp_path): | |
425 | self.log.debug("Unlinking checkpoint %s", cp_path) |
|
464 | self.log.debug("Unlinking checkpoint %s", cp_path) | |
426 | os.unlink(cp_path) |
|
465 | with self.perm_to_403(): | |
|
466 | rm(cp_path) | |||
427 |
|
467 | |||
428 | if os.path.isdir(os_path): |
|
468 | if os.path.isdir(os_path): | |
429 | self.log.debug("Removing directory %s", os_path) |
|
469 | self.log.debug("Removing directory %s", os_path) | |
430 | shutil.rmtree(os_path) |
|
470 | with self.perm_to_403(): | |
|
471 | shutil.rmtree(os_path) | |||
431 | else: |
|
472 | else: | |
432 | self.log.debug("Unlinking file %s", os_path) |
|
473 | self.log.debug("Unlinking file %s", os_path) | |
433 | rm(os_path) |
|
474 | with self.perm_to_403(): | |
|
475 | rm(os_path) | |||
434 |
|
476 | |||
435 | def rename(self, old_path, new_path): |
|
477 | def rename(self, old_path, new_path): | |
436 | """Rename a file.""" |
|
478 | """Rename a file.""" | |
@@ -448,7 +490,10 b' class FileContentsManager(ContentsManager):' | |||||
448 |
|
490 | |||
449 | # Move the file |
|
491 | # Move the file | |
450 | try: |
|
492 | try: | |
451 | shutil.move(old_os_path, new_os_path) |
|
493 | with self.perm_to_403(): | |
|
494 | shutil.move(old_os_path, new_os_path) | |||
|
495 | except web.HTTPError: | |||
|
496 | raise | |||
452 | except Exception as e: |
|
497 | except Exception as e: | |
453 | raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e)) |
|
498 | raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e)) | |
454 |
|
499 | |||
@@ -460,7 +505,8 b' class FileContentsManager(ContentsManager):' | |||||
460 | new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path) |
|
505 | new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path) | |
461 | if os.path.isfile(old_cp_path): |
|
506 | if os.path.isfile(old_cp_path): | |
462 | self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path) |
|
507 | self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path) | |
463 | shutil.move(old_cp_path, new_cp_path) |
|
508 | with self.perm_to_403(): | |
|
509 | shutil.move(old_cp_path, new_cp_path) | |||
464 |
|
510 | |||
465 | # Checkpoint-related utilities |
|
511 | # Checkpoint-related utilities | |
466 |
|
512 | |||
@@ -477,7 +523,8 b' class FileContentsManager(ContentsManager):' | |||||
477 | ) |
|
523 | ) | |
478 | os_path = self._get_os_path(path=parent) |
|
524 | os_path = self._get_os_path(path=parent) | |
479 | cp_dir = os.path.join(os_path, self.checkpoint_dir) |
|
525 | cp_dir = os.path.join(os_path, self.checkpoint_dir) | |
480 | ensure_dir_exists(cp_dir) |
|
526 | with self.perm_to_403(): | |
|
527 | ensure_dir_exists(cp_dir) | |||
481 | cp_path = os.path.join(cp_dir, filename) |
|
528 | cp_path = os.path.join(cp_dir, filename) | |
482 | return cp_path |
|
529 | return cp_path | |
483 |
|
530 | |||
@@ -505,7 +552,8 b' class FileContentsManager(ContentsManager):' | |||||
505 | checkpoint_id = u"checkpoint" |
|
552 | checkpoint_id = u"checkpoint" | |
506 | cp_path = self.get_checkpoint_path(checkpoint_id, path) |
|
553 | cp_path = self.get_checkpoint_path(checkpoint_id, path) | |
507 | self.log.debug("creating checkpoint for %s", path) |
|
554 | self.log.debug("creating checkpoint for %s", path) | |
508 | self._copy(src_path, cp_path) |
|
555 | with self.perm_to_403(): | |
|
556 | self._copy(src_path, cp_path) | |||
509 |
|
557 | |||
510 | # return the checkpoint info |
|
558 | # return the checkpoint info | |
511 | return self.get_checkpoint_model(checkpoint_id, path) |
|
559 | return self.get_checkpoint_model(checkpoint_id, path) | |
@@ -537,10 +585,11 b' class FileContentsManager(ContentsManager):' | |||||
537 | ) |
|
585 | ) | |
538 | # ensure notebook is readable (never restore from an unreadable notebook) |
|
586 | # ensure notebook is readable (never restore from an unreadable notebook) | |
539 | if cp_path.endswith('.ipynb'): |
|
587 | if cp_path.endswith('.ipynb'): | |
540 |
with |
|
588 | with self.open(cp_path, 'r', encoding='utf-8') as f: | |
541 | nbformat.read(f, as_version=4) |
|
589 | nbformat.read(f, as_version=4) | |
542 | self._copy(cp_path, nb_path) |
|
|||
543 | self.log.debug("copying %s -> %s", cp_path, nb_path) |
|
590 | self.log.debug("copying %s -> %s", cp_path, nb_path) | |
|
591 | with self.perm_to_403(): | |||
|
592 | self._copy(cp_path, nb_path) | |||
544 |
|
593 | |||
545 | def delete_checkpoint(self, checkpoint_id, path): |
|
594 | def delete_checkpoint(self, checkpoint_id, path): | |
546 | """delete a file's checkpoint""" |
|
595 | """delete a file's checkpoint""" |
@@ -110,7 +110,7 b' define([' | |||||
110 | CodeMirror.runMode(code, mode, el); |
|
110 | CodeMirror.runMode(code, mode, el); | |
111 | callback(null, el.innerHTML); |
|
111 | callback(null, el.innerHTML); | |
112 | } catch (err) { |
|
112 | } catch (err) { | |
113 |
console.log("Failed to highlight " + lang + " code", err |
|
113 | console.log("Failed to highlight " + lang + " code", err); | |
114 | callback(err, code); |
|
114 | callback(err, code); | |
115 | } |
|
115 | } | |
116 | }, function (err) { |
|
116 | }, function (err) { | |
@@ -132,6 +132,7 b' define([' | |||||
132 | this.undelete_index = null; |
|
132 | this.undelete_index = null; | |
133 | this.undelete_below = false; |
|
133 | this.undelete_below = false; | |
134 | this.paste_enabled = false; |
|
134 | this.paste_enabled = false; | |
|
135 | this.writable = false; | |||
135 | // It is important to start out in command mode to match the intial mode |
|
136 | // It is important to start out in command mode to match the intial mode | |
136 | // of the KeyboardManager. |
|
137 | // of the KeyboardManager. | |
137 | this.mode = 'command'; |
|
138 | this.mode = 'command'; | |
@@ -1896,6 +1897,10 b' define([' | |||||
1896 | if (this.autosave_timer) { |
|
1897 | if (this.autosave_timer) { | |
1897 | clearInterval(this.autosave_timer); |
|
1898 | clearInterval(this.autosave_timer); | |
1898 | } |
|
1899 | } | |
|
1900 | if (!this.writable) { | |||
|
1901 | // disable autosave if not writable | |||
|
1902 | interval = 0; | |||
|
1903 | } | |||
1899 |
|
1904 | |||
1900 | this.autosave_interval = this.minimum_autosave_interval = interval; |
|
1905 | this.autosave_interval = this.minimum_autosave_interval = interval; | |
1901 | if (interval) { |
|
1906 | if (interval) { | |
@@ -1918,12 +1923,18 b' define([' | |||||
1918 | * @method save_notebook |
|
1923 | * @method save_notebook | |
1919 | */ |
|
1924 | */ | |
1920 | Notebook.prototype.save_notebook = function () { |
|
1925 | Notebook.prototype.save_notebook = function () { | |
1921 | if(!this._fully_loaded){ |
|
1926 | if (!this._fully_loaded) { | |
1922 | this.events.trigger('notebook_save_failed.Notebook', |
|
1927 | this.events.trigger('notebook_save_failed.Notebook', | |
1923 | new Error("Load failed, save is disabled") |
|
1928 | new Error("Load failed, save is disabled") | |
1924 | ); |
|
1929 | ); | |
1925 | return; |
|
1930 | return; | |
|
1931 | } else if (!this.writable) { | |||
|
1932 | this.events.trigger('notebook_save_failed.Notebook', | |||
|
1933 | new Error("Notebook is read-only") | |||
|
1934 | ); | |||
|
1935 | return; | |||
1926 | } |
|
1936 | } | |
|
1937 | ||||
1927 | // Create a JSON model to be sent to the server. |
|
1938 | // Create a JSON model to be sent to the server. | |
1928 | var model = { |
|
1939 | var model = { | |
1929 | type : "notebook", |
|
1940 | type : "notebook", | |
@@ -2052,7 +2063,8 b' define([' | |||||
2052 | }); |
|
2063 | }); | |
2053 | }; |
|
2064 | }; | |
2054 |
|
2065 | |||
2055 | Notebook.prototype.copy_notebook = function(){ |
|
2066 | Notebook.prototype.copy_notebook = function () { | |
|
2067 | var that = this; | |||
2056 | var base_url = this.base_url; |
|
2068 | var base_url = this.base_url; | |
2057 | var w = window.open(); |
|
2069 | var w = window.open(); | |
2058 | var parent = utils.url_path_split(this.notebook_path)[0]; |
|
2070 | var parent = utils.url_path_split(this.notebook_path)[0]; | |
@@ -2064,7 +2076,7 b' define([' | |||||
2064 | }, |
|
2076 | }, | |
2065 | function(error) { |
|
2077 | function(error) { | |
2066 | w.close(); |
|
2078 | w.close(); | |
2067 | console.log(error); |
|
2079 | that.events.trigger('notebook_copy_failed', error); | |
2068 | } |
|
2080 | } | |
2069 | ); |
|
2081 | ); | |
2070 | }; |
|
2082 | }; | |
@@ -2175,6 +2187,7 b' define([' | |||||
2175 | } |
|
2187 | } | |
2176 | this.set_dirty(false); |
|
2188 | this.set_dirty(false); | |
2177 | this.scroll_to_top(); |
|
2189 | this.scroll_to_top(); | |
|
2190 | this.writable = data.writable || false; | |||
2178 | var nbmodel = data.content; |
|
2191 | var nbmodel = data.content; | |
2179 | var orig_nbformat = nbmodel.metadata.orig_nbformat; |
|
2192 | var orig_nbformat = nbmodel.metadata.orig_nbformat; | |
2180 | var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor; |
|
2193 | var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor; | |
@@ -2249,7 +2262,12 b' define([' | |||||
2249 | } else { |
|
2262 | } else { | |
2250 | celltoolbar.CellToolbar.global_hide(); |
|
2263 | celltoolbar.CellToolbar.global_hide(); | |
2251 | } |
|
2264 | } | |
2252 |
|
2265 | |||
|
2266 | if (!this.writable) { | |||
|
2267 | this.set_autosave_interval(0); | |||
|
2268 | this.events.trigger('notebook_read_only.Notebook'); | |||
|
2269 | } | |||
|
2270 | ||||
2253 | // now that we're fully loaded, it is safe to restore save functionality |
|
2271 | // now that we're fully loaded, it is safe to restore save functionality | |
2254 | this._fully_loaded = true; |
|
2272 | this._fully_loaded = true; | |
2255 | this.events.trigger('notebook_loaded.Notebook'); |
|
2273 | this.events.trigger('notebook_loaded.Notebook'); |
@@ -340,8 +340,11 b' define([' | |||||
340 | this.events.on('notebook_saved.Notebook', function () { |
|
340 | this.events.on('notebook_saved.Notebook', function () { | |
341 | nnw.set_message("Notebook saved",2000); |
|
341 | nnw.set_message("Notebook saved",2000); | |
342 | }); |
|
342 | }); | |
343 |
this.events.on('notebook_save_failed.Notebook', function (evt, |
|
343 | this.events.on('notebook_save_failed.Notebook', function (evt, error) { | |
344 |
nnw.warning( |
|
344 | nnw.warning(error.message || "Notebook save failed"); | |
|
345 | }); | |||
|
346 | this.events.on('notebook_copy_failed.Notebook', function (evt, error) { | |||
|
347 | nnw.warning(error.message || "Notebook copy failed"); | |||
345 | }); |
|
348 | }); | |
346 |
|
349 | |||
347 | // Checkpoint events |
|
350 | // Checkpoint events |
@@ -46,6 +46,11 b' define([' | |||||
46 | this.events.on('notebook_save_failed.Notebook', function () { |
|
46 | this.events.on('notebook_save_failed.Notebook', function () { | |
47 | that.set_save_status('Autosave Failed!'); |
|
47 | that.set_save_status('Autosave Failed!'); | |
48 | }); |
|
48 | }); | |
|
49 | this.events.on('notebook_read_only.Notebook', function () { | |||
|
50 | that.set_save_status('(read only)'); | |||
|
51 | // disable future set_save_status | |||
|
52 | that.set_save_status = function () {}; | |||
|
53 | }); | |||
49 | this.events.on('checkpoints_listed.Notebook', function (event, data) { |
|
54 | this.events.on('checkpoints_listed.Notebook', function (event, data) { | |
50 | that._set_last_checkpoint(data[0]); |
|
55 | that._set_last_checkpoint(data[0]); | |
51 | }); |
|
56 | }); |
@@ -1,16 +1,7 b'' | |||||
1 | """Notebook related utilities |
|
1 | """Notebook related utilities""" | |
2 |
|
2 | |||
3 | Authors: |
|
3 | # Copyright (c) IPython Development Team. | |
4 |
|
4 | # Distributed under the terms of the Modified BSD License. | ||
5 | * Brian Granger |
|
|||
6 | """ |
|
|||
7 |
|
||||
8 | #----------------------------------------------------------------------------- |
|
|||
9 | # Copyright (C) 2011 The IPython Development Team |
|
|||
10 | # |
|
|||
11 | # Distributed under the terms of the BSD License. The full license is in |
|
|||
12 | # the file COPYING, distributed as part of this software. |
|
|||
13 | #----------------------------------------------------------------------------- |
|
|||
14 |
|
5 | |||
15 | from __future__ import print_function |
|
6 | from __future__ import print_function | |
16 |
|
7 | |||
@@ -28,9 +19,6 b' from IPython.utils import py3compat' | |||||
28 | # It is used by BSD to indicate hidden files. |
|
19 | # It is used by BSD to indicate hidden files. | |
29 | UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768) |
|
20 | UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768) | |
30 |
|
21 | |||
31 | #----------------------------------------------------------------------------- |
|
|||
32 | # Imports |
|
|||
33 | #----------------------------------------------------------------------------- |
|
|||
34 |
|
22 | |||
35 | def url_path_join(*pieces): |
|
23 | def url_path_join(*pieces): | |
36 | """Join components of url into a relative url |
|
24 | """Join components of url into a relative url | |
@@ -137,4 +125,17 b" def to_os_path(path, root=''):" | |||||
137 | parts = [p for p in parts if p != ''] # remove duplicate splits |
|
125 | parts = [p for p in parts if p != ''] # remove duplicate splits | |
138 | path = os.path.join(root, *parts) |
|
126 | path = os.path.join(root, *parts) | |
139 | return path |
|
127 | return path | |
|
128 | ||||
|
129 | def to_api_path(os_path, root=''): | |||
|
130 | """Convert a filesystem path to an API path | |||
140 |
|
|
131 | ||
|
132 | If given, root will be removed from the path. | |||
|
133 | root must be a filesystem path already. | |||
|
134 | """ | |||
|
135 | if os_path.startswith(root): | |||
|
136 | os_path = os_path[len(root):] | |||
|
137 | parts = os_path.strip(os.path.sep).split(os.path.sep) | |||
|
138 | parts = [p for p in parts if p != ''] # remove duplicate splits | |||
|
139 | path = '/'.join(parts) | |||
|
140 | return path | |||
|
141 |
General Comments 0
You need to be logged in to leave comments.
Login now