|
@@ -0,0 +1,106 @@
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+"""Safe access to git files."""
|
|
|
+
|
|
|
+
|
|
|
+import errno
|
|
|
+import os
|
|
|
+
|
|
|
+
|
|
|
+def GitFile(filename, mode='r', bufsize=-1):
|
|
|
+ if 'a' in mode:
|
|
|
+ raise IOError('append mode not supported for Git files')
|
|
|
+ if 'w' in mode:
|
|
|
+ return _GitFile(filename, mode, bufsize)
|
|
|
+ else:
|
|
|
+ return file(filename, mode, bufsize)
|
|
|
+
|
|
|
+
|
|
|
+class _GitFile(object):
|
|
|
+ """File that follows the git locking protocol for writes.
|
|
|
+
|
|
|
+ All writes to a file foo will be written into foo.lock in the same
|
|
|
+ directory, and the lockfile will be renamed to overwrite the original file
|
|
|
+ on close. The lockfile is automatically removed upon filesystem error.
|
|
|
+
|
|
|
+ :note: You *must* call close() or abort() on a _GitFile for the lock to be
|
|
|
+ released. Typically this will happen in a finally block.
|
|
|
+ """
|
|
|
+
|
|
|
+ PROXY_PROPERTIES = set(['closed', 'encoding', 'errors', 'mode', 'name',
|
|
|
+ 'newlines', 'softspace'])
|
|
|
+ PROXY_METHODS = ('__iter__', 'flush', 'fileno', 'isatty', 'next', 'read',
|
|
|
+ 'readline', 'readlines', 'xreadlines', 'seek', 'tell',
|
|
|
+ 'truncate', 'write', 'writelines')
|
|
|
+ def __init__(self, filename, mode, bufsize):
|
|
|
+ self._filename = filename
|
|
|
+ self._lockfilename = '%s.lock' % self._filename
|
|
|
+ fd = os.open(self._lockfilename, os.O_RDWR | os.O_CREAT | os.O_EXCL)
|
|
|
+ self._file = os.fdopen(fd, mode, bufsize)
|
|
|
+
|
|
|
+ for method in self.PROXY_METHODS:
|
|
|
+ setattr(self, method,
|
|
|
+ self._safe_method(getattr(self._file, method)))
|
|
|
+
|
|
|
+ def _safe_method(self, file_method):
|
|
|
+
|
|
|
+ def do_safe_method(*args):
|
|
|
+ try:
|
|
|
+ return file_method(*args)
|
|
|
+ except (OSError, IOError):
|
|
|
+ self.abort()
|
|
|
+ raise
|
|
|
+ return do_safe_method
|
|
|
+
|
|
|
+ def abort(self):
|
|
|
+ """Close and discard the lockfile without overwriting the target.
|
|
|
+
|
|
|
+ If the file is already closed, this is a no-op.
|
|
|
+ """
|
|
|
+ self._file.close()
|
|
|
+ try:
|
|
|
+ os.remove(self._lockfilename)
|
|
|
+ except OSError, e:
|
|
|
+
|
|
|
+ if e.errno != errno.ENOENT:
|
|
|
+ raise
|
|
|
+
|
|
|
+ def close(self):
|
|
|
+ """Close this file, saving the lockfile over the original.
|
|
|
+
|
|
|
+ :note: If this method fails, it will attempt to delete the lockfile.
|
|
|
+ However, it is not guaranteed to do so (e.g. if a filesystem becomes
|
|
|
+ suddenly read-only), which will prevent future writes to this file
|
|
|
+ until the lockfile is removed manually.
|
|
|
+ :raises OSError: if the original file could not be overwritten. The lock
|
|
|
+ file is still closed, so further attempts to write to the same file
|
|
|
+ object will raise ValueError.
|
|
|
+ """
|
|
|
+ self._file.close()
|
|
|
+ try:
|
|
|
+ os.rename(self._lockfilename, self._filename)
|
|
|
+ finally:
|
|
|
+ self.abort()
|
|
|
+
|
|
|
+ def __getattr__(self, name):
|
|
|
+ """Proxy property calls to the underlying file."""
|
|
|
+ if name in self.PROXY_PROPERTIES:
|
|
|
+ return getattr(self._file, name)
|
|
|
+ raise AttributeError(name)
|