From 5c5b59bc92069533738cbdfe0bc682ffc8aca3ec Mon Sep 17 00:00:00 2001 From: TW Date: Sun, 12 Dec 2021 22:54:11 +0100 Subject: [PATCH] atomically create the CACHE_TAG file, see #6028 --- src/borg/helpers.py | 25 ++++++++++++++++--------- src/borg/platform/base.py | 9 ++++++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 75fa53c71..42c432bcb 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -584,15 +584,22 @@ def get_cache_dir(): cache_dir = os.environ.get('BORG_CACHE_DIR', os.path.join(cache_home, 'borg')) # Create path if it doesn't exist yet ensure_dir(cache_dir) - cache_fn = os.path.join(cache_dir, CACHE_TAG_NAME) - if not os.path.exists(cache_fn): - with open(cache_fn, 'wb') as fd: - fd.write(CACHE_TAG_CONTENTS) - fd.write(textwrap.dedent(""" - # This file is a cache directory tag created by Borg. - # For information about cache directory tags, see: - # http://www.bford.info/cachedir/spec.html - """).encode('ascii')) + cache_tag_fn = os.path.join(cache_dir, CACHE_TAG_NAME) + if not os.path.exists(cache_tag_fn): + cache_tag_contents = CACHE_TAG_CONTENTS + textwrap.dedent(""" + # This file is a cache directory tag created by Borg. + # For information about cache directory tags, see: + # http://www.bford.info/cachedir/spec.html + """).encode('ascii') + from .platform import SaveFile + try: + with SaveFile(cache_tag_fn, binary=True) as fd: + fd.write(cache_tag_contents) + except FileExistsError: + # if we have multiple SaveFile calls running in parallel for same cache_tag_fn, + # it is fine if just one (usually first/quicker one) of them run gets through + # and all others raise FileExistsError. + pass return cache_dir diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index b046b6597..c0b990c9c 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -97,12 +97,14 @@ class SyncFile: Note that POSIX doesn't specify *anything* about power failures (or similar failures). A system that routinely loses files or corrupts file on power loss is POSIX compliant. + Calling SyncFile(path) for an existing path will raise FileExistsError, see comment in __init__. + TODO: Use F_FULLSYNC on OSX. TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH. """ def __init__(self, path, binary=False): - mode = 'xb' if binary else 'x' + mode = 'xb' if binary else 'x' # x -> raise FileExists exception in open() if file exists already self.fd = open(path, mode) self.fileno = self.fd.fileno() @@ -149,6 +151,11 @@ class SaveFile: On a journaling file system the file contents are always updated atomically and won't become corrupted, even on power failures or crashes (for caveats see SyncFile). + + Calling SaveFile(path) in parallel for same path is safe (even when using the same SUFFIX), but the + caller needs to catch potential FileExistsError exceptions that may happen in this racy situation. + The caller executing SaveFile->SyncFile->open() first will win. + All other callers will raise a FileExistsError in open(), at least until the os.replace is executed. """ SUFFIX = '.tmp'