Show More
@@ -313,7 +313,7 b' class FileContentsManager(ContentsManager):' | |||
|
313 | 313 | bcontent = base64.decodestring(b64_bytes) |
|
314 | 314 | except Exception as e: |
|
315 | 315 | raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e)) |
|
316 |
with atomic_writing(os_path, |
|
|
316 | with atomic_writing(os_path, text=False) as f: | |
|
317 | 317 | f.write(bcontent) |
|
318 | 318 | |
|
319 | 319 | def _save_directory(self, os_path, model, name='', path=''): |
@@ -17,6 +17,7 b' from __future__ import absolute_import' | |||
|
17 | 17 | #----------------------------------------------------------------------------- |
|
18 | 18 | import codecs |
|
19 | 19 | from contextlib import contextmanager |
|
20 | import io | |
|
20 | 21 | import os |
|
21 | 22 | import sys |
|
22 | 23 | import tempfile |
@@ -219,21 +220,55 b" def temp_pyfile(src, ext='.py'):" | |||
|
219 | 220 | return fname, f |
|
220 | 221 | |
|
221 | 222 | @contextmanager |
|
222 |
def atomic_writing(path, |
|
|
223 | tmp_file = path + '.tmp-write' | |
|
224 | if 'b' in mode: | |
|
225 | encoding = None | |
|
226 | ||
|
227 | with open(tmp_file, mode, encoding=encoding, **kwargs) as f: | |
|
228 | yield f | |
|
229 | ||
|
230 | # Written successfully, now rename it | |
|
223 | def atomic_writing(path, text=True, encoding='utf-8', **kwargs): | |
|
224 | """Context manager to write to a file only if the entire write is successful. | |
|
225 | ||
|
226 | This works by creating a temporary file in the same directory, and renaming | |
|
227 | it over the old file if the context is exited without an error. If the | |
|
228 | target file is a symlink or a hardlink, this will not be preserved: it will | |
|
229 | be replaced by a new regular file. | |
|
230 | ||
|
231 | On Windows, there is a small chink in the atomicity: the target file is | |
|
232 | deleted before renaming the temporary file over it. This appears to be | |
|
233 | unavoidable. | |
|
234 | ||
|
235 | Parameters | |
|
236 | ---------- | |
|
237 | path : str | |
|
238 | The target file to write to. | |
|
239 | ||
|
240 | text : bool, optional | |
|
241 | Whether to open the file in text mode (i.e. to write unicode). Default is | |
|
242 | True. | |
|
243 | ||
|
244 | encoding : str, optional | |
|
245 | The encoding to use for files opened in text mode. Default is UTF-8. | |
|
246 | ||
|
247 | **kwargs | |
|
248 | Passed to :func:`io.open`. | |
|
249 | """ | |
|
250 | dirname, basename = os.path.split(path) | |
|
251 | handle, tmp_path = tempfile.mkstemp(prefix=basename, dir=dirname, text=text) | |
|
252 | if text: | |
|
253 | fileobj = io.open(handle, 'w', encoding=encoding, **kwargs) | |
|
254 | else: | |
|
255 | fileobj = io.open(handle, 'wb', **kwargs) | |
|
256 | ||
|
257 | try: | |
|
258 | yield fileobj | |
|
259 | except: | |
|
260 | fileobj.close() | |
|
261 | os.remove(tmp_path) | |
|
262 | raise | |
|
263 | else: | |
|
264 | # Written successfully, now rename it | |
|
265 | fileobj.close() | |
|
231 | 266 | |
|
232 | if os.name == 'nt' and os.path.exists(path): | |
|
233 | # Rename over existing file doesn't work on Windows | |
|
234 | os.remove(path) | |
|
267 | if os.name == 'nt' and os.path.exists(path): | |
|
268 | # Rename over existing file doesn't work on Windows | |
|
269 | os.remove(path) | |
|
235 | 270 | |
|
236 |
os.rename(tmp_ |
|
|
271 | os.rename(tmp_path, path) | |
|
237 | 272 | |
|
238 | 273 | |
|
239 | 274 | def raw_print(*args, **kw): |
@@ -15,6 +15,7 b' from __future__ import print_function' | |||
|
15 | 15 | from __future__ import absolute_import |
|
16 | 16 | |
|
17 | 17 | import io as stdlib_io |
|
18 | import os.path | |
|
18 | 19 | import sys |
|
19 | 20 | |
|
20 | 21 | from subprocess import Popen, PIPE |
@@ -23,8 +24,11 b' import unittest' | |||
|
23 | 24 | import nose.tools as nt |
|
24 | 25 | |
|
25 | 26 | from IPython.testing.decorators import skipif |
|
26 | from IPython.utils.io import Tee, capture_output, unicode_std_stream | |
|
27 | from IPython.utils.io import (Tee, capture_output, unicode_std_stream, | |
|
28 | atomic_writing, | |
|
29 | ) | |
|
27 | 30 | from IPython.utils.py3compat import doctest_refactor_print, PY3 |
|
31 | from IPython.utils.tempdir import TemporaryDirectory | |
|
28 | 32 | |
|
29 | 33 | if PY3: |
|
30 | 34 | from io import StringIO |
@@ -121,4 +125,27 b' def test_UnicodeStdStream_nowrap():' | |||
|
121 | 125 | nt.assert_is(unicode_std_stream(), sys.stdout) |
|
122 | 126 | assert not sys.stdout.closed |
|
123 | 127 | finally: |
|
124 | sys.stdout = orig_stdout No newline at end of file | |
|
128 | sys.stdout = orig_stdout | |
|
129 | ||
|
130 | def test_atomic_writing(): | |
|
131 | class CustomExc(Exception): pass | |
|
132 | ||
|
133 | with TemporaryDirectory() as td: | |
|
134 | f1 = os.path.join(td, 'penguin') | |
|
135 | with stdlib_io.open(f1, 'w') as f: | |
|
136 | f.write(u'Before') | |
|
137 | ||
|
138 | with nt.assert_raises(CustomExc): | |
|
139 | with atomic_writing(f1) as f: | |
|
140 | f.write(u'Failing write') | |
|
141 | raise CustomExc | |
|
142 | ||
|
143 | # Because of the exception, the file should not have been modified | |
|
144 | with stdlib_io.open(f1, 'r') as f: | |
|
145 | nt.assert_equal(f.read(), u'Before') | |
|
146 | ||
|
147 | with atomic_writing(f1) as f: | |
|
148 | f.write(u'Overwritten') | |
|
149 | ||
|
150 | with stdlib_io.open(f1, 'r') as f: | |
|
151 | nt.assert_equal(f.read(), u'Overwritten') |
General Comments 0
You need to be logged in to leave comments.
Login now