From 4f0beac1ee3e0ad36a115d975d246e9bf4f7fbd4 2014-08-04 18:22:42
From: Thomas Kluyver <takowl@gmail.com>
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()."""