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
19
import bzrlib.errors as errors
20
from bzrlib.errors import LockError, ReadOnlyError
21
from bzrlib.trace import mutter
22
import bzrlib.transactions as transactions
23
from osutils import file_iterator
25
class LockableFiles(object):
26
"""Object representing a set of lockable files
32
If _lock_mode is true, a positive count of the number of times the
36
Lock object from bzrlib.lock.
42
# If set to False (by a plugin, etc) BzrBranch will not set the
43
# mode on created files or directories
47
def __init__(self, transport, lock_name):
49
self._transport = transport
50
self.lock_name = lock_name
51
self._transaction = None
55
if self._lock_mode or self._lock:
56
# XXX: This should show something every time, and be suitable for
57
# headless operation and embedding
58
from warnings import warn
59
warn("file group %r was not explicitly unlocked" % self)
62
def _escape(self, file_or_path):
63
if not isinstance(file_or_path, basestring):
64
file_or_path = '/'.join(file_or_path)
65
if file_or_path == '':
67
return bzrlib.transport.urlescape(unicode(file_or_path))
69
def _find_modes(self):
70
"""Determine the appropriate modes for files and directories."""
73
st = self._transport.stat(u'.')
74
except errors.NoSuchFile:
75
# The .bzr/ directory doesn't exist, try to
76
# inherit the permissions from the parent directory
77
# but only try 1 level up
78
temp_transport = self._transport.clone('..')
79
st = temp_transport.stat(u'.')
80
except (errors.TransportNotPossible, errors.NoSuchFile):
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
def controlfile(self, file_or_path, mode='r'):
97
"""Open a control file for this branch.
99
There are two classes of file in the control directory: text
100
and binary. binary files are untranslated byte streams. Text
101
control files are stored with Unix newlines and in UTF-8, even
102
if the platform or locale defaults are different.
104
Controlfiles should almost never be opened in write mode but
105
rather should be atomically copied and replaced using atomicfile.
109
relpath = self._escape(file_or_path)
110
#TODO: codecs.open() buffers linewise, so it was overloaded with
111
# a much larger buffer, do we need to do the same for getreader/getwriter?
113
return self._transport.get(relpath)
115
raise BzrError("Branch.controlfile(mode='wb') is not supported, use put[_utf8]")
117
# XXX: Do we really want errors='replace'? Perhaps it should be
118
# an error, or at least reported, if there's incorrectly-encoded
119
# data inside a file.
120
# <https://launchpad.net/products/bzr/+bug/3823>
121
return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
123
raise BzrError("Branch.controlfile(mode='w') is not supported, use put[_utf8]")
125
raise BzrError("invalid controlfile mode %r" % mode)
127
def put(self, path, file):
130
:param path: The path to put the file, relative to the .bzr control
132
:param f: A file-like or string object whose contents should be copied.
134
if not self._lock_mode == 'w':
135
raise ReadOnlyError()
136
self._transport.put(self._escape(path), file, mode=self._file_mode)
138
def put_utf8(self, path, file, mode=None):
139
"""Write a file, encoding as utf-8.
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.
146
from iterablefile import IterableFile
148
if hasattr(file, 'read'):
149
iterator = file_iterator(file)
152
# IterableFile would not be needed if Transport.put took iterables
153
# instead of files. ADHB 2005-12-25
154
# RBC 20060103 surely its not needed anyway, with codecs transcode
156
# JAM 20060103 We definitely don't want encode(..., 'replace')
157
# these are valuable files which should have exact contents.
158
encoded_file = IterableFile(b.encode('utf-8') for b in
160
self.put(path, encoded_file)
162
def lock_write(self):
163
mutter("lock write: %s (%s)", self, self._lock_count)
164
# TODO: Upgrade locking to support using a Transport,
165
# and potentially a remote locking protocol
167
if self._lock_mode != 'w':
168
raise LockError("can't upgrade to a write lock from %r" %
170
self._lock_count += 1
172
self._lock = self._transport.lock_write(
173
self._escape(self.lock_name))
174
self._lock_mode = 'w'
176
self._set_transaction(transactions.PassThroughTransaction())
179
mutter("lock read: %s (%s)", self, self._lock_count)
181
assert self._lock_mode in ('r', 'w'), \
182
"invalid lock mode %r" % self._lock_mode
183
self._lock_count += 1
185
self._lock = self._transport.lock_read(
186
self._escape(self.lock_name))
187
self._lock_mode = 'r'
189
self._set_transaction(transactions.ReadOnlyTransaction())
190
# 5K may be excessive, but hey, its a knob.
191
self.get_transaction().set_cache_size(5000)
194
mutter("unlock: %s (%s)", self, self._lock_count)
195
if not self._lock_mode:
196
raise LockError('branch %r is not locked' % (self))
198
if self._lock_count > 1:
199
self._lock_count -= 1
201
self._finish_transaction()
204
self._lock_mode = self._lock_count = None
206
def get_transaction(self):
207
"""Return the current active transaction.
209
If no transaction is active, this returns a passthrough object
210
for which all data is immediately flushed and no caching happens.
212
if self._transaction is None:
213
return transactions.PassThroughTransaction()
215
return self._transaction
217
def _set_transaction(self, new_transaction):
218
"""Set a new active transaction."""
219
if self._transaction is not None:
220
raise errors.LockError('Branch %s is in a transaction already.' %
222
self._transaction = new_transaction
224
def _finish_transaction(self):
225
"""Exit the current transaction."""
226
if self._transaction is None:
227
raise errors.LockError('Branch %s is not in a transaction' %
229
transaction = self._transaction
230
self._transaction = None