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."""
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):
89
self._file_mode = 0644
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:
96
if not self._set_file_mode:
97
self._file_mode = None
99
def controlfilename(self, file_or_path):
100
"""Return location relative to branch."""
101
return self._transport.abspath(self._escape(file_or_path))
103
@deprecated_method(zero_eight)
104
def controlfile(self, file_or_path, mode='r'):
105
"""Open a control file for this branch.
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.
112
Such files are not openable in write mode : they are managed via
113
put and put_utf8 which atomically replace old versions using
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?
121
return self.get(relpath)
123
raise BzrError("Branch.controlfile(mode='wb') is not supported, use put[_utf8]")
125
return self.get_utf8(relpath)
127
raise BzrError("Branch.controlfile(mode='w') is not supported, use put[_utf8]")
129
raise BzrError("invalid controlfile mode %r" % mode)
132
def get(self, relpath):
133
"""Get a file as a bytestream."""
134
relpath = self._escape(relpath)
135
return self._transport.get(relpath)
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))
145
def put(self, path, file):
148
:param path: The path to put the file, relative to the .bzr control
150
:param f: A file-like or string object whose contents should be copied.
152
self._transport.put(self._escape(path), file, mode=self._file_mode)
155
def put_utf8(self, path, a_string):
156
"""Write a string, encoding as utf-8.
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.
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
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')))
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
176
if self._lock_mode != 'w':
177
raise ReadOnlyError("can't upgrade to a write lock from %r" %
179
self._lock_count += 1
181
self._lock = self._transport.lock_write(
182
self._escape(self.lock_name))
183
self._lock_mode = 'w'
185
self._set_transaction(transactions.PassThroughTransaction())
188
mutter("lock read: %s (%s)", self, self._lock_count)
190
assert self._lock_mode in ('r', 'w'), \
191
"invalid lock mode %r" % self._lock_mode
192
self._lock_count += 1
194
self._lock = self._transport.lock_read(
195
self._escape(self.lock_name))
196
self._lock_mode = 'r'
198
self._set_transaction(transactions.ReadOnlyTransaction())
199
# 5K may be excessive, but hey, its a knob.
200
self.get_transaction().set_cache_size(5000)
203
mutter("unlock: %s (%s)", self, self._lock_count)
204
if not self._lock_mode:
205
raise LockError('branch %r is not locked' % (self))
207
if self._lock_count > 1:
208
self._lock_count -= 1
210
self._finish_transaction()
213
self._lock_mode = self._lock_count = None
215
def get_transaction(self):
216
"""Return the current active transaction.
218
If no transaction is active, this returns a passthrough object
219
for which all data is immediately flushed and no caching happens.
221
if self._transaction is None:
222
return transactions.PassThroughTransaction()
224
return self._transaction
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.' %
231
self._transaction = new_transaction
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' %
238
transaction = self._transaction
239
self._transaction = None