server: Change capabilities methods to classmethods.
[jelmer/dulwich-libgit2.git] / dulwich / file.py
1 # file.py -- Safe access to git files
2 # Copyright (C) 2010 Google, Inc.
3 #
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.
8 #
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.
13 #
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,
17 # MA  02110-1301, USA.
18
19 """Safe access to git files."""
20
21 import errno
22 import os
23 import tempfile
24
25 def ensure_dir_exists(dirname):
26     """Ensure a directory exists, creating if necessary."""
27     try:
28         os.makedirs(dirname)
29     except OSError, e:
30         if e.errno != errno.EEXIST:
31             raise
32
33 def fancy_rename(oldname, newname):
34     """Rename file with temporary backup file to rollback if rename fails"""
35     if not os.path.exists(newname):
36         try:
37             os.rename(oldname, newname)
38         except OSError, e:
39             raise
40         return
41
42     # destination file exists
43     try:
44         (fd, tmpfile) = tempfile.mkstemp(".tmp", prefix=oldname+".", dir=".")
45         os.close(fd)
46         os.remove(tmpfile)
47     except OSError, e:
48         # either file could not be created (e.g. permission problem)
49         # or could not be deleted (e.g. rude virus scanner)
50         raise
51     try:
52         os.rename(newname, tmpfile)
53     except OSError, e:
54         raise   # no rename occurred
55     try:
56         os.rename(oldname, newname)
57     except OSError, e:
58         os.rename(tmpfile, newname)
59         raise
60     os.remove(tmpfile)
61
62
63 def GitFile(filename, mode='rb', bufsize=-1):
64     """Create a file object that obeys the git file locking protocol.
65
66     :return: a builtin file object or a _GitFile object
67
68     :note: See _GitFile for a description of the file locking protocol.
69
70     Only read-only and write-only (binary) modes are supported; r+, w+, and a
71     are not.  To read and write from the same file, you can take advantage of
72     the fact that opening a file for write does not actually open the file you
73     request:
74
75     >>> write_file = GitFile('filename', 'wb')
76     >>> read_file = GitFile('filename', 'rb')
77     >>> read_file.readlines()
78     ['contents\n', 'of\n', 'the\n', 'file\n']
79     >>> write_file.write('foo')
80     >>> read_file.close()
81     >>> write_file.close()
82     >>> new_file = GitFile('filename', 'rb')
83     'foo'
84     >>> new_file.close()
85     >>> other_file = GitFile('filename', 'wb')
86     Traceback (most recent call last):
87         ...
88     OSError: [Errno 17] File exists: 'filename.lock'
89     """
90     if 'a' in mode:
91         raise IOError('append mode not supported for Git files')
92     if '+' in mode:
93         raise IOError('read/write mode not supported for Git files')
94     if 'b' not in mode:
95         raise IOError('text mode not supported for Git files')
96     if 'w' in mode:
97         return _GitFile(filename, mode, bufsize)
98     else:
99         return file(filename, mode, bufsize)
100
101
102 class _GitFile(object):
103     """File that follows the git locking protocol for writes.
104
105     All writes to a file foo will be written into foo.lock in the same
106     directory, and the lockfile will be renamed to overwrite the original file
107     on close.
108
109     :note: You *must* call close() or abort() on a _GitFile for the lock to be
110         released. Typically this will happen in a finally block.
111     """
112
113     PROXY_PROPERTIES = set(['closed', 'encoding', 'errors', 'mode', 'name',
114                             'newlines', 'softspace'])
115     PROXY_METHODS = ('__iter__', 'flush', 'fileno', 'isatty', 'next', 'read',
116                      'readline', 'readlines', 'xreadlines', 'seek', 'tell',
117                      'truncate', 'write', 'writelines')
118     def __init__(self, filename, mode, bufsize):
119         self._filename = filename
120         self._lockfilename = '%s.lock' % self._filename
121         fd = os.open(self._lockfilename,
122             os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0))
123         self._file = os.fdopen(fd, mode, bufsize)
124         self._closed = False
125
126         for method in self.PROXY_METHODS:
127             setattr(self, method, getattr(self._file, method))
128
129     def abort(self):
130         """Close and discard the lockfile without overwriting the target.
131
132         If the file is already closed, this is a no-op.
133         """
134         if self._closed:
135             return
136         self._file.close()
137         try:
138             os.remove(self._lockfilename)
139             self._closed = True
140         except OSError, e:
141             # The file may have been removed already, which is ok.
142             if e.errno != errno.ENOENT:
143                 raise
144             self._closed = True
145
146     def close(self):
147         """Close this file, saving the lockfile over the original.
148
149         :note: If this method fails, it will attempt to delete the lockfile.
150             However, it is not guaranteed to do so (e.g. if a filesystem becomes
151             suddenly read-only), which will prevent future writes to this file
152             until the lockfile is removed manually.
153         :raises OSError: if the original file could not be overwritten. The lock
154             file is still closed, so further attempts to write to the same file
155             object will raise ValueError.
156         """
157         if self._closed:
158             return
159         self._file.close()
160         try:
161             try:
162                 os.rename(self._lockfilename, self._filename)
163             except OSError, e:
164                 # Windows versions prior to Vista don't support atomic renames
165                 if e.errno != errno.EEXIST:
166                     raise
167                 fancy_rename(self._lockfilename, self._filename)
168         finally:
169             self.abort()
170
171     def __getattr__(self, name):
172         """Proxy property calls to the underlying file."""
173         if name in self.PROXY_PROPERTIES:
174             return getattr(self._file, name)
175         raise AttributeError(name)