From 4f0beac1ee3e0ad36a115d975d246e9bf4f7fbd4 2014-08-04 18:22:42 From: Thomas Kluyver Date: 2014-08-04 18:22:42 Subject: [PATCH] Implement atomic save Ping @fperez, this should avoid issues with corrupted/lost notebooks when the disk is full, though I haven't worked out how to test it just yet. Closes gh-6254 --- diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index 59da309..ad36f39 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -13,6 +13,7 @@ from tornado import web from .manager import ContentsManager from IPython.nbformat import current +from IPython.utils.io import atomic_writing from IPython.utils.path import ensure_dir_exists from IPython.utils.traitlets import Unicode, Bool, TraitError from IPython.utils.py3compat import getcwd @@ -295,7 +296,7 @@ class FileContentsManager(ContentsManager): if 'name' in nb['metadata']: nb['metadata']['name'] = u'' - with io.open(os_path, 'w', encoding='utf-8') as f: + with atomic_writing(os_path, encoding='utf-8') as f: current.write(nb, f, u'json') def _save_file(self, os_path, model, name='', path=''): @@ -312,7 +313,7 @@ class FileContentsManager(ContentsManager): bcontent = base64.decodestring(b64_bytes) except Exception as e: raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e)) - with io.open(os_path, 'wb') as f: + with atomic_writing(os_path, 'wb') as f: f.write(bcontent) def _save_directory(self, os_path, model, name='', path=''): diff --git a/IPython/utils/io.py b/IPython/utils/io.py index c3390ed..4f624ff 100644 --- a/IPython/utils/io.py +++ b/IPython/utils/io.py @@ -16,6 +16,7 @@ from __future__ import absolute_import # Imports #----------------------------------------------------------------------------- import codecs +from contextlib import contextmanager import os import sys import tempfile @@ -217,6 +218,23 @@ def temp_pyfile(src, ext='.py'): f.flush() return fname, f +@contextmanager +def atomic_writing(path, mode='w', encoding='utf-8', **kwargs): + tmp_file = path + '.tmp-write' + if 'b' in mode: + encoding = None + + with open(tmp_file, mode, encoding=encoding, **kwargs) as f: + yield f + + # Written successfully, now rename it + + if os.name == 'nt' and os.path.exists(path): + # Rename over existing file doesn't work on Windows + os.remove(path) + + os.rename(tmp_file, path) + def raw_print(*args, **kw): """Raw print to sys.__stdout__, otherwise identical interface to print()."""