|
@@ -1,6 +1,7 @@
|
|
|
import errno
|
|
|
import os
|
|
|
import socket
|
|
|
+import tempfile
|
|
|
import uuid
|
|
|
|
|
|
from borg.helpers import safe_unlink
|
|
@@ -103,10 +104,22 @@ class SyncFile:
|
|
|
TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH.
|
|
|
"""
|
|
|
|
|
|
- def __init__(self, path, binary=False):
|
|
|
+ def __init__(self, path, *, fd=None, binary=False):
|
|
|
+ """
|
|
|
+ Open a SyncFile.
|
|
|
+
|
|
|
+ :param path: full path/filename
|
|
|
+ :param fd: additionally to path, it is possible to give an already open OS-level fd
|
|
|
+ that corresponds to path (like from os.open(path, ...) or os.mkstemp(...))
|
|
|
+ :param binary: whether to open in binary mode, default is False.
|
|
|
+ """
|
|
|
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()
|
|
|
+ self.path = path
|
|
|
+ if fd is None:
|
|
|
+ self.f = open(path, mode=mode) # python file object
|
|
|
+ else:
|
|
|
+ self.f = os.fdopen(fd, mode=mode)
|
|
|
+ self.fd = self.f.fileno() # OS-level fd
|
|
|
|
|
|
def __enter__(self):
|
|
|
return self
|
|
@@ -115,7 +128,7 @@ class SyncFile:
|
|
|
self.close()
|
|
|
|
|
|
def write(self, data):
|
|
|
- self.fd.write(data)
|
|
|
+ self.f.write(data)
|
|
|
|
|
|
def sync(self):
|
|
|
"""
|
|
@@ -123,21 +136,21 @@ class SyncFile:
|
|
|
after sync().
|
|
|
"""
|
|
|
from .. import platform
|
|
|
- self.fd.flush()
|
|
|
- platform.fdatasync(self.fileno)
|
|
|
+ self.f.flush()
|
|
|
+ platform.fdatasync(self.fd)
|
|
|
# tell the OS that it does not need to cache what we just wrote,
|
|
|
# avoids spoiling the cache for the OS and other processes.
|
|
|
- safe_fadvise(self.fileno, 0, 0, 'DONTNEED')
|
|
|
+ safe_fadvise(self.fd, 0, 0, 'DONTNEED')
|
|
|
|
|
|
def close(self):
|
|
|
"""sync() and close."""
|
|
|
from .. import platform
|
|
|
dirname = None
|
|
|
try:
|
|
|
- dirname = os.path.dirname(self.fd.name)
|
|
|
+ dirname = os.path.dirname(self.path)
|
|
|
self.sync()
|
|
|
finally:
|
|
|
- self.fd.close()
|
|
|
+ self.f.close()
|
|
|
if dirname:
|
|
|
platform.sync_dir(dirname)
|
|
|
|
|
@@ -152,36 +165,41 @@ class SaveFile:
|
|
|
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.
|
|
|
+ SaveFile can safely by used in parallel (e.g. by multiple processes) to write
|
|
|
+ to the same target path. Whatever writer finishes last (executes the os.replace
|
|
|
+ last) "wins" and has successfully written its content to the target path.
|
|
|
+ Internally used temporary files are created in the target directory and are
|
|
|
+ named <BASENAME>-<RANDOMCHARS>.tmp and cleaned up in normal and error conditions.
|
|
|
"""
|
|
|
-
|
|
|
- SUFFIX = '.tmp'
|
|
|
-
|
|
|
def __init__(self, path, binary=False):
|
|
|
self.binary = binary
|
|
|
self.path = path
|
|
|
- self.tmppath = self.path + self.SUFFIX
|
|
|
+ self.dir = os.path.dirname(path)
|
|
|
+ self.tmp_prefix = os.path.basename(path) + '-'
|
|
|
+ self.tmp_fd = None # OS-level fd
|
|
|
+ self.tmp_fname = None # full path/filename corresponding to self.tmp_fd
|
|
|
+ self.f = None # python-file-like SyncFile
|
|
|
|
|
|
def __enter__(self):
|
|
|
from .. import platform
|
|
|
- try:
|
|
|
- safe_unlink(self.tmppath)
|
|
|
- except FileNotFoundError:
|
|
|
- pass
|
|
|
- self.fd = platform.SyncFile(self.tmppath, self.binary)
|
|
|
- return self.fd
|
|
|
+ self.tmp_fd, self.tmp_fname = tempfile.mkstemp(prefix=self.tmp_prefix, suffix='.tmp', dir=self.dir)
|
|
|
+ self.f = platform.SyncFile(self.tmp_fname, fd=self.tmp_fd, binary=self.binary)
|
|
|
+ return self.f
|
|
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
from .. import platform
|
|
|
- self.fd.close()
|
|
|
+ self.f.close() # this indirectly also closes self.tmp_fd
|
|
|
+ self.tmp_fd = None
|
|
|
if exc_type is not None:
|
|
|
- safe_unlink(self.tmppath)
|
|
|
- return
|
|
|
- os.replace(self.tmppath, self.path)
|
|
|
- platform.sync_dir(os.path.dirname(self.path))
|
|
|
+ safe_unlink(self.tmp_fname) # with-body has failed, clean up tmp file
|
|
|
+ return # continue processing the exception normally
|
|
|
+ try:
|
|
|
+ os.replace(self.tmp_fname, self.path) # POSIX: atomic rename
|
|
|
+ except OSError:
|
|
|
+ safe_unlink(self.tmp_fname) # rename has failed, clean up tmp file
|
|
|
+ raise
|
|
|
+ finally:
|
|
|
+ platform.sync_dir(self.dir)
|
|
|
|
|
|
|
|
|
def swidth(s):
|