diff --git a/src/borg/helpers/fs.py b/src/borg/helpers/fs.py index 2ec90a0b7..d52ce5f12 100644 --- a/src/borg/helpers/fs.py +++ b/src/borg/helpers/fs.py @@ -92,15 +92,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 ba94f91d0..c6351b24b 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -141,12 +141,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() @@ -193,6 +195,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'