1
# Copyright (C) 2005, 2006 Canonical Ltd
1
# Copyright (C) 2005 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
17
"""Transport for the local filesystem.
19
This is a fairly thin wrapper on regular file IO.
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(), """
24
from stat import ST_MODE, S_ISDIR, ST_SIZE
38
28
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
29
from bzrlib.transport import Transport, register_transport, \
30
TransportError, NoSuchFile, FileExists
31
from bzrlib.osutils import abspath
33
class LocalTransportError(TransportError):
49
37
class LocalTransport(Transport):
75
58
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)
60
return LocalTransport(self.abspath(offset))
96
62
def abspath(self, relpath):
97
"""Return the full url to the given relative URL."""
98
# TODO: url escape the result. RBC 20060523.
63
"""Return the full url to the given relative URL.
64
This can be supplied with a string or a list
99
66
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)
67
return os.path.join(self.base, urllib.unquote(relpath))
121
69
def relpath(self, abspath):
122
70
"""Return the local path portion from a given absolute path.
72
from bzrlib.osutils import relpath
124
73
if abspath is None:
127
return urlutils.file_relpath(
128
urlutils.strip_trailing_slash(self.base),
129
urlutils.strip_trailing_slash(abspath))
75
return relpath(self.base, abspath)
131
77
def has(self, relpath):
132
return os.access(self._abspath(relpath), os.F_OK)
78
return os.access(self.abspath(relpath), os.F_OK)
134
80
def get(self, relpath):
135
81
"""Get the file at the given relative path.
137
83
: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)
86
path = self.abspath(relpath)
144
87
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,
89
if e.errno in (errno.ENOENT, errno.ENOTDIR):
90
raise NoSuchFile('File or directory %r does not exist' % path, orig_error=e)
91
raise LocalTransportError(orig_error=e)
93
def put(self, relpath, f):
94
"""Copy the file-like or string object into the location.
96
:param relpath: Location to put the contents, relative to base.
97
:param f: File-like or string object.
99
from bzrlib.atomicfile import AtomicFile
102
path = self.abspath(relpath)
103
fp = AtomicFile(path, 'wb')
105
if e.errno == errno.ENOENT:
106
raise NoSuchFile('File %r does not exist' % path, orig_error=e)
107
raise LocalTransportError(orig_error=e)
274
114
def iter_files_recursive(self):
275
115
"""Iter the relative paths of files in the transports sub-tree."""
276
queue = list(self.list_dir(u'.'))
116
queue = list(self.list_dir('.'))
278
relpath = queue.pop(0)
118
relpath = urllib.quote(queue.pop(0))
279
119
st = self.stat(relpath)
280
120
if S_ISDIR(st[ST_MODE]):
281
121
for i, basename in enumerate(self.list_dir(relpath)):
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):
126
def mkdir(self, relpath):
303
127
"""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)
129
os.mkdir(self.abspath(relpath))
131
if e.errno == errno.EEXIST:
132
raise FileExists(orig_error=e)
133
elif e.errno == errno.ENOENT:
134
raise NoSuchFile(orig_error=e)
135
raise LocalTransportError(orig_error=e)
137
def append(self, relpath, f):
138
"""Append the text in the file-like object into the final
141
fp = open(self.abspath(relpath), 'ab')
368
144
def copy(self, rel_from, rel_to):
369
145
"""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)
147
path_from = self.abspath(rel_from)
148
path_to = self.abspath(rel_to)
373
150
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)
152
raise LocalTransportError(orig_error=e)
388
154
def move(self, rel_from, rel_to):
389
155
"""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)
156
path_from = self.abspath(rel_from)
157
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)
160
os.rename(path_from, path_to)
162
raise LocalTransportError(orig_error=e)
400
164
def delete(self, relpath):
401
165
"""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):
167
os.remove(self.abspath(relpath))
169
raise LocalTransportError(orig_error=e)
171
def copy_to(self, relpaths, other, pb=None):
415
172
"""Copy a set of entries from self into another Transport.
417
174
:param relpaths: A list/generator of entries to be copied.
482
225
:return: A lock object, which should be passed to Transport.unlock()
484
227
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.
228
return WriteLock(self.abspath(relpath))
231
class ScratchTransport(LocalTransport):
232
"""A transport that works in a temporary dir and cleans up after itself.
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.
234
The dir only exists for the lifetime of the Python object.
235
Obviously you should not put anything precious in 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),
238
def __init__(self, base=None):
240
base = tempfile.mkdtemp()
241
super(ScratchTransport, self).__init__(base)
244
shutil.rmtree(self.base, ignore_errors=True)
245
mutter("%r destroyed" % self)