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)
67
# Special case : windows has no "root", but does have
68
# multiple lettered drives inside it. #240910
69
if sys.platform == 'win32' and base == 'file:///':
72
super(LocalTransport, self).__init__(base)
75
super(LocalTransport, self).__init__(base)
76
self._local_base = urlutils.local_path_from_url(base)
78
def clone(self, offset=None):
79
"""Return a new LocalTransport with root at self.base + offset
80
Because the local filesystem does not require a connection,
81
we can just return a new object.
84
return LocalTransport(self.base)
86
abspath = self.abspath(offset)
87
if abspath == 'file://':
88
# fix upwalk for UNC path
89
# when clone from //HOST/path updir recursively
90
# we should stop at least at //HOST part
92
return LocalTransport(abspath)
94
def _abspath(self, relative_reference):
95
"""Return a path for use in os calls.
97
Several assumptions are made:
98
- relative_reference does not contain '..'
99
- relative_reference is url escaped.
101
if relative_reference in ('.', ''):
102
return self._local_base
103
return self._local_base + urlutils.unescape(relative_reference)
105
def abspath(self, relpath):
106
"""Return the full url to the given relative URL."""
107
# TODO: url escape the result. RBC 20060523.
108
# jam 20060426 Using normpath on the real path, because that ensures
109
# proper handling of stuff like
110
path = osutils.normpath(osutils.pathjoin(
111
self._local_base, urlutils.unescape(relpath)))
112
return urlutils.local_path_to_url(path)
114
def local_abspath(self, relpath):
115
"""Transform the given relative path URL into the actual path on disk
117
This function only exists for the LocalTransport, since it is
118
the only one that has direct local access.
119
This is mostly for stuff like WorkingTree which needs to know
120
the local working directory.
122
This function is quite expensive: it calls realpath which resolves
125
absurl = self.abspath(relpath)
126
# mutter(u'relpath %s => base: %s, absurl %s', relpath, self.base, absurl)
127
return urlutils.local_path_from_url(absurl)
129
def relpath(self, abspath):
130
"""Return the local path portion from a given absolute path.
135
return urlutils.file_relpath(
136
urlutils.strip_trailing_slash(self.base),
137
urlutils.strip_trailing_slash(abspath))
139
def has(self, relpath):
140
return os.access(self._abspath(relpath), os.F_OK)
142
def get(self, relpath):
143
"""Get the file at the given relative path.
145
:param relpath: The relative path to the file
147
canonical_url = self.abspath(relpath)
148
if canonical_url in transport._file_streams:
149
transport._file_streams[canonical_url].flush()
151
path = self._abspath(relpath)
152
return open(path, 'rb')
153
except (IOError, OSError),e:
154
if e.errno == errno.EISDIR:
155
return LateReadError(relpath)
156
self._translate_error(e, path)
158
def put_file(self, relpath, f, mode=None):
159
"""Copy the file-like object into the location.
161
:param relpath: Location to put the contents, relative to base.
162
:param f: File-like object.
163
:param mode: The mode for the newly created file,
164
None means just use the default
169
path = self._abspath(relpath)
170
osutils.check_legal_path(path)
171
fp = atomicfile.AtomicFile(path, 'wb', new_mode=mode)
172
except (IOError, OSError),e:
173
self._translate_error(e, path)
175
length = self._pump(f, fp)
181
def put_bytes(self, relpath, bytes, mode=None):
182
"""Copy the string into the location.
184
:param relpath: Location to put the contents, relative to base.
190
path = self._abspath(relpath)
191
osutils.check_legal_path(path)
192
fp = atomicfile.AtomicFile(path, 'wb', new_mode=mode)
193
except (IOError, OSError),e:
194
self._translate_error(e, path)
201
def _put_non_atomic_helper(self, relpath, writer,
203
create_parent_dir=False,
205
"""Common functionality information for the put_*_non_atomic.
207
This tracks all the create_parent_dir stuff.
209
:param relpath: the path we are putting to.
210
:param writer: A function that takes an os level file descriptor
211
and writes whatever data it needs to write there.
212
:param mode: The final file mode.
213
:param create_parent_dir: Should we be creating the parent directory
216
abspath = self._abspath(relpath)
218
# os.open() will automatically use the umask
223
fd = os.open(abspath, _put_non_atomic_flags, local_mode)
224
except (IOError, OSError),e:
225
# We couldn't create the file, maybe we need to create
226
# the parent directory, and try again
227
if (not create_parent_dir
228
or e.errno not in (errno.ENOENT,errno.ENOTDIR)):
229
self._translate_error(e, relpath)
230
parent_dir = os.path.dirname(abspath)
232
self._translate_error(e, relpath)
233
self._mkdir(parent_dir, mode=dir_mode)
234
# We created the parent directory, lets try to open the
237
fd = os.open(abspath, _put_non_atomic_flags, local_mode)
238
except (IOError, OSError), e:
239
self._translate_error(e, relpath)
242
if mode is not None and mode != S_IMODE(st.st_mode):
243
# Because of umask, we may still need to chmod the file.
244
# But in the general case, we won't have to
245
os.chmod(abspath, mode)
250
def put_file_non_atomic(self, relpath, f, mode=None,
251
create_parent_dir=False,
253
"""Copy the file-like object into the target location.
255
This function is not strictly safe to use. It is only meant to
256
be used when you already know that the target does not exist.
257
It is not safe, because it will open and truncate the remote
258
file. So there may be a time when the file has invalid contents.
260
:param relpath: The remote location to put the contents.
261
:param f: File-like object.
262
:param mode: Possible access permissions for new file.
263
None means do not set remote permissions.
264
:param create_parent_dir: If we cannot create the target file because
265
the parent directory does not exist, go ahead and
266
create it, and then try again.
269
self._pump_to_fd(f, fd)
270
self._put_non_atomic_helper(relpath, writer, mode=mode,
271
create_parent_dir=create_parent_dir,
274
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
275
create_parent_dir=False, dir_mode=None):
278
self._put_non_atomic_helper(relpath, writer, mode=mode,
279
create_parent_dir=create_parent_dir,
282
def iter_files_recursive(self):
283
"""Iter the relative paths of files in the transports sub-tree."""
284
queue = list(self.list_dir(u'.'))
286
relpath = queue.pop(0)
287
st = self.stat(relpath)
288
if S_ISDIR(st[ST_MODE]):
289
for i, basename in enumerate(self.list_dir(relpath)):
290
queue.insert(i, relpath+'/'+basename)
294
def _mkdir(self, abspath, mode=None):
295
"""Create a real directory, filtering through mode"""
297
# os.mkdir() will filter through umask
302
os.mkdir(abspath, local_mode)
304
# It is probably faster to just do the chmod, rather than
305
# doing a stat, and then trying to compare
306
os.chmod(abspath, mode)
307
except (IOError, OSError),e:
308
self._translate_error(e, abspath)
310
def mkdir(self, relpath, mode=None):
311
"""Create a directory at the given path."""
312
self._mkdir(self._abspath(relpath), mode=mode)
314
def open_write_stream(self, relpath, mode=None):
315
"""See Transport.open_write_stream."""
316
# initialise the file
317
self.put_bytes_non_atomic(relpath, "", mode=mode)
318
abspath = self._abspath(relpath)
319
handle = open(abspath, 'wb')
321
self._check_mode_and_size(abspath, handle.fileno(), mode)
322
transport._file_streams[self.abspath(relpath)] = handle
323
return transport.FileFileStream(self, relpath, handle)
325
def _get_append_file(self, relpath, mode=None):
326
"""Call os.open() for the given relpath"""
327
file_abspath = self._abspath(relpath)
329
# os.open() will automatically use the umask
334
return file_abspath, os.open(file_abspath, _append_flags, local_mode)
335
except (IOError, OSError),e:
336
self._translate_error(e, relpath)
338
def _check_mode_and_size(self, file_abspath, fd, mode=None):
339
"""Check the mode of the file, and return the current size"""
341
if mode is not None and mode != S_IMODE(st.st_mode):
342
# Because of umask, we may still need to chmod the file.
343
# But in the general case, we won't have to
344
os.chmod(file_abspath, mode)
347
def append_file(self, relpath, f, mode=None):
348
"""Append the text in the file-like object into the final location."""
349
file_abspath, fd = self._get_append_file(relpath, mode=mode)
351
result = self._check_mode_and_size(file_abspath, fd, mode=mode)
352
self._pump_to_fd(f, fd)
357
def append_bytes(self, relpath, bytes, mode=None):
358
"""Append the text in the string into the final location."""
359
file_abspath, fd = self._get_append_file(relpath, mode=mode)
361
result = self._check_mode_and_size(file_abspath, fd, mode=mode)
367
def _pump_to_fd(self, fromfile, to_fd):
368
"""Copy contents of one file to another."""
371
b = fromfile.read(BUFSIZE)
376
def copy(self, rel_from, rel_to):
377
"""Copy the item at rel_from to the location at rel_to"""
378
path_from = self._abspath(rel_from)
379
path_to = self._abspath(rel_to)
381
shutil.copy(path_from, path_to)
382
except (IOError, OSError),e:
383
# TODO: What about path_to?
384
self._translate_error(e, path_from)
386
def rename(self, rel_from, rel_to):
387
path_from = self._abspath(rel_from)
389
# *don't* call bzrlib.osutils.rename, because we want to
390
# detect errors on rename
391
os.rename(path_from, self._abspath(rel_to))
392
except (IOError, OSError),e:
393
# TODO: What about path_to?
394
self._translate_error(e, path_from)
396
def move(self, rel_from, rel_to):
397
"""Move the item at rel_from to the location at rel_to"""
398
path_from = self._abspath(rel_from)
399
path_to = self._abspath(rel_to)
402
# this version will delete the destination if necessary
403
osutils.rename(path_from, path_to)
404
except (IOError, OSError),e:
405
# TODO: What about path_to?
406
self._translate_error(e, path_from)
408
def delete(self, relpath):
409
"""Delete the item at relpath"""
412
path = self._abspath(relpath)
414
except (IOError, OSError),e:
415
self._translate_error(e, path)
417
def external_url(self):
418
"""See bzrlib.transport.Transport.external_url."""
419
# File URL's are externally usable.
422
def copy_to(self, relpaths, other, mode=None, pb=None):
423
"""Copy a set of entries from self into another Transport.
425
:param relpaths: A list/generator of entries to be copied.
427
if isinstance(other, LocalTransport):
428
# Both from & to are on the local filesystem
429
# Unfortunately, I can't think of anything faster than just
430
# copying them across, one by one :(
431
total = self._get_total(relpaths)
433
for path in relpaths:
434
self._update_pb(pb, 'copy-to', count, total)
436
mypath = self._abspath(path)
437
otherpath = other._abspath(path)
438
shutil.copy(mypath, otherpath)
440
os.chmod(otherpath, mode)
441
except (IOError, OSError),e:
442
self._translate_error(e, path)
446
return super(LocalTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
449
"""See Transport.listable."""
452
def list_dir(self, relpath):
453
"""Return a list of all files at the given location.
454
WARNING: many transports do not support this, so trying avoid using
455
it if at all possible.
457
path = self._abspath(relpath)
459
entries = os.listdir(path)
460
except (IOError, OSError), e:
461
self._translate_error(e, path)
462
return [urlutils.escape(entry) for entry in entries]
464
def stat(self, relpath):
465
"""Return the stat information for a file.
469
path = self._abspath(relpath)
471
except (IOError, OSError),e:
472
self._translate_error(e, path)
474
def lock_read(self, relpath):
475
"""Lock the given file for shared (read) access.
476
:return: A lock object, which should be passed to Transport.unlock()
478
from bzrlib.lock import ReadLock
481
path = self._abspath(relpath)
482
return ReadLock(path)
483
except (IOError, OSError), e:
484
self._translate_error(e, path)
486
def lock_write(self, relpath):
487
"""Lock the given file for exclusive (write) access.
488
WARNING: many transports do not support this, so trying avoid using it
490
:return: A lock object, which should be passed to Transport.unlock()
492
from bzrlib.lock import WriteLock
493
return WriteLock(self._abspath(relpath))
495
def rmdir(self, relpath):
496
"""See Transport.rmdir."""
499
path = self._abspath(relpath)
501
except (IOError, OSError),e:
502
self._translate_error(e, path)
504
def _can_roundtrip_unix_modebits(self):
505
if sys.platform == 'win32':
512
class EmulatedWin32LocalTransport(LocalTransport):
513
"""Special transport for testing Win32 [UNC] paths on non-windows"""
515
def __init__(self, base):
518
super(LocalTransport, self).__init__(base)
519
self._local_base = urlutils._win32_local_path_from_url(base)
521
def abspath(self, relpath):
522
path = osutils.normpath(osutils.pathjoin(
523
self._local_base, urlutils.unescape(relpath)))
524
return urlutils._win32_local_path_to_url(path)
526
def clone(self, offset=None):
527
"""Return a new LocalTransport with root at self.base + offset
528
Because the local filesystem does not require a connection,
529
we can just return a new object.
532
return EmulatedWin32LocalTransport(self.base)
534
abspath = self.abspath(offset)
535
if abspath == 'file://':
536
# fix upwalk for UNC path
537
# when clone from //HOST/path updir recursively
538
# we should stop at least at //HOST part
540
return EmulatedWin32LocalTransport(abspath)
543
class LocalURLServer(Server):
544
"""A pretend server for local transports, using file:// urls.
546
Of course no actual server is required to access the local filesystem, so
547
this just exists to tell the test code how to get to it.
551
"""Setup the server to service requests.
553
:param decorated_transport: ignored by this implementation.
557
"""See Transport.Server.get_url."""
558
return urlutils.local_path_to_url('')
561
def get_test_permutations():
562
"""Return the permutations to be used in testing."""
564
(LocalTransport, LocalURLServer),