1
# Copyright (C) 2005-2012, 2016 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
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Transport for the local filesystem.
19
This is a fairly thin wrapper on regular file IO.
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16
"""Implementation of Transport for the local filesystem.
22
from __future__ import absolute_import
25
from stat import ST_MODE, S_ISDIR, S_IMODE
28
from bzrlib.lazy_import import lazy_import
29
lazy_import(globals(), """
39
from bzrlib.transport import LateReadError
42
from bzrlib import transport
45
_append_flags = os.O_CREAT | os.O_APPEND | os.O_WRONLY | osutils.O_BINARY | osutils.O_NOINHERIT
46
_put_non_atomic_flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | osutils.O_BINARY | osutils.O_NOINHERIT
49
class LocalTransport(transport.Transport):
19
from bzrlib.transport import Transport, register_transport, \
20
TransportError, NoSuchFile, FileExists
23
class LocalTransportError(TransportError):
26
class LocalTransport(Transport):
50
27
"""This is the transport agent for local filesystem access."""
52
29
def __init__(self, base):
53
30
"""Set the base path where files will be stored."""
54
if not base.startswith('file://'):
55
raise AssertionError("not a file:// url: %r" % base)
59
# Special case : windows has no "root", but does have
60
# multiple lettered drives inside it. #240910
61
if sys.platform == 'win32' and base == 'file:///':
64
super(LocalTransport, self).__init__(base)
67
super(LocalTransport, self).__init__(base)
68
self._local_base = urlutils.local_path_from_url(base)
69
if self._local_base[-1] != '/':
70
self._local_base = self._local_base + '/'
31
if base.startswith('file://'):
33
# realpath is incompatible with symlinks. When we traverse
34
# up we might be able to normpath stuff. RBC 20051003
35
super(LocalTransport, self).__init__(
36
os.path.normpath(os.path.abspath(base)))
38
def should_cache(self):
72
41
def clone(self, offset=None):
73
42
"""Return a new LocalTransport with root at self.base + offset
74
Because the local filesystem does not require a connection,
43
Because the local filesystem does not require a connection,
75
44
we can just return a new object.
78
47
return LocalTransport(self.base)
80
abspath = self.abspath(offset)
81
if abspath == 'file://':
82
# fix upwalk for UNC path
83
# when clone from //HOST/path updir recursively
84
# we should stop at least at //HOST part
86
return LocalTransport(abspath)
88
def _abspath(self, relative_reference):
89
"""Return a path for use in os calls.
91
Several assumptions are made:
92
- relative_reference does not contain '..'
93
- relative_reference is url escaped.
95
if relative_reference in ('.', ''):
96
# _local_base normally has a trailing slash; strip it so that stat
97
# on a transport pointing to a symlink reads the link not the
98
# referent but be careful of / and c:\
99
return osutils.split(self._local_base)[0]
100
return self._local_base + urlutils.unescape(relative_reference)
49
return LocalTransport(self.abspath(offset))
102
51
def abspath(self, relpath):
103
"""Return the full url to the given relative URL."""
104
# TODO: url escape the result. RBC 20060523.
105
# jam 20060426 Using normpath on the real path, because that ensures
106
# proper handling of stuff like
107
path = osutils.normpath(osutils.pathjoin(
108
self._local_base, urlutils.unescape(relpath)))
109
# on windows, our _local_base may or may not have a drive specified
110
# (ie, it may be "/" or "c:/foo").
111
# If 'relpath' is '/' we *always* get back an abspath without
112
# the drive letter - but if our transport already has a drive letter,
113
# we want our abspaths to have a drive letter too - so handle that
115
if (sys.platform == "win32" and self._local_base[1:2] == ":"
117
path = self._local_base[:3]
119
return urlutils.local_path_to_url(path)
121
def local_abspath(self, relpath):
122
"""Transform the given relative path URL into the actual path on disk
124
This function only exists for the LocalTransport, since it is
125
the only one that has direct local access.
126
This is mostly for stuff like WorkingTree which needs to know
127
the local working directory. The returned path will always contain
128
forward slashes as the path separator, regardless of the platform.
130
This function is quite expensive: it calls realpath which resolves
52
"""Return the full url to the given relative path.
53
This can be supplied with a string or a list
133
absurl = self.abspath(relpath)
134
# mutter(u'relpath %s => base: %s, absurl %s', relpath, self.base, absurl)
135
return urlutils.local_path_from_url(absurl)
55
if isinstance(relpath, basestring):
57
return os.path.join(self.base, *relpath)
137
59
def relpath(self, abspath):
138
60
"""Return the local path portion from a given absolute path.
143
return urlutils.file_relpath(self.base, abspath)
62
from bzrlib.branch import _relpath
63
return _relpath(self.base, abspath)
145
65
def has(self, relpath):
146
return os.access(self._abspath(relpath), os.F_OK)
66
return os.access(self.abspath(relpath), os.F_OK)
148
68
def get(self, relpath):
149
69
"""Get the file at the given relative path.
151
71
:param relpath: The relative path to the file
153
canonical_url = self.abspath(relpath)
154
if canonical_url in transport._file_streams:
155
transport._file_streams[canonical_url].flush()
157
path = self._abspath(relpath)
158
return osutils.open_file(path, 'rb')
159
except (IOError, OSError),e:
160
if e.errno == errno.EISDIR:
161
return LateReadError(relpath)
162
self._translate_error(e, path)
164
def put_file(self, relpath, f, mode=None):
165
"""Copy the file-like object into the location.
167
:param relpath: Location to put the contents, relative to base.
168
:param f: File-like object.
169
:param mode: The mode for the newly created file,
170
None means just use the default
175
path = self._abspath(relpath)
176
osutils.check_legal_path(path)
177
fp = atomicfile.AtomicFile(path, 'wb', new_mode=mode)
178
except (IOError, OSError),e:
179
self._translate_error(e, path)
181
length = self._pump(f, fp)
187
def put_bytes(self, relpath, raw_bytes, mode=None):
188
"""Copy the string into the location.
190
:param relpath: Location to put the contents, relative to base.
191
:param raw_bytes: String
193
if not isinstance(raw_bytes, str):
195
'raw_bytes must be a plain string, not %s' % type(raw_bytes))
198
path = self._abspath(relpath)
199
osutils.check_legal_path(path)
200
fp = atomicfile.AtomicFile(path, 'wb', new_mode=mode)
201
except (IOError, OSError),e:
202
self._translate_error(e, path)
210
def _put_non_atomic_helper(self, relpath, writer,
212
create_parent_dir=False,
214
"""Common functionality information for the put_*_non_atomic.
216
This tracks all the create_parent_dir stuff.
218
:param relpath: the path we are putting to.
219
:param writer: A function that takes an os level file descriptor
220
and writes whatever data it needs to write there.
221
:param mode: The final file mode.
222
:param create_parent_dir: Should we be creating the parent directory
225
abspath = self._abspath(relpath)
227
# os.open() will automatically use the umask
232
fd = os.open(abspath, _put_non_atomic_flags, local_mode)
233
except (IOError, OSError),e:
234
# We couldn't create the file, maybe we need to create
235
# the parent directory, and try again
236
if (not create_parent_dir
237
or e.errno not in (errno.ENOENT,errno.ENOTDIR)):
238
self._translate_error(e, relpath)
239
parent_dir = os.path.dirname(abspath)
241
self._translate_error(e, relpath)
242
self._mkdir(parent_dir, mode=dir_mode)
243
# We created the parent directory, lets try to open the
246
fd = os.open(abspath, _put_non_atomic_flags, local_mode)
247
except (IOError, OSError), e:
248
self._translate_error(e, relpath)
251
if mode is not None and mode != S_IMODE(st.st_mode):
252
# Because of umask, we may still need to chmod the file.
253
# But in the general case, we won't have to
254
osutils.chmod_if_possible(abspath, mode)
259
def put_file_non_atomic(self, relpath, f, mode=None,
260
create_parent_dir=False,
262
"""Copy the file-like object into the target location.
264
This function is not strictly safe to use. It is only meant to
265
be used when you already know that the target does not exist.
266
It is not safe, because it will open and truncate the remote
267
file. So there may be a time when the file has invalid contents.
269
:param relpath: The remote location to put the contents.
270
:param f: File-like object.
271
:param mode: Possible access permissions for new file.
272
None means do not set remote permissions.
273
:param create_parent_dir: If we cannot create the target file because
274
the parent directory does not exist, go ahead and
275
create it, and then try again.
278
self._pump_to_fd(f, fd)
279
self._put_non_atomic_helper(relpath, writer, mode=mode,
280
create_parent_dir=create_parent_dir,
283
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
284
create_parent_dir=False, dir_mode=None):
288
self._put_non_atomic_helper(relpath, writer, mode=mode,
289
create_parent_dir=create_parent_dir,
292
def iter_files_recursive(self):
293
"""Iter the relative paths of files in the transports sub-tree."""
294
queue = list(self.list_dir(u'.'))
296
relpath = queue.pop(0)
297
st = self.stat(relpath)
298
if S_ISDIR(st[ST_MODE]):
299
for i, basename in enumerate(self.list_dir(relpath)):
300
queue.insert(i, relpath+'/'+basename)
304
def _mkdir(self, abspath, mode=None):
305
"""Create a real directory, filtering through mode"""
307
# os.mkdir() will filter through umask
312
os.mkdir(abspath, local_mode)
313
except (IOError, OSError),e:
314
self._translate_error(e, abspath)
317
osutils.chmod_if_possible(abspath, mode)
318
except (IOError, OSError), e:
319
self._translate_error(e, abspath)
321
def mkdir(self, relpath, mode=None):
74
path = self.abspath(relpath)
75
return open(path, 'rb')
77
if e.errno in (errno.ENOENT, errno.ENOTDIR):
78
raise NoSuchFile('File or directory %r does not exist' % path, orig_error=e)
79
raise LocalTransportError(orig_error=e)
81
def get_partial(self, relpath, start, length=None):
82
"""Get just part of a file.
84
:param relpath: Path to the file, relative to base
85
:param start: The starting position to read from
86
:param length: The length to read. A length of None indicates
87
read to the end of the file.
88
:return: A file-like object containing at least the specified bytes.
89
Some implementations may return objects which can be read
90
past this length, but this is not guaranteed.
92
# LocalTransport.get_partial() doesn't care about the length
93
# argument, because it is using a local file, and thus just
94
# returns the file seek'ed to the appropriate location.
96
path = self.abspath(relpath)
101
if e.errno == errno.ENOENT:
102
raise NoSuchFile('File %r does not exist' % path, orig_error=e)
103
raise LocalTransportError(orig_error=e)
105
def put(self, relpath, f):
106
"""Copy the file-like or string object into the location.
108
:param relpath: Location to put the contents, relative to base.
109
:param f: File-like or string object.
111
from bzrlib.atomicfile import AtomicFile
114
path = self.abspath(relpath)
115
fp = AtomicFile(path, 'wb')
117
if e.errno == errno.ENOENT:
118
raise NoSuchFile('File %r does not exist' % path, orig_error=e)
119
raise LocalTransportError(orig_error=e)
126
def mkdir(self, relpath):
322
127
"""Create a directory at the given path."""
323
self._mkdir(self._abspath(relpath), mode=mode)
325
def open_write_stream(self, relpath, mode=None):
326
"""See Transport.open_write_stream."""
327
abspath = self._abspath(relpath)
329
handle = osutils.open_file(abspath, 'wb')
330
except (IOError, OSError),e:
331
self._translate_error(e, abspath)
334
self._check_mode_and_size(abspath, handle.fileno(), mode)
335
transport._file_streams[self.abspath(relpath)] = handle
336
return transport.FileFileStream(self, relpath, handle)
338
def _get_append_file(self, relpath, mode=None):
339
"""Call os.open() for the given relpath"""
340
file_abspath = self._abspath(relpath)
342
# os.open() will automatically use the umask
347
return file_abspath, os.open(file_abspath, _append_flags, local_mode)
348
except (IOError, OSError),e:
349
self._translate_error(e, relpath)
351
def _check_mode_and_size(self, file_abspath, fd, mode=None):
352
"""Check the mode of the file, and return the current size"""
354
if mode is not None and mode != S_IMODE(st.st_mode):
355
# Because of umask, we may still need to chmod the file.
356
# But in the general case, we won't have to
357
osutils.chmod_if_possible(file_abspath, mode)
360
def append_file(self, relpath, f, mode=None):
361
"""Append the text in the file-like object into the final location."""
362
file_abspath, fd = self._get_append_file(relpath, mode=mode)
364
result = self._check_mode_and_size(file_abspath, fd, mode=mode)
365
self._pump_to_fd(f, fd)
370
def append_bytes(self, relpath, bytes, mode=None):
371
"""Append the text in the string into the final location."""
372
file_abspath, fd = self._get_append_file(relpath, mode=mode)
374
result = self._check_mode_and_size(file_abspath, fd, mode=mode)
381
def _pump_to_fd(self, fromfile, to_fd):
382
"""Copy contents of one file to another."""
385
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')
390
144
def copy(self, rel_from, rel_to):
391
145
"""Copy the item at rel_from to the location at rel_to"""
392
path_from = self._abspath(rel_from)
393
path_to = self._abspath(rel_to)
147
path_from = self.abspath(rel_from)
148
path_to = self.abspath(rel_to)
395
150
shutil.copy(path_from, path_to)
396
except (IOError, OSError),e:
397
# TODO: What about path_to?
398
self._translate_error(e, path_from)
400
def rename(self, rel_from, rel_to):
401
path_from = self._abspath(rel_from)
402
path_to = self._abspath(rel_to)
404
# *don't* call bzrlib.osutils.rename, because we want to
405
# detect conflicting names on rename, and osutils.rename tries to
406
# mask cross-platform differences there
407
os.rename(path_from, path_to)
408
except (IOError, OSError),e:
409
# TODO: What about path_to?
410
self._translate_error(e, path_from)
152
raise LocalTransportError(orig_error=e)
412
154
def move(self, rel_from, rel_to):
413
155
"""Move the item at rel_from to the location at rel_to"""
414
path_from = self._abspath(rel_from)
415
path_to = self._abspath(rel_to)
156
path_from = self.abspath(rel_from)
157
path_to = self.abspath(rel_to)
418
# this version will delete the destination if necessary
419
osutils.rename(path_from, path_to)
420
except (IOError, OSError),e:
421
# TODO: What about path_to?
422
self._translate_error(e, path_from)
160
os.rename(path_from, path_to)
162
raise LocalTransportError(orig_error=e)
424
164
def delete(self, relpath):
425
165
"""Delete the item at relpath"""
428
path = self._abspath(relpath)
430
except (IOError, OSError),e:
431
self._translate_error(e, path)
433
def external_url(self):
434
"""See bzrlib.transport.Transport.external_url."""
435
# File URL's are externally usable.
438
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):
439
172
"""Copy a set of entries from self into another Transport.
441
174
:param relpaths: A list/generator of entries to be copied.