~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lockable_files.py

MergeĀ fromĀ mainine.

Show diffs side-by-side

added added

removed removed

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