~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lockable_files.py

Merge in bzrdir work to enable checkout improvements.

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
            st = self._transport.stat('.')
 
80
        except errors.TransportNotPossible:
 
81
            self._dir_mode = 0755
 
82
            self._file_mode = 0644
 
83
        else:
 
84
            self._dir_mode = st.st_mode & 07777
 
85
            # Remove the sticky and execute bits for files
 
86
            self._file_mode = self._dir_mode & ~07111
 
87
        if not self._set_dir_mode:
 
88
            self._dir_mode = None
 
89
        if not self._set_file_mode:
 
90
            self._file_mode = None
 
91
 
 
92
    def controlfilename(self, file_or_path):
 
93
        """Return location relative to branch."""
 
94
        return self._transport.abspath(self._escape(file_or_path))
 
95
 
 
96
    @deprecated_method(zero_eight)
 
97
    def controlfile(self, file_or_path, mode='r'):
 
98
        """Open a control file for this branch.
 
99
 
 
100
        There are two classes of file in a lockable directory: text
 
101
        and binary.  binary files are untranslated byte streams.  Text
 
102
        control files are stored with Unix newlines and in UTF-8, even
 
103
        if the platform or locale defaults are different.
 
104
 
 
105
        Such files are not openable in write mode : they are managed via
 
106
        put and put_utf8 which atomically replace old versions using
 
107
        atomicfile.
 
108
        """
 
109
 
 
110
        relpath = self._escape(file_or_path)
 
111
        #TODO: codecs.open() buffers linewise, so it was overloaded with
 
112
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
113
        if mode == 'rb': 
 
114
            return self.get(relpath)
 
115
        elif mode == 'wb':
 
116
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put[_utf8]")
 
117
        elif mode == 'r':
 
118
            return self.get_utf8(relpath)
 
119
        elif mode == 'w':
 
120
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put[_utf8]")
 
121
        else:
 
122
            raise BzrError("invalid controlfile mode %r" % mode)
 
123
 
 
124
    @needs_read_lock
 
125
    def get(self, relpath):
 
126
        """Get a file as a bytestream."""
 
127
        relpath = self._escape(relpath)
 
128
        return self._transport.get(relpath)
 
129
 
 
130
    @needs_read_lock
 
131
    def get_utf8(self, relpath):
 
132
        """Get a file as a unicode stream."""
 
133
        relpath = self._escape(relpath)
 
134
        # DO NOT introduce an errors=replace here.
 
135
        return codecs.getreader('utf-8')(self._transport.get(relpath))
 
136
 
 
137
    @needs_write_lock
 
138
    def put(self, path, file):
 
139
        """Write a file.
 
140
        
 
141
        :param path: The path to put the file, relative to the .bzr control
 
142
                     directory
 
143
        :param f: A file-like or string object whose contents should be copied.
 
144
        """
 
145
        self._transport.put(self._escape(path), file, mode=self._file_mode)
 
146
 
 
147
    @needs_write_lock
 
148
    def put_utf8(self, path, a_string):
 
149
        """Write a string, encoding as utf-8.
 
150
 
 
151
        :param path: The path to put the string, relative to the transport root.
 
152
        :param string: A file-like or string object whose contents should be copied.
 
153
        """
 
154
        # IterableFile would not be needed if Transport.put took iterables
 
155
        # instead of files.  ADHB 2005-12-25
 
156
        # RBC 20060103 surely its not needed anyway, with codecs transcode
 
157
        # file support ?
 
158
        # JAM 20060103 We definitely don't want encode(..., 'replace')
 
159
        # these are valuable files which should have exact contents.
 
160
        if not isinstance(a_string, basestring):
 
161
            raise errors.BzrBadParameterNotString(a_string)
 
162
        self.put(path, StringIO(a_string.encode('utf-8')))
 
163
 
 
164
    def lock_write(self):
 
165
        mutter("lock write: %s (%s)", self, self._lock_count)
 
166
        # TODO: Upgrade locking to support using a Transport,
 
167
        # and potentially a remote locking protocol
 
168
        if self._lock_mode:
 
169
            if self._lock_mode != 'w':
 
170
                raise ReadOnlyError("can't upgrade to a write lock from %r" %
 
171
                                self._lock_mode)
 
172
            self._lock_count += 1
 
173
        else:
 
174
            self._lock = self._transport.lock_write(
 
175
                    self._escape(self.lock_name))
 
176
            self._lock_mode = 'w'
 
177
            self._lock_count = 1
 
178
            self._set_transaction(transactions.PassThroughTransaction())
 
179
 
 
180
    def lock_read(self):
 
181
        mutter("lock read: %s (%s)", self, self._lock_count)
 
182
        if self._lock_mode:
 
183
            assert self._lock_mode in ('r', 'w'), \
 
184
                   "invalid lock mode %r" % self._lock_mode
 
185
            self._lock_count += 1
 
186
        else:
 
187
            self._lock = self._transport.lock_read(
 
188
                    self._escape(self.lock_name))
 
189
            self._lock_mode = 'r'
 
190
            self._lock_count = 1
 
191
            self._set_transaction(transactions.ReadOnlyTransaction())
 
192
            # 5K may be excessive, but hey, its a knob.
 
193
            self.get_transaction().set_cache_size(5000)
 
194
                        
 
195
    def unlock(self):
 
196
        mutter("unlock: %s (%s)", self, self._lock_count)
 
197
        if not self._lock_mode:
 
198
            raise LockError('branch %r is not locked' % (self))
 
199
 
 
200
        if self._lock_count > 1:
 
201
            self._lock_count -= 1
 
202
        else:
 
203
            self._finish_transaction()
 
204
            self._lock.unlock()
 
205
            self._lock = None
 
206
            self._lock_mode = self._lock_count = None
 
207
 
 
208
    def get_transaction(self):
 
209
        """Return the current active transaction.
 
210
 
 
211
        If no transaction is active, this returns a passthrough object
 
212
        for which all data is immediately flushed and no caching happens.
 
213
        """
 
214
        if self._transaction is None:
 
215
            return transactions.PassThroughTransaction()
 
216
        else:
 
217
            return self._transaction
 
218
 
 
219
    def _set_transaction(self, new_transaction):
 
220
        """Set a new active transaction."""
 
221
        if self._transaction is not None:
 
222
            raise errors.LockError('Branch %s is in a transaction already.' %
 
223
                                   self)
 
224
        self._transaction = new_transaction
 
225
 
 
226
    def _finish_transaction(self):
 
227
        """Exit the current transaction."""
 
228
        if self._transaction is None:
 
229
            raise errors.LockError('Branch %s is not in a transaction' %
 
230
                                   self)
 
231
        transaction = self._transaction
 
232
        self._transaction = None
 
233
        transaction.finish()