~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lockable_files.py

MergeĀ fromĀ jam-storage.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import bzrlib
 
2
import bzrlib.errors as errors
 
3
from bzrlib.errors import LockError, ReadOnlyError
 
4
from bzrlib.trace import mutter
 
5
import bzrlib.transactions as transactions
 
6
from osutils import file_iterator
 
7
 
 
8
class LockableFiles(object):
 
9
    """Object representing a set of lockable files
 
10
 
 
11
    _lock_mode
 
12
        None, or 'r' or 'w'
 
13
 
 
14
    _lock_count
 
15
        If _lock_mode is true, a positive count of the number of times the
 
16
        lock has been taken.
 
17
 
 
18
    _lock
 
19
        Lock object from bzrlib.lock.
 
20
    """
 
21
 
 
22
    _lock_mode = None
 
23
    _lock_count = None
 
24
    _lock = None
 
25
    # If set to False (by a plugin, etc) BzrBranch will not set the
 
26
    # mode on created files or directories
 
27
    _set_file_mode = True
 
28
    _set_dir_mode = True
 
29
 
 
30
    def __init__(self, transport, lock_name):
 
31
        object.__init__(self)
 
32
        self._transport = transport
 
33
        self.lock_name = lock_name
 
34
        self._transaction = None
 
35
        self._find_modes()
 
36
 
 
37
    def __del__(self):
 
38
        if self._lock_mode or self._lock:
 
39
            # XXX: This should show something every time, and be suitable for
 
40
            # headless operation and embedding
 
41
            from warnings import warn
 
42
            warn("file group %r was not explicitly unlocked" % self)
 
43
            self._lock.unlock()
 
44
 
 
45
    def _escape(self, file_or_path):
 
46
        if not isinstance(file_or_path, basestring):
 
47
            file_or_path = '/'.join(file_or_path)
 
48
        if file_or_path == '':
 
49
            return u''
 
50
        return bzrlib.transport.urlescape(unicode(file_or_path))
 
51
 
 
52
    def _find_modes(self):
 
53
        """Determine the appropriate modes for files and directories."""
 
54
        try:
 
55
            try:
 
56
                st = self._transport.stat(u'.')
 
57
            except errors.NoSuchFile:
 
58
                # The .bzr/ directory doesn't exist, try to
 
59
                # inherit the permissions from the parent directory
 
60
                # but only try 1 level up
 
61
                temp_transport = self._transport.clone('..')
 
62
                st = temp_transport.stat(u'.')
 
63
        except (errors.TransportNotPossible, errors.NoSuchFile):
 
64
            self._dir_mode = 0755
 
65
            self._file_mode = 0644
 
66
        else:
 
67
            self._dir_mode = st.st_mode & 07777
 
68
            # Remove the sticky and execute bits for files
 
69
            self._file_mode = self._dir_mode & ~07111
 
70
        if not self._set_dir_mode:
 
71
            self._dir_mode = None
 
72
        if not self._set_file_mode:
 
73
            self._file_mode = None
 
74
 
 
75
    def controlfilename(self, file_or_path):
 
76
        """Return location relative to branch."""
 
77
        return self._transport.abspath(self._escape(file_or_path))
 
78
 
 
79
    def controlfile(self, file_or_path, mode='r'):
 
80
        """Open a control file for this branch.
 
81
 
 
82
        There are two classes of file in the control directory: text
 
83
        and binary.  binary files are untranslated byte streams.  Text
 
84
        control files are stored with Unix newlines and in UTF-8, even
 
85
        if the platform or locale defaults are different.
 
86
 
 
87
        Controlfiles should almost never be opened in write mode but
 
88
        rather should be atomically copied and replaced using atomicfile.
 
89
        """
 
90
        import codecs
 
91
 
 
92
        relpath = self._escape(file_or_path)
 
93
        #TODO: codecs.open() buffers linewise, so it was overloaded with
 
94
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
95
        if mode == 'rb': 
 
96
            return self._transport.get(relpath)
 
97
        elif mode == 'wb':
 
98
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put[_utf8]")
 
99
        elif mode == 'r':
 
100
            # XXX: Do we really want errors='replace'?   Perhaps it should be
 
101
            # an error, or at least reported, if there's incorrectly-encoded
 
102
            # data inside a file.
 
103
            # <https://launchpad.net/products/bzr/+bug/3823>
 
104
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
 
105
        elif mode == 'w':
 
106
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put[_utf8]")
 
107
        else:
 
108
            raise BzrError("invalid controlfile mode %r" % mode)
 
109
 
 
110
    def put(self, path, file):
 
111
        """Write a file.
 
112
        
 
113
        :param path: The path to put the file, relative to the .bzr control
 
114
                     directory
 
115
        :param f: A file-like or string object whose contents should be copied.
 
116
        """
 
117
        if not self._lock_mode == 'w':
 
118
            raise ReadOnlyError()
 
119
        self._transport.put(self._escape(path), file, mode=self._file_mode)
 
120
 
 
121
    def put_utf8(self, path, file, mode=None):
 
122
        """Write a file, encoding as utf-8.
 
123
 
 
124
        :param path: The path to put the file, relative to the .bzr control
 
125
                     directory
 
126
        :param f: A file-like or string object whose contents should be copied.
 
127
        """
 
128
        import codecs
 
129
        from iterablefile import IterableFile
 
130
        ctrl_files = []
 
131
        if hasattr(file, 'read'):
 
132
            iterator = file_iterator(file)
 
133
        else:
 
134
            iterator = file
 
135
        # IterableFile would not be needed if Transport.put took iterables
 
136
        # instead of files.  ADHB 2005-12-25
 
137
        # RBC 20060103 surely its not needed anyway, with codecs transcode
 
138
        # file support ?
 
139
        # JAM 20060103 We definitely don't want encode(..., 'replace')
 
140
        # these are valuable files which should have exact contents.
 
141
        encoded_file = IterableFile(b.encode('utf-8') for b in 
 
142
                                    iterator)
 
143
        self.put(path, encoded_file)
 
144
 
 
145
    def lock_write(self):
 
146
        mutter("lock write: %s (%s)", self, self._lock_count)
 
147
        # TODO: Upgrade locking to support using a Transport,
 
148
        # and potentially a remote locking protocol
 
149
        if self._lock_mode:
 
150
            if self._lock_mode != 'w':
 
151
                raise LockError("can't upgrade to a write lock from %r" %
 
152
                                self._lock_mode)
 
153
            self._lock_count += 1
 
154
        else:
 
155
            self._lock = self._transport.lock_write(
 
156
                    self._escape(self.lock_name))
 
157
            self._lock_mode = 'w'
 
158
            self._lock_count = 1
 
159
            self._set_transaction(transactions.PassThroughTransaction())
 
160
 
 
161
    def lock_read(self):
 
162
        mutter("lock read: %s (%s)", self, self._lock_count)
 
163
        if self._lock_mode:
 
164
            assert self._lock_mode in ('r', 'w'), \
 
165
                   "invalid lock mode %r" % self._lock_mode
 
166
            self._lock_count += 1
 
167
        else:
 
168
            self._lock = self._transport.lock_read(
 
169
                    self._escape(self.lock_name))
 
170
            self._lock_mode = 'r'
 
171
            self._lock_count = 1
 
172
            self._set_transaction(transactions.ReadOnlyTransaction())
 
173
            # 5K may be excessive, but hey, its a knob.
 
174
            self.get_transaction().set_cache_size(5000)
 
175
                        
 
176
    def unlock(self):
 
177
        mutter("unlock: %s (%s)", self, self._lock_count)
 
178
        if not self._lock_mode:
 
179
            raise LockError('branch %r is not locked' % (self))
 
180
 
 
181
        if self._lock_count > 1:
 
182
            self._lock_count -= 1
 
183
        else:
 
184
            self._finish_transaction()
 
185
            self._lock.unlock()
 
186
            self._lock = None
 
187
            self._lock_mode = self._lock_count = None
 
188
 
 
189
    def get_transaction(self):
 
190
        """Return the current active transaction.
 
191
 
 
192
        If no transaction is active, this returns a passthrough object
 
193
        for which all data is immediately flushed and no caching happens.
 
194
        """
 
195
        if self._transaction is None:
 
196
            return transactions.PassThroughTransaction()
 
197
        else:
 
198
            return self._transaction
 
199
 
 
200
    def _set_transaction(self, new_transaction):
 
201
        """Set a new active transaction."""
 
202
        if self._transaction is not None:
 
203
            raise errors.LockError('Branch %s is in a transaction already.' %
 
204
                                   self)
 
205
        self._transaction = new_transaction
 
206
 
 
207
    def _finish_transaction(self):
 
208
        """Exit the current transaction."""
 
209
        if self._transaction is None:
 
210
            raise errors.LockError('Branch %s is not in a transaction' %
 
211
                                   self)
 
212
        transaction = self._transaction
 
213
        self._transaction = None
 
214
        transaction.finish()