/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/lockable_files.py

(test_escaped_store) fix typo

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
from cStringIO import StringIO
 
18
import codecs
 
19
#import traceback
 
20
 
 
21
import bzrlib
 
22
from bzrlib.decorators import *
 
23
import bzrlib.errors as errors
 
24
from bzrlib.errors import LockError, ReadOnlyError
 
25
from bzrlib.osutils import file_iterator, safe_unicode
 
26
from bzrlib.symbol_versioning import *
 
27
from bzrlib.symbol_versioning import deprecated_method, zero_eight
 
28
from bzrlib.trace import mutter, note
 
29
import bzrlib.transactions as transactions
 
30
 
 
31
# XXX: The tracking here of lock counts and whether the lock is held is
 
32
# somewhat redundant with what's done in LockDir; the main difference is that
 
33
# LockableFiles permits reentrancy.
 
34
 
 
35
class LockableFiles(object):
 
36
    """Object representing a set of related files locked within the same scope.
 
37
 
 
38
    These files are used by a WorkingTree, Repository or Branch, and should
 
39
    generally only be touched by that object.
 
40
 
 
41
    LockableFiles also provides some policy on top of Transport for encoding
 
42
    control files as utf-8.
 
43
 
 
44
    LockableFiles manage a lock count and can be locked repeatedly by
 
45
    a single caller.  (The underlying lock implementation generally does not
 
46
    support this.)
 
47
 
 
48
    Instances of this class are often called control_files.
 
49
    
 
50
    This object builds on top of a Transport, which is used to actually write
 
51
    the files to disk, and an OSLock or LockDir, which controls how access to
 
52
    the files is controlled.  The particular type of locking used is set when
 
53
    the object is constructed.  In older formats OSLocks are used everywhere.
 
54
    in newer formats a LockDir is used for Repositories and Branches, and 
 
55
    OSLocks for the local filesystem.
 
56
    """
 
57
 
 
58
    # _lock_mode: None, or 'r' or 'w'
 
59
 
 
60
    # _lock_count: If _lock_mode is true, a positive count of the number of
 
61
    # times the lock has been taken *by this process*.   
 
62
    
 
63
    # If set to False (by a plugin, etc) BzrBranch will not set the
 
64
    # mode on created files or directories
 
65
    _set_file_mode = True
 
66
    _set_dir_mode = True
 
67
 
 
68
    def __init__(self, transport, lock_name, lock_class):
 
69
        """Create a LockableFiles group
 
70
 
 
71
        :param transport: Transport pointing to the directory holding the 
 
72
            control files and lock.
 
73
        :param lock_name: Name of the lock guarding these files.
 
74
        :param lock_class: Class of lock strategy to use: typically
 
75
            either LockDir or TransportLock.
 
76
        """
 
77
        object.__init__(self)
 
78
        self._transport = transport
 
79
        self.lock_name = lock_name
 
80
        self._transaction = None
 
81
        self._find_modes()
 
82
        self._lock_mode = None
 
83
        self._lock_count = 0
 
84
        esc_name = self._escape(lock_name)
 
85
        self._lock = lock_class(transport, esc_name,
 
86
                                file_modebits=self._file_mode,
 
87
                                dir_modebits=self._dir_mode)
 
88
 
 
89
    def create_lock(self):
 
90
        """Create the lock.
 
91
 
 
92
        This should normally be called only when the LockableFiles directory
 
93
        is first created on disk.
 
94
        """
 
95
        self._lock.create()
 
96
 
 
97
    def __repr__(self):
 
98
        return '%s(%r)' % (self.__class__.__name__,
 
99
                           self._transport)
 
100
    def __str__(self):
 
101
        return 'LockableFiles(%s, %s)' % (self.lock_name, self._transport.base)
 
102
 
 
103
    def __del__(self):
 
104
        if self.is_locked():
 
105
            # XXX: This should show something every time, and be suitable for
 
106
            # headless operation and embedding
 
107
            from warnings import warn
 
108
            warn("file group %r was not explicitly unlocked" % self)
 
109
            self._lock.unlock()
 
110
 
 
111
    def _escape(self, file_or_path):
 
112
        if not isinstance(file_or_path, basestring):
 
113
            file_or_path = '/'.join(file_or_path)
 
114
        if file_or_path == '':
 
115
            return u''
 
116
        return bzrlib.transport.urlescape(safe_unicode(file_or_path))
 
117
 
 
118
    def _find_modes(self):
 
119
        """Determine the appropriate modes for files and directories."""
 
120
        try:
 
121
            st = self._transport.stat('.')
 
122
        except errors.TransportNotPossible:
 
123
            self._dir_mode = 0755
 
124
            self._file_mode = 0644
 
125
        else:
 
126
            self._dir_mode = st.st_mode & 07777
 
127
            # Remove the sticky and execute bits for files
 
128
            self._file_mode = self._dir_mode & ~07111
 
129
        if not self._set_dir_mode:
 
130
            self._dir_mode = None
 
131
        if not self._set_file_mode:
 
132
            self._file_mode = None
 
133
 
 
134
    def controlfilename(self, file_or_path):
 
135
        """Return location relative to branch."""
 
136
        return self._transport.abspath(self._escape(file_or_path))
 
137
 
 
138
    @deprecated_method(zero_eight)
 
139
    def controlfile(self, file_or_path, mode='r'):
 
140
        """Open a control file for this branch.
 
141
 
 
142
        There are two classes of file in a lockable directory: text
 
143
        and binary.  binary files are untranslated byte streams.  Text
 
144
        control files are stored with Unix newlines and in UTF-8, even
 
145
        if the platform or locale defaults are different.
 
146
 
 
147
        Such files are not openable in write mode : they are managed via
 
148
        put and put_utf8 which atomically replace old versions using
 
149
        atomicfile.
 
150
        """
 
151
 
 
152
        relpath = self._escape(file_or_path)
 
153
        # TODO: codecs.open() buffers linewise, so it was overloaded with
 
154
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
155
        if mode == 'rb': 
 
156
            return self.get(relpath)
 
157
        elif mode == 'wb':
 
158
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put[_utf8]")
 
159
        elif mode == 'r':
 
160
            return self.get_utf8(relpath)
 
161
        elif mode == 'w':
 
162
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put[_utf8]")
 
163
        else:
 
164
            raise BzrError("invalid controlfile mode %r" % mode)
 
165
 
 
166
    @needs_read_lock
 
167
    def get(self, relpath):
 
168
        """Get a file as a bytestream."""
 
169
        relpath = self._escape(relpath)
 
170
        return self._transport.get(relpath)
 
171
 
 
172
    @needs_read_lock
 
173
    def get_utf8(self, relpath):
 
174
        """Get a file as a unicode stream."""
 
175
        relpath = self._escape(relpath)
 
176
        # DO NOT introduce an errors=replace here.
 
177
        return codecs.getreader('utf-8')(self._transport.get(relpath))
 
178
 
 
179
    @needs_write_lock
 
180
    def put(self, path, file):
 
181
        """Write a file.
 
182
        
 
183
        :param path: The path to put the file, relative to the .bzr control
 
184
                     directory
 
185
        :param f: A file-like or string object whose contents should be copied.
 
186
        """
 
187
        self._transport.put(self._escape(path), file, mode=self._file_mode)
 
188
 
 
189
    @needs_write_lock
 
190
    def put_utf8(self, path, a_string):
 
191
        """Write a string, encoding as utf-8.
 
192
 
 
193
        :param path: The path to put the string, relative to the transport root.
 
194
        :param string: A file-like or string object whose contents should be copied.
 
195
        """
 
196
        # IterableFile would not be needed if Transport.put took iterables
 
197
        # instead of files.  ADHB 2005-12-25
 
198
        # RBC 20060103 surely its not needed anyway, with codecs transcode
 
199
        # file support ?
 
200
        # JAM 20060103 We definitely don't want encode(..., 'replace')
 
201
        # these are valuable files which should have exact contents.
 
202
        if not isinstance(a_string, basestring):
 
203
            raise errors.BzrBadParameterNotString(a_string)
 
204
        self.put(path, StringIO(a_string.encode('utf-8')))
 
205
 
 
206
    def lock_write(self):
 
207
        # mutter("lock write: %s (%s)", self, self._lock_count)
 
208
        # TODO: Upgrade locking to support using a Transport,
 
209
        # and potentially a remote locking protocol
 
210
        if self._lock_mode:
 
211
            if self._lock_mode != 'w' or not self.get_transaction().writeable():
 
212
                raise ReadOnlyError(self)
 
213
            self._lock_count += 1
 
214
        else:
 
215
            self._lock.lock_write()
 
216
            #note('write locking %s', self)
 
217
            #traceback.print_stack()
 
218
            self._lock_mode = 'w'
 
219
            self._lock_count = 1
 
220
            self._set_transaction(transactions.WriteTransaction())
 
221
 
 
222
    def lock_read(self):
 
223
        # mutter("lock read: %s (%s)", self, self._lock_count)
 
224
        if self._lock_mode:
 
225
            assert self._lock_mode in ('r', 'w'), \
 
226
                   "invalid lock mode %r" % self._lock_mode
 
227
            self._lock_count += 1
 
228
        else:
 
229
            self._lock.lock_read()
 
230
            #note('read locking %s', self)
 
231
            #traceback.print_stack()
 
232
            self._lock_mode = 'r'
 
233
            self._lock_count = 1
 
234
            self._set_transaction(transactions.ReadOnlyTransaction())
 
235
            # 5K may be excessive, but hey, its a knob.
 
236
            self.get_transaction().set_cache_size(5000)
 
237
                        
 
238
    def unlock(self):
 
239
        # mutter("unlock: %s (%s)", self, self._lock_count)
 
240
        if not self._lock_mode:
 
241
            raise errors.LockNotHeld(self)
 
242
        if self._lock_count > 1:
 
243
            self._lock_count -= 1
 
244
        else:
 
245
            #note('unlocking %s', self)
 
246
            #traceback.print_stack()
 
247
            self._finish_transaction()
 
248
            self._lock.unlock()
 
249
            self._lock_mode = self._lock_count = None
 
250
 
 
251
    def is_locked(self):
 
252
        """Return true if this LockableFiles group is locked"""
 
253
        return self._lock_count >= 1
 
254
 
 
255
    def get_transaction(self):
 
256
        """Return the current active transaction.
 
257
 
 
258
        If no transaction is active, this returns a passthrough object
 
259
        for which all data is immediately flushed and no caching happens.
 
260
        """
 
261
        if self._transaction is None:
 
262
            return transactions.PassThroughTransaction()
 
263
        else:
 
264
            return self._transaction
 
265
 
 
266
    def _set_transaction(self, new_transaction):
 
267
        """Set a new active transaction."""
 
268
        if self._transaction is not None:
 
269
            raise errors.LockError('Branch %s is in a transaction already.' %
 
270
                                   self)
 
271
        self._transaction = new_transaction
 
272
 
 
273
    def _finish_transaction(self):
 
274
        """Exit the current transaction."""
 
275
        if self._transaction is None:
 
276
            raise errors.LockError('Branch %s is not in a transaction' %
 
277
                                   self)
 
278
        transaction = self._transaction
 
279
        self._transaction = None
 
280
        transaction.finish()
 
281
 
 
282
 
 
283
class TransportLock(object):
 
284
    """Locking method which uses transport-dependent locks.
 
285
 
 
286
    On the local filesystem these transform into OS-managed locks.
 
287
 
 
288
    These do not guard against concurrent access via different
 
289
    transports.
 
290
 
 
291
    This is suitable for use only in WorkingTrees (which are at present
 
292
    always local).
 
293
    """
 
294
    def __init__(self, transport, escaped_name, file_modebits, dir_modebits):
 
295
        self._transport = transport
 
296
        self._escaped_name = escaped_name
 
297
        self._file_modebits = file_modebits
 
298
        self._dir_modebits = dir_modebits
 
299
 
 
300
    def lock_write(self):
 
301
        self._lock = self._transport.lock_write(self._escaped_name)
 
302
 
 
303
    def lock_read(self):
 
304
        self._lock = self._transport.lock_read(self._escaped_name)
 
305
 
 
306
    def unlock(self):
 
307
        self._lock.unlock()
 
308
        self._lock = None
 
309
 
 
310
    def create(self):
 
311
        """Create lock mechanism"""
 
312
        # for old-style locks, create the file now
 
313
        self._transport.put(self._escaped_name, StringIO(), 
 
314
                            mode=self._file_modebits)