1
# Copyright (C) 2005, 2006 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
"""Transport for the local filesystem.
19
This is a fairly thin wrapper on regular file IO.
23
from stat import ST_MODE, S_ISDIR, ST_SIZE, S_IMODE
26
from bzrlib.lazy_import import lazy_import
27
lazy_import(globals(), """
38
from bzrlib.trace import mutter
39
from bzrlib.transport import LateReadError
42
from bzrlib.transport import Transport, Server
45
_append_flags = os.O_CREAT | os.O_APPEND | os.O_WRONLY | osutils.O_BINARY
46
_put_non_atomic_flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | osutils.O_BINARY
49
class LocalTransport(Transport):
50
"""This is the transport agent for local filesystem access."""
52
def __init__(self, base):
53
"""Set the base path where files will be stored."""
54
if not base.startswith('file://'):
55
symbol_versioning.warn(
56
"Instantiating LocalTransport with a filesystem path"
57
" is deprecated as of bzr 0.8."
58
" Please use bzrlib.transport.get_transport()"
59
" or pass in a file:// url.",
63
base = urlutils.local_path_to_url(base)
66
super(LocalTransport, self).__init__(base)
67
self._local_base = urlutils.local_path_from_url(base)
69
def clone(self, offset=None):
70
"""Return a new LocalTransport with root at self.base + offset
71
Because the local filesystem does not require a connection,
72
we can just return a new object.
75
return LocalTransport(self.base)
77
abspath = self.abspath(offset)
78
if abspath == 'file://':
79
# fix upwalk for UNC path
80
# when clone from //HOST/path updir recursively
81
# we should stop at least at //HOST part
83
return LocalTransport(abspath)
85
def _abspath(self, relative_reference):
86
"""Return a path for use in os calls.
88
Several assumptions are made:
89
- relative_reference does not contain '..'
90
- relative_reference is url escaped.
92
if relative_reference in ('.', ''):
93
return self._local_base
94
return self._local_base + urlutils.unescape(relative_reference)
96
def abspath(self, relpath):
97
"""Return the full url to the given relative URL."""
98
# TODO: url escape the result. RBC 20060523.
99
assert isinstance(relpath, basestring), (type(relpath), relpath)
100
# jam 20060426 Using normpath on the real path, because that ensures
101
# proper handling of stuff like
102
path = osutils.normpath(osutils.pathjoin(
103
self._local_base, urlutils.unescape(relpath)))
104
return urlutils.local_path_to_url(path)
106
def local_abspath(self, relpath):
107
"""Transform the given relative path URL into the actual path on disk
109
This function only exists for the LocalTransport, since it is
110
the only one that has direct local access.
111
This is mostly for stuff like WorkingTree which needs to know
112
the local working directory.
114
This function is quite expensive: it calls realpath which resolves
117
absurl = self.abspath(relpath)
118
# mutter(u'relpath %s => base: %s, absurl %s', relpath, self.base, absurl)
119
return urlutils.local_path_from_url(absurl)
121
def relpath(self, abspath):
122
"""Return the local path portion from a given absolute path.
127
return urlutils.file_relpath(
128
urlutils.strip_trailing_slash(self.base),
129
urlutils.strip_trailing_slash(abspath))
131
def has(self, relpath):
132
return os.access(self._abspath(relpath), os.F_OK)
134
def get(self, relpath):
135
"""Get the file at the given relative path.
137
:param relpath: The relative path to the file
139
canonical_url = self.abspath(relpath)
140
if canonical_url in transport._file_streams:
141
transport._file_streams[canonical_url].flush()
143
path = self._abspath(relpath)
144
return open(path, 'rb')
145
except (IOError, OSError),e:
146
if e.errno == errno.EISDIR:
147
return LateReadError(relpath)
148
self._translate_error(e, path)
150
def put_file(self, relpath, f, mode=None):
151
"""Copy the file-like object into the location.
153
:param relpath: Location to put the contents, relative to base.
154
:param f: File-like object.
155
:param mode: The mode for the newly created file,
156
None means just use the default
161
path = self._abspath(relpath)
162
osutils.check_legal_path(path)
163
fp = atomicfile.AtomicFile(path, 'wb', new_mode=mode)
164
except (IOError, OSError),e:
165
self._translate_error(e, path)
167
length = self._pump(f, fp)
173
def put_bytes(self, relpath, bytes, mode=None):
174
"""Copy the string into the location.
176
:param relpath: Location to put the contents, relative to base.
182
path = self._abspath(relpath)
183
osutils.check_legal_path(path)
184
fp = atomicfile.AtomicFile(path, 'wb', new_mode=mode)
185
except (IOError, OSError),e:
186
self._translate_error(e, path)
193
def _put_non_atomic_helper(self, relpath, writer,
195
create_parent_dir=False,
197
"""Common functionality information for the put_*_non_atomic.
199
This tracks all the create_parent_dir stuff.
201
:param relpath: the path we are putting to.
202
:param writer: A function that takes an os level file descriptor
203
and writes whatever data it needs to write there.
204
:param mode: The final file mode.
205
:param create_parent_dir: Should we be creating the parent directory
208
abspath = self._abspath(relpath)
210
# os.open() will automatically use the umask
215
fd = os.open(abspath, _put_non_atomic_flags, local_mode)
216
except (IOError, OSError),e:
217
# We couldn't create the file, maybe we need to create
218
# the parent directory, and try again
219
if (not create_parent_dir
220
or e.errno not in (errno.ENOENT,errno.ENOTDIR)):
221
self._translate_error(e, relpath)
222
parent_dir = os.path.dirname(abspath)
224
self._translate_error(e, relpath)
225
self._mkdir(parent_dir, mode=dir_mode)
226
# We created the parent directory, lets try to open the
229
fd = os.open(abspath, _put_non_atomic_flags, local_mode)
230
except (IOError, OSError), e:
231
self._translate_error(e, relpath)
234
if mode is not None and mode != S_IMODE(st.st_mode):
235
# Because of umask, we may still need to chmod the file.
236
# But in the general case, we won't have to
237
os.chmod(abspath, mode)
242
def put_file_non_atomic(self, relpath, f, mode=None,
243
create_parent_dir=False,
245
"""Copy the file-like object into the target location.
247
This function is not strictly safe to use. It is only meant to
248
be used when you already know that the target does not exist.
249
It is not safe, because it will open and truncate the remote
250
file. So there may be a time when the file has invalid contents.
252
:param relpath: The remote location to put the contents.
253
:param f: File-like object.
254
:param mode: Possible access permissions for new file.
255
None means do not set remote permissions.
256
:param create_parent_dir: If we cannot create the target file because
257
the parent directory does not exist, go ahead and
258
create it, and then try again.
261
self._pump_to_fd(f, fd)
262
self._put_non_atomic_helper(relpath, writer, mode=mode,
263
create_parent_dir=create_parent_dir,
266
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
267
create_parent_dir=False, dir_mode=None):
270
self._put_non_atomic_helper(relpath, writer, mode=mode,
271
create_parent_dir=create_parent_dir,
274
def iter_files_recursive(self):
275
"""Iter the relative paths of files in the transports sub-tree."""
276
queue = list(self.list_dir(u'.'))
278
relpath = queue.pop(0)
279
st = self.stat(relpath)
280
if S_ISDIR(st[ST_MODE]):
281
for i, basename in enumerate(self.list_dir(relpath)):
282
queue.insert(i, relpath+'/'+basename)
286
def _mkdir(self, abspath, mode=None):
287
"""Create a real directory, filtering through mode"""
289
# os.mkdir() will filter through umask
294
os.mkdir(abspath, local_mode)
296
# It is probably faster to just do the chmod, rather than
297
# doing a stat, and then trying to compare
298
os.chmod(abspath, mode)
299
except (IOError, OSError),e:
300
self._translate_error(e, abspath)
302
def mkdir(self, relpath, mode=None):
303
"""Create a directory at the given path."""
304
self._mkdir(self._abspath(relpath), mode=mode)
306
def open_write_stream(self, relpath, mode=None):
307
"""See Transport.open_write_stream."""
308
# initialise the file
309
self.put_bytes_non_atomic(relpath, "", mode=mode)
310
abspath = self._abspath(relpath)
311
handle = open(abspath, 'wb')
313
self._check_mode_and_size(abspath, handle.fileno(), mode)
314
transport._file_streams[self.abspath(relpath)] = handle
315
return transport.FileFileStream(self, relpath, handle)
317
def _get_append_file(self, relpath, mode=None):
318
"""Call os.open() for the given relpath"""
319
file_abspath = self._abspath(relpath)
321
# os.open() will automatically use the umask
326
return file_abspath, os.open(file_abspath, _append_flags, local_mode)
327
except (IOError, OSError),e:
328
self._translate_error(e, relpath)
330
def _check_mode_and_size(self, file_abspath, fd, mode=None):
331
"""Check the mode of the file, and return the current size"""
333
if mode is not None and mode != S_IMODE(st.st_mode):
334
# Because of umask, we may still need to chmod the file.
335
# But in the general case, we won't have to
336
os.chmod(file_abspath, mode)
339
def append_file(self, relpath, f, mode=None):
340
"""Append the text in the file-like object into the final location."""
341
file_abspath, fd = self._get_append_file(relpath, mode=mode)
343
result = self._check_mode_and_size(file_abspath, fd, mode=mode)
344
self._pump_to_fd(f, fd)
349
def append_bytes(self, relpath, bytes, mode=None):
350
"""Append the text in the string into the final location."""
351
file_abspath, fd = self._get_append_file(relpath, mode=mode)
353
result = self._check_mode_and_size(file_abspath, fd, mode=mode)
359
def _pump_to_fd(self, fromfile, to_fd):
360
"""Copy contents of one file to another."""
363
b = fromfile.read(BUFSIZE)
368
def copy(self, rel_from, rel_to):
369
"""Copy the item at rel_from to the location at rel_to"""
370
path_from = self._abspath(rel_from)
371
path_to = self._abspath(rel_to)
373
shutil.copy(path_from, path_to)
374
except (IOError, OSError),e:
375
# TODO: What about path_to?
376
self._translate_error(e, path_from)
378
def rename(self, rel_from, rel_to):
379
path_from = self._abspath(rel_from)
381
# *don't* call bzrlib.osutils.rename, because we want to
382
# detect errors on rename
383
os.rename(path_from, self._abspath(rel_to))
384
except (IOError, OSError),e:
385
# TODO: What about path_to?
386
self._translate_error(e, path_from)
388
def move(self, rel_from, rel_to):
389
"""Move the item at rel_from to the location at rel_to"""
390
path_from = self._abspath(rel_from)
391
path_to = self._abspath(rel_to)
394
# this version will delete the destination if necessary
395
osutils.rename(path_from, path_to)
396
except (IOError, OSError),e:
397
# TODO: What about path_to?
398
self._translate_error(e, path_from)
400
def delete(self, relpath):
401
"""Delete the item at relpath"""
404
path = self._abspath(relpath)
406
except (IOError, OSError),e:
407
self._translate_error(e, path)
409
def external_url(self):
410
"""See bzrlib.transport.Transport.external_url."""
411
# File URL's are externally usable.
414
def copy_to(self, relpaths, other, mode=None, pb=None):
415
"""Copy a set of entries from self into another Transport.
417
:param relpaths: A list/generator of entries to be copied.
419
if isinstance(other, LocalTransport):
420
# Both from & to are on the local filesystem
421
# Unfortunately, I can't think of anything faster than just
422
# copying them across, one by one :(
423
total = self._get_total(relpaths)
425
for path in relpaths:
426
self._update_pb(pb, 'copy-to', count, total)
428
mypath = self._abspath(path)
429
otherpath = other._abspath(path)
430
shutil.copy(mypath, otherpath)
432
os.chmod(otherpath, mode)
433
except (IOError, OSError),e:
434
self._translate_error(e, path)
438
return super(LocalTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
441
"""See Transport.listable."""
444
def list_dir(self, relpath):
445
"""Return a list of all files at the given location.
446
WARNING: many transports do not support this, so trying avoid using
447
it if at all possible.
449
path = self._abspath(relpath)
451
entries = os.listdir(path)
452
except (IOError, OSError), e:
453
self._translate_error(e, path)
454
return [urlutils.escape(entry) for entry in entries]
456
def stat(self, relpath):
457
"""Return the stat information for a file.
461
path = self._abspath(relpath)
463
except (IOError, OSError),e:
464
self._translate_error(e, path)
466
def lock_read(self, relpath):
467
"""Lock the given file for shared (read) access.
468
:return: A lock object, which should be passed to Transport.unlock()
470
from bzrlib.lock import ReadLock
473
path = self._abspath(relpath)
474
return ReadLock(path)
475
except (IOError, OSError), e:
476
self._translate_error(e, path)
478
def lock_write(self, relpath):
479
"""Lock the given file for exclusive (write) access.
480
WARNING: many transports do not support this, so trying avoid using it
482
:return: A lock object, which should be passed to Transport.unlock()
484
from bzrlib.lock import WriteLock
485
return WriteLock(self._abspath(relpath))
487
def rmdir(self, relpath):
488
"""See Transport.rmdir."""
491
path = self._abspath(relpath)
493
except (IOError, OSError),e:
494
self._translate_error(e, path)
496
def _can_roundtrip_unix_modebits(self):
497
if sys.platform == 'win32':
504
class EmulatedWin32LocalTransport(LocalTransport):
505
"""Special transport for testing Win32 [UNC] paths on non-windows"""
507
def __init__(self, base):
510
super(LocalTransport, self).__init__(base)
511
self._local_base = urlutils._win32_local_path_from_url(base)
513
def abspath(self, relpath):
514
assert isinstance(relpath, basestring), (type(relpath), relpath)
515
path = osutils.normpath(osutils.pathjoin(
516
self._local_base, urlutils.unescape(relpath)))
517
return urlutils._win32_local_path_to_url(path)
519
def clone(self, offset=None):
520
"""Return a new LocalTransport with root at self.base + offset
521
Because the local filesystem does not require a connection,
522
we can just return a new object.
525
return EmulatedWin32LocalTransport(self.base)
527
abspath = self.abspath(offset)
528
if abspath == 'file://':
529
# fix upwalk for UNC path
530
# when clone from //HOST/path updir recursively
531
# we should stop at least at //HOST part
533
return EmulatedWin32LocalTransport(abspath)
536
class LocalURLServer(Server):
537
"""A pretend server for local transports, using file:// urls.
539
Of course no actual server is required to access the local filesystem, so
540
this just exists to tell the test code how to get to it.
544
"""Setup the server to service requests.
546
:param decorated_transport: ignored by this implementation.
550
"""See Transport.Server.get_url."""
551
return urlutils.local_path_to_url('')
554
def get_test_permutations():
555
"""Return the permutations to be used in testing."""
557
(LocalTransport, LocalURLServer),