##// END OF EJS Templates
catch IOError in addition to OSError...
Min RK -
Show More
@@ -1,166 +1,166
1 """
1 """
2 Utilities for file-based Contents/Checkpoints managers.
2 Utilities for file-based Contents/Checkpoints managers.
3 """
3 """
4
4
5 # Copyright (c) IPython Development Team.
5 # Copyright (c) IPython Development Team.
6 # Distributed under the terms of the Modified BSD License.
6 # Distributed under the terms of the Modified BSD License.
7
7
8 import base64
8 import base64
9 from contextlib import contextmanager
9 from contextlib import contextmanager
10 import errno
10 import errno
11 import io
11 import io
12 import os
12 import os
13 import shutil
13 import shutil
14
14
15 from tornado.web import HTTPError
15 from tornado.web import HTTPError
16
16
17 from IPython.html.utils import (
17 from IPython.html.utils import (
18 to_api_path,
18 to_api_path,
19 to_os_path,
19 to_os_path,
20 )
20 )
21 from IPython import nbformat
21 from IPython import nbformat
22 from IPython.utils.io import atomic_writing
22 from IPython.utils.io import atomic_writing
23 from IPython.utils.py3compat import str_to_unicode
23 from IPython.utils.py3compat import str_to_unicode
24
24
25
25
26 class FileManagerMixin(object):
26 class FileManagerMixin(object):
27 """
27 """
28 Mixin for ContentsAPI classes that interact with the filesystem.
28 Mixin for ContentsAPI classes that interact with the filesystem.
29
29
30 Provides facilities for reading, writing, and copying both notebooks and
30 Provides facilities for reading, writing, and copying both notebooks and
31 generic files.
31 generic files.
32
32
33 Shared by FileContentsManager and FileCheckpoints.
33 Shared by FileContentsManager and FileCheckpoints.
34
34
35 Note
35 Note
36 ----
36 ----
37 Classes using this mixin must provide the following attributes:
37 Classes using this mixin must provide the following attributes:
38
38
39 root_dir : unicode
39 root_dir : unicode
40 A directory against against which API-style paths are to be resolved.
40 A directory against against which API-style paths are to be resolved.
41
41
42 log : logging.Logger
42 log : logging.Logger
43 """
43 """
44
44
45 @contextmanager
45 @contextmanager
46 def open(self, os_path, *args, **kwargs):
46 def open(self, os_path, *args, **kwargs):
47 """wrapper around io.open that turns permission errors into 403"""
47 """wrapper around io.open that turns permission errors into 403"""
48 with self.perm_to_403(os_path):
48 with self.perm_to_403(os_path):
49 with io.open(os_path, *args, **kwargs) as f:
49 with io.open(os_path, *args, **kwargs) as f:
50 yield f
50 yield f
51
51
52 @contextmanager
52 @contextmanager
53 def atomic_writing(self, os_path, *args, **kwargs):
53 def atomic_writing(self, os_path, *args, **kwargs):
54 """wrapper around atomic_writing that turns permission errors to 403"""
54 """wrapper around atomic_writing that turns permission errors to 403"""
55 with self.perm_to_403(os_path):
55 with self.perm_to_403(os_path):
56 with atomic_writing(os_path, *args, **kwargs) as f:
56 with atomic_writing(os_path, *args, **kwargs) as f:
57 yield f
57 yield f
58
58
59 @contextmanager
59 @contextmanager
60 def perm_to_403(self, os_path=''):
60 def perm_to_403(self, os_path=''):
61 """context manager for turning permission errors into 403."""
61 """context manager for turning permission errors into 403."""
62 try:
62 try:
63 yield
63 yield
64 except OSError as e:
64 except (OSError, IOError) as e:
65 if e.errno in {errno.EPERM, errno.EACCES}:
65 if e.errno in {errno.EPERM, errno.EACCES}:
66 # make 403 error message without root prefix
66 # make 403 error message without root prefix
67 # this may not work perfectly on unicode paths on Python 2,
67 # this may not work perfectly on unicode paths on Python 2,
68 # but nobody should be doing that anyway.
68 # but nobody should be doing that anyway.
69 if not os_path:
69 if not os_path:
70 os_path = str_to_unicode(e.filename or 'unknown file')
70 os_path = str_to_unicode(e.filename or 'unknown file')
71 path = to_api_path(os_path, root=self.root_dir)
71 path = to_api_path(os_path, root=self.root_dir)
72 raise HTTPError(403, u'Permission denied: %s' % path)
72 raise HTTPError(403, u'Permission denied: %s' % path)
73 else:
73 else:
74 raise
74 raise
75
75
76 def _copy(self, src, dest):
76 def _copy(self, src, dest):
77 """copy src to dest
77 """copy src to dest
78
78
79 like shutil.copy2, but log errors in copystat
79 like shutil.copy2, but log errors in copystat
80 """
80 """
81 shutil.copyfile(src, dest)
81 shutil.copyfile(src, dest)
82 try:
82 try:
83 shutil.copystat(src, dest)
83 shutil.copystat(src, dest)
84 except OSError:
84 except OSError:
85 self.log.debug("copystat on %s failed", dest, exc_info=True)
85 self.log.debug("copystat on %s failed", dest, exc_info=True)
86
86
87 def _get_os_path(self, path):
87 def _get_os_path(self, path):
88 """Given an API path, return its file system path.
88 """Given an API path, return its file system path.
89
89
90 Parameters
90 Parameters
91 ----------
91 ----------
92 path : string
92 path : string
93 The relative API path to the named file.
93 The relative API path to the named file.
94
94
95 Returns
95 Returns
96 -------
96 -------
97 path : string
97 path : string
98 Native, absolute OS path to for a file.
98 Native, absolute OS path to for a file.
99 """
99 """
100 return to_os_path(path, self.root_dir)
100 return to_os_path(path, self.root_dir)
101
101
102 def _read_notebook(self, os_path, as_version=4):
102 def _read_notebook(self, os_path, as_version=4):
103 """Read a notebook from an os path."""
103 """Read a notebook from an os path."""
104 with self.open(os_path, 'r', encoding='utf-8') as f:
104 with self.open(os_path, 'r', encoding='utf-8') as f:
105 try:
105 try:
106 return nbformat.read(f, as_version=as_version)
106 return nbformat.read(f, as_version=as_version)
107 except Exception as e:
107 except Exception as e:
108 raise HTTPError(
108 raise HTTPError(
109 400,
109 400,
110 u"Unreadable Notebook: %s %r" % (os_path, e),
110 u"Unreadable Notebook: %s %r" % (os_path, e),
111 )
111 )
112
112
113 def _save_notebook(self, os_path, nb):
113 def _save_notebook(self, os_path, nb):
114 """Save a notebook to an os_path."""
114 """Save a notebook to an os_path."""
115 with self.atomic_writing(os_path, encoding='utf-8') as f:
115 with self.atomic_writing(os_path, encoding='utf-8') as f:
116 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
116 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
117
117
118 def _read_file(self, os_path, format):
118 def _read_file(self, os_path, format):
119 """Read a non-notebook file.
119 """Read a non-notebook file.
120
120
121 os_path: The path to be read.
121 os_path: The path to be read.
122 format:
122 format:
123 If 'text', the contents will be decoded as UTF-8.
123 If 'text', the contents will be decoded as UTF-8.
124 If 'base64', the raw bytes contents will be encoded as base64.
124 If 'base64', the raw bytes contents will be encoded as base64.
125 If not specified, try to decode as UTF-8, and fall back to base64
125 If not specified, try to decode as UTF-8, and fall back to base64
126 """
126 """
127 if not os.path.isfile(os_path):
127 if not os.path.isfile(os_path):
128 raise HTTPError(400, "Cannot read non-file %s" % os_path)
128 raise HTTPError(400, "Cannot read non-file %s" % os_path)
129
129
130 with self.open(os_path, 'rb') as f:
130 with self.open(os_path, 'rb') as f:
131 bcontent = f.read()
131 bcontent = f.read()
132
132
133 if format is None or format == 'text':
133 if format is None or format == 'text':
134 # Try to interpret as unicode if format is unknown or if unicode
134 # Try to interpret as unicode if format is unknown or if unicode
135 # was explicitly requested.
135 # was explicitly requested.
136 try:
136 try:
137 return bcontent.decode('utf8'), 'text'
137 return bcontent.decode('utf8'), 'text'
138 except UnicodeError:
138 except UnicodeError:
139 if format == 'text':
139 if format == 'text':
140 raise HTTPError(
140 raise HTTPError(
141 400,
141 400,
142 "%s is not UTF-8 encoded" % os_path,
142 "%s is not UTF-8 encoded" % os_path,
143 reason='bad format',
143 reason='bad format',
144 )
144 )
145 return base64.encodestring(bcontent).decode('ascii'), 'base64'
145 return base64.encodestring(bcontent).decode('ascii'), 'base64'
146
146
147 def _save_file(self, os_path, content, format):
147 def _save_file(self, os_path, content, format):
148 """Save content of a generic file."""
148 """Save content of a generic file."""
149 if format not in {'text', 'base64'}:
149 if format not in {'text', 'base64'}:
150 raise HTTPError(
150 raise HTTPError(
151 400,
151 400,
152 "Must specify format of file contents as 'text' or 'base64'",
152 "Must specify format of file contents as 'text' or 'base64'",
153 )
153 )
154 try:
154 try:
155 if format == 'text':
155 if format == 'text':
156 bcontent = content.encode('utf8')
156 bcontent = content.encode('utf8')
157 else:
157 else:
158 b64_bytes = content.encode('ascii')
158 b64_bytes = content.encode('ascii')
159 bcontent = base64.decodestring(b64_bytes)
159 bcontent = base64.decodestring(b64_bytes)
160 except Exception as e:
160 except Exception as e:
161 raise HTTPError(
161 raise HTTPError(
162 400, u'Encoding error saving %s: %s' % (os_path, e)
162 400, u'Encoding error saving %s: %s' % (os_path, e)
163 )
163 )
164
164
165 with self.atomic_writing(os_path, text=False) as f:
165 with self.atomic_writing(os_path, text=False) as f:
166 f.write(bcontent)
166 f.write(bcontent)
General Comments 0
You need to be logged in to leave comments. Login now