From 36ebc827488aa8a42548d78b573a812b6925af11 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 9 Jul 2016 21:07:56 +0200 Subject: [PATCH] Add platform.SaveFile --- src/borg/platform/__init__.py | 2 +- src/borg/platform/base.py | 44 +++++++++++++++++++++++++++++++++-- src/borg/platform/linux.pyx | 4 ++-- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/borg/platform/__init__.py b/src/borg/platform/__init__.py index 29a97fc43..e1772c5e1 100644 --- a/src/borg/platform/__init__.py +++ b/src/borg/platform/__init__.py @@ -8,7 +8,7 @@ Public APIs are documented in platform.base. from .base import acl_get, acl_set from .base import set_flags, get_flags -from .base import SyncFile, sync_dir, fdatasync +from .base import SaveFile, SyncFile, sync_dir, fdatasync from .base import swidth, API_VERSION if sys.platform.startswith('linux'): # pragma: linux only diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index ef8853e31..9580f06ba 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -80,8 +80,11 @@ class SyncFile: TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH. """ - def __init__(self, path): - self.fd = open(path, 'xb') + def __init__(self, path, binary=True): + mode = 'x' + if binary: + mode += 'b' + self.fd = open(path, mode) self.fileno = self.fd.fileno() def __enter__(self): @@ -112,6 +115,43 @@ class SyncFile: platform.sync_dir(os.path.dirname(self.fd.name)) +class SaveFile: + """ + Update file contents atomically. + + Must be used as a context manager (defining the scope of the transaction). + + On a journaling file system the file contents are always updated + atomically and won't become corrupted, even on pure failures or + crashes (for caveats see SyncFile). + """ + + SUFFIX = '.tmp' + + def __init__(self, path, binary=True): + self.binary = binary + self.path = path + self.tmppath = self.path + self.SUFFIX + + def __enter__(self): + from .. import platform + try: + os.unlink(self.tmppath) + except OSError: + pass + self.fd = platform.SyncFile(self.tmppath, self.binary) + return self.fd + + def __exit__(self, exc_type, exc_val, exc_tb): + from .. import platform + self.fd.close() + if exc_type is not None: + os.unlink(self.tmppath) + return + os.rename(self.tmppath, self.path) + platform.sync_dir(os.path.dirname(self.path)) + + def swidth(s): """terminal output width of string diff --git a/src/borg/platform/linux.pyx b/src/borg/platform/linux.pyx index 4bbdcc356..7dea321a0 100644 --- a/src/borg/platform/linux.pyx +++ b/src/borg/platform/linux.pyx @@ -228,8 +228,8 @@ class SyncFile(BaseSyncFile): disk in the immediate future. """ - def __init__(self, path): - super().__init__(path) + def __init__(self, path, binary=True): + super().__init__(path, binary) self.offset = 0 self.write_window = (16 * 1024 ** 2) & ~PAGE_MASK self.last_sync = 0