Return chunks from unpack_object.
[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
20 """Safe access to git files."""
21
22
23 import errno
24 import os
25
26 def ensure_dir_exists(dirname):
27     """Ensure a directory exists, creating if necessary."""
28     try:
29         os.makedirs(dirname)
30     except OSError, e:
31         if e.errno != errno.EEXIST:
32             raise
33
34 def GitFile(filename, mode='r', bufsize=-1):
35     """Create a file object that obeys the git file locking protocol.
36
37     See _GitFile for a description of the file locking protocol.
38
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
42     request:
43
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')
49     >>> read_file.close()
50     >>> write_file.close()
51     >>> new_file = GitFile('filename', 'rb')
52     'foo'
53     >>> new_file.close()
54     >>> other_file = GitFile('filename', 'wb')
55     Traceback (most recent call last):
56         ...
57     OSError: [Errno 17] File exists: 'filename.lock'
58
59     :return: a builtin file object or a _GitFile object
60     """
61     if 'a' in mode:
62         raise IOError('append mode not supported for Git files')
63     if '+' in mode:
64         raise IOError('read/write mode not supported for Git files')
65     if 'b' not in mode:
66         raise IOError('text mode not supported for Git files')
67     if 'w' in mode:
68         return _GitFile(filename, mode, bufsize)
69     else:
70         return file(filename, mode, bufsize)
71
72
73 class _GitFile(object):
74     """File that follows the git locking protocol for writes.
75
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
78     on close.
79
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.
82     """
83
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)
94         self._closed = False
95
96         for method in self.PROXY_METHODS:
97             setattr(self, method, getattr(self._file, method))
98
99     def abort(self):
100         """Close and discard the lockfile without overwriting the target.
101
102         If the file is already closed, this is a no-op.
103         """
104         if self._closed:
105             return
106         self._file.close()
107         try:
108             os.remove(self._lockfilename)
109             self._closed = True
110         except OSError, e:
111             # The file may have been removed already, which is ok.
112             if e.errno != errno.ENOENT:
113                 raise
114
115     def close(self):
116         """Close this file, saving the lockfile over the original.
117
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.
125         """
126         if self._closed:
127             return
128         self._file.close()
129         try:
130             os.rename(self._lockfilename, self._filename)
131         finally:
132             self.abort()
133
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)