1 # file.py -- Safe access to git files
2 # Copyright (C) 2010 Google, Inc.
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; version 2
7 # of the License or (at your option) a later version of the License.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20 """Safe access to git files."""
26 def ensure_dir_exists(dirname):
27 """Ensure a directory exists, creating if necessary."""
31 if e.errno != errno.EEXIST:
34 def GitFile(filename, mode='r', bufsize=-1):
35 """Create a file object that obeys the git file locking protocol.
37 See _GitFile for a description of the file locking protocol.
39 Only read-only and write-only (binary) modes are supported; r+, w+, and a
40 are not. To read and write from the same file, you can take advantage of
41 the fact that opening a file for write does not actually open the file you
44 >>> write_file = GitFile('filename', 'wb')
45 >>> read_file = GitFile('filename', 'rb')
46 >>> read_file.readlines()
47 ['contents\n', 'of\n', 'the\n', 'file\n']
48 >>> write_file.write('foo')
50 >>> write_file.close()
51 >>> new_file = GitFile('filename', 'rb')
54 >>> other_file = GitFile('filename', 'wb')
55 Traceback (most recent call last):
57 OSError: [Errno 17] File exists: 'filename.lock'
59 :return: a builtin file object or a _GitFile object
62 raise IOError('append mode not supported for Git files')
64 raise IOError('read/write mode not supported for Git files')
66 raise IOError('text mode not supported for Git files')
68 return _GitFile(filename, mode, bufsize)
70 return file(filename, mode, bufsize)
73 class _GitFile(object):
74 """File that follows the git locking protocol for writes.
76 All writes to a file foo will be written into foo.lock in the same
77 directory, and the lockfile will be renamed to overwrite the original file
80 :note: You *must* call close() or abort() on a _GitFile for the lock to be
81 released. Typically this will happen in a finally block.
84 PROXY_PROPERTIES = set(['closed', 'encoding', 'errors', 'mode', 'name',
85 'newlines', 'softspace'])
86 PROXY_METHODS = ('__iter__', 'flush', 'fileno', 'isatty', 'next', 'read',
87 'readline', 'readlines', 'xreadlines', 'seek', 'tell',
88 'truncate', 'write', 'writelines')
89 def __init__(self, filename, mode, bufsize):
90 self._filename = filename
91 self._lockfilename = '%s.lock' % self._filename
92 fd = os.open(self._lockfilename, os.O_RDWR | os.O_CREAT | os.O_EXCL)
93 self._file = os.fdopen(fd, mode, bufsize)
96 for method in self.PROXY_METHODS:
97 setattr(self, method, getattr(self._file, method))
100 """Close and discard the lockfile without overwriting the target.
102 If the file is already closed, this is a no-op.
108 os.remove(self._lockfilename)
111 # The file may have been removed already, which is ok.
112 if e.errno != errno.ENOENT:
116 """Close this file, saving the lockfile over the original.
118 :note: If this method fails, it will attempt to delete the lockfile.
119 However, it is not guaranteed to do so (e.g. if a filesystem becomes
120 suddenly read-only), which will prevent future writes to this file
121 until the lockfile is removed manually.
122 :raises OSError: if the original file could not be overwritten. The lock
123 file is still closed, so further attempts to write to the same file
124 object will raise ValueError.
130 os.rename(self._lockfilename, self._filename)
134 def __getattr__(self, name):
135 """Proxy property calls to the underlying file."""
136 if name in self.PROXY_PROPERTIES:
137 return getattr(self._file, name)
138 raise AttributeError(name)