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
18
from osutils import file_iterator
21
import bzrlib.errors as errors
22
from bzrlib.errors import LockError, ReadOnlyError
23
from bzrlib.trace import mutter
24
import bzrlib.transactions as transactions
26
class LockableFiles(object):
27
"""Object representing a set of files locked within the same scope
33
If _lock_mode is true, a positive count of the number of times the
34
lock has been taken *by this process*. Others may have compatible
38
Lock object from bzrlib.lock.
44
# If set to False (by a plugin, etc) BzrBranch will not set the
45
# mode on created files or directories
49
def __init__(self, transport, lock_name):
51
self._transport = transport
52
self.lock_name = lock_name
53
self._transaction = None
57
if self._lock_mode or self._lock:
58
# XXX: This should show something every time, and be suitable for
59
# headless operation and embedding
60
from warnings import warn
61
warn("file group %r was not explicitly unlocked" % self)
64
def _escape(self, file_or_path):
65
if not isinstance(file_or_path, basestring):
66
file_or_path = '/'.join(file_or_path)
67
if file_or_path == '':
69
return bzrlib.transport.urlescape(unicode(file_or_path))
71
def _find_modes(self):
72
"""Determine the appropriate modes for files and directories."""
75
st = self._transport.stat(u'.')
76
except errors.NoSuchFile:
77
# The .bzr/ directory doesn't exist, try to
78
# inherit the permissions from the parent directory
79
# but only try 1 level up
80
temp_transport = self._transport.clone('..')
81
st = temp_transport.stat(u'.')
82
except (errors.TransportNotPossible, errors.NoSuchFile):
84
self._file_mode = 0644
86
self._dir_mode = st.st_mode & 07777
87
# Remove the sticky and execute bits for files
88
self._file_mode = self._dir_mode & ~07111
89
if not self._set_dir_mode:
91
if not self._set_file_mode:
92
self._file_mode = None
94
def controlfilename(self, file_or_path):
95
"""Return location relative to branch."""
96
return self._transport.abspath(self._escape(file_or_path))
98
def controlfile(self, file_or_path, mode='r'):
99
"""Open a control file for this branch.
101
There are two classes of file in the control directory: text
102
and binary. binary files are untranslated byte streams. Text
103
control files are stored with Unix newlines and in UTF-8, even
104
if the platform or locale defaults are different.
106
Controlfiles should almost never be opened in write mode but
107
rather should be atomically copied and replaced using atomicfile.
111
relpath = self._escape(file_or_path)
112
#TODO: codecs.open() buffers linewise, so it was overloaded with
113
# a much larger buffer, do we need to do the same for getreader/getwriter?
115
return self._transport.get(relpath)
117
raise BzrError("Branch.controlfile(mode='wb') is not supported, use put[_utf8]")
119
# XXX: Do we really want errors='replace'? Perhaps it should be
120
# an error, or at least reported, if there's incorrectly-encoded
121
# data inside a file.
122
# <https://launchpad.net/products/bzr/+bug/3823>
123
return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
125
raise BzrError("Branch.controlfile(mode='w') is not supported, use put[_utf8]")
127
raise BzrError("invalid controlfile mode %r" % mode)
129
def put(self, path, file):
132
:param path: The path to put the file, relative to the .bzr control
134
:param f: A file-like or string object whose contents should be copied.
136
if not self._lock_mode == 'w':
137
raise ReadOnlyError()
138
self._transport.put(self._escape(path), file, mode=self._file_mode)
140
def put_utf8(self, path, file, mode=None):
141
"""Write a file, encoding as utf-8.
143
:param path: The path to put the file, relative to the .bzr control
145
:param f: A file-like or string object whose contents should be copied.
148
from iterablefile import IterableFile
150
if hasattr(file, 'read'):
151
iterator = file_iterator(file)
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
encoded_file = IterableFile(b.encode('utf-8') for b in
162
self.put(path, encoded_file)
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 LockError("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