1
# Copyright (C) 2005 Canonical Ltd
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.
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.
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
17
from cStringIO import StringIO
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
31
class LockableFiles(object):
32
"""Object representing a set of files locked within the same scope
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
43
Lock object from bzrlib.lock.
49
# If set to False (by a plugin, etc) BzrBranch will not set the
50
# mode on created files or directories
54
def __init__(self, transport, lock_name):
56
self._transport = transport
57
self.lock_name = lock_name
58
self._transaction = None
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)
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 == '':
74
return bzrlib.transport.urlescape(safe_unicode(file_or_path))
76
def _find_modes(self):
77
"""Determine the appropriate modes for files and directories."""
79
st = self._transport.stat('.')
80
except errors.TransportNotPossible:
82
self._file_mode = 0644
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:
89
if not self._set_file_mode:
90
self._file_mode = None
92
def controlfilename(self, file_or_path):
93
"""Return location relative to branch."""
94
return self._transport.abspath(self._escape(file_or_path))
96
@deprecated_method(zero_eight)
97
def controlfile(self, file_or_path, mode='r'):
98
"""Open a control file for this branch.
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.
105
Such files are not openable in write mode : they are managed via
106
put and put_utf8 which atomically replace old versions using
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?
114
return self.get(relpath)
116
raise BzrError("Branch.controlfile(mode='wb') is not supported, use put[_utf8]")
118
return self.get_utf8(relpath)
120
raise BzrError("Branch.controlfile(mode='w') is not supported, use put[_utf8]")
122
raise BzrError("invalid controlfile mode %r" % mode)
125
def get(self, relpath):
126
"""Get a file as a bytestream."""
127
relpath = self._escape(relpath)
128
return self._transport.get(relpath)
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))
138
def put(self, path, file):
141
:param path: The path to put the file, relative to the .bzr control
143
:param f: A file-like or string object whose contents should be copied.
145
self._transport.put(self._escape(path), file, mode=self._file_mode)
148
def put_utf8(self, path, a_string):
149
"""Write a string, encoding as utf-8.
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.
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
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')))
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
169
if self._lock_mode != 'w':
170
raise ReadOnlyError("can't upgrade to a write lock from %r" %
172
self._lock_count += 1
174
self._lock = self._transport.lock_write(
175
self._escape(self.lock_name))
176
self._lock_mode = 'w'
178
self._set_transaction(transactions.PassThroughTransaction())
181
mutter("lock read: %s (%s)", self, self._lock_count)
183
assert self._lock_mode in ('r', 'w'), \
184
"invalid lock mode %r" % self._lock_mode
185
self._lock_count += 1
187
self._lock = self._transport.lock_read(
188
self._escape(self.lock_name))
189
self._lock_mode = 'r'
191
self._set_transaction(transactions.ReadOnlyTransaction())
192
# 5K may be excessive, but hey, its a knob.
193
self.get_transaction().set_cache_size(5000)
196
mutter("unlock: %s (%s)", self, self._lock_count)
197
if not self._lock_mode:
198
raise LockError('branch %r is not locked' % (self))
200
if self._lock_count > 1:
201
self._lock_count -= 1
203
self._finish_transaction()
206
self._lock_mode = self._lock_count = None
208
def get_transaction(self):
209
"""Return the current active transaction.
211
If no transaction is active, this returns a passthrough object
212
for which all data is immediately flushed and no caching happens.
214
if self._transaction is None:
215
return transactions.PassThroughTransaction()
217
return self._transaction
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.' %
224
self._transaction = new_transaction
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' %
231
transaction = self._transaction
232
self._transaction = None