53
59
"""FTP failed for path: %(path)s%(extra)s"""
57
def _find_FTP(hostname, port, username, password, is_active):
58
"""Find an ftplib.FTP instance attached to this triplet."""
59
key = (hostname, port, username, password, is_active)
60
alt_key = (hostname, port, username, '********', is_active)
61
if key not in _FTP_cache:
62
mutter("Constructing FTP instance against %r" % (alt_key,))
65
conn.connect(host=hostname, port=port)
66
if username and username != 'anonymous' and not password:
67
password = bzrlib.ui.ui_factory.get_password(
68
prompt='FTP %(user)s@%(host)s password',
69
user=username, host=hostname)
70
conn.login(user=username, passwd=password)
71
conn.set_pasv(not is_active)
73
_FTP_cache[key] = conn
75
return _FTP_cache[key]
78
62
class FtpStatResult(object):
79
63
def __init__(self, f, relpath):
92
76
_number_of_retries = 2
93
77
_sleep_between_retries = 5
95
class FtpTransport(Transport):
79
# FIXME: there are inconsistencies in the way temporary errors are
80
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
81
# be taken to analyze the implications for write operations (read operations
82
# are safe to retry). Overall even some read operations are never
83
# retried. --vila 20070720 (Bug #127164)
84
class FtpTransport(ConnectedTransport):
96
85
"""This is the transport agent for ftp:// access."""
98
def __init__(self, base, _provided_instance=None):
87
def __init__(self, base, _from_transport=None):
99
88
"""Set the base path where files will be stored."""
100
89
assert base.startswith('ftp://') or base.startswith('aftp://')
102
self.is_active = base.startswith('aftp://')
104
# urlparse won't handle aftp://
106
if not base.endswith('/'):
108
(self._proto, self._username,
109
self._password, self._host,
110
self._port, self._path) = split_url(base)
111
base = self._unparse_url()
113
super(FtpTransport, self).__init__(base)
114
self._FTP_instance = _provided_instance
116
def _unparse_url(self, path=None):
119
path = urllib.quote(path)
120
netloc = urllib.quote(self._host)
121
if self._username is not None:
122
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
123
if self._port is not None:
124
netloc = '%s:%d' % (netloc, self._port)
125
return urlparse.urlunparse(('ftp', netloc, path, '', '', ''))
90
super(FtpTransport, self).__init__(base,
91
_from_transport=_from_transport)
92
self._unqualified_scheme = 'ftp'
93
if self._scheme == 'aftp':
96
self.is_active = False
127
98
def _get_FTP(self):
128
99
"""Return the ftplib.FTP instance for this object."""
129
if self._FTP_instance is not None:
130
return self._FTP_instance
100
# Ensures that a connection is established
101
connection = self._get_connection()
102
if connection is None:
103
# First connection ever
104
connection, credentials = self._create_connection()
105
self._set_connection(connection, credentials)
108
def _create_connection(self, credentials=None):
109
"""Create a new connection with the provided credentials.
111
:param credentials: The credentials needed to establish the connection.
113
:return: The created connection and its associated credentials.
115
The credentials are only the password as it may have been entered
116
interactively by the user and may be different from the one provided
117
in base url at transport creation time.
119
if credentials is None:
120
password = self._password
122
password = credentials
124
mutter("Constructing FTP instance against %r" %
125
((self._host, self._port, self._user, '********',
133
self._FTP_instance = _find_FTP(self._host, self._port,
134
self._username, self._password,
136
return self._FTP_instance
128
connection = ftplib.FTP()
129
connection.connect(host=self._host, port=self._port)
130
if self._user and self._user != 'anonymous' and \
131
password is not None: # '' is a valid password
132
get_password = bzrlib.ui.ui_factory.get_password
133
password = get_password(prompt='FTP %(user)s@%(host)s password',
134
user=self._user, host=self._host)
135
connection.login(user=self._user, passwd=password)
136
connection.set_pasv(not self.is_active)
137
137
except ftplib.error_perm, e:
138
raise errors.TransportError(msg="Error setting up connection: %s"
139
% str(e), orig_error=e)
141
def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
138
raise errors.TransportError(msg="Error setting up connection:"
139
" %s" % str(e), orig_error=e)
140
return connection, password
142
def _reconnect(self):
143
"""Create a new connection with the previously used credentials"""
144
credentials = self.get_credentials()
145
connection, credentials = self._create_connection(credentials)
146
self._set_connection(connection, credentials)
148
def _translate_perm_error(self, err, path, extra=None,
149
unknown_exc=FtpPathError):
142
150
"""Try to translate an ftplib.error_perm exception.
144
152
:param err: The error to translate into a bzr error
181
def clone(self, offset=None):
182
"""Return a new FtpTransport with root at self.base + offset.
186
return FtpTransport(self.base, self._FTP_instance)
188
return FtpTransport(self.abspath(offset), self._FTP_instance)
190
def _abspath(self, relpath):
191
assert isinstance(relpath, basestring)
192
relpath = urllib.unquote(relpath)
193
relpath_parts = relpath.split('/')
194
if len(relpath_parts) > 1:
195
if relpath_parts[0] == '':
196
raise ValueError("path %r within branch %r seems to be absolute"
197
% (relpath, self._path))
198
basepath = self._path.split('/')
199
if len(basepath) > 0 and basepath[-1] == '':
200
basepath = basepath[:-1]
201
for p in relpath_parts:
203
if len(basepath) == 0:
204
# In most filesystems, a request for the parent
205
# of root, just returns root.
208
elif p == '.' or p == '':
212
# Possibly, we could use urlparse.urljoin() here, but
213
# I'm concerned about when it chooses to strip the last
214
# portion of the path, and when it doesn't.
215
return '/'.join(basepath) or '/'
217
def abspath(self, relpath):
218
"""Return the full url to the given relative path.
219
This can be supplied with a string or a list
221
path = self._abspath(relpath)
222
return self._unparse_url(path)
191
def _remote_path(self, relpath):
192
# XXX: It seems that ftplib does not handle Unicode paths
193
# at the same time, medusa won't handle utf8 paths So if
194
# we .encode(utf8) here (see ConnectedTransport
195
# implementation), then we get a Server failure. while
196
# if we use str(), we get a UnicodeError, and the test
197
# suite just skips testing UnicodePaths.
198
relative = str(urlutils.unescape(relpath))
199
remote_path = self._combine_paths(self._path, relative)
224
202
def has(self, relpath):
225
203
"""Does the target location exist?"""
290
268
:param retries: Number of retries after temporary failures so far
291
269
for this operation.
293
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
271
TODO: jam 20051215 ftp as a protocol seems to support chmod, but
295
abspath = self._abspath(relpath)
274
abspath = self._remote_path(relpath)
296
275
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
297
276
os.getpid(), random.randint(0,0x7FFFFFFF))
298
if not hasattr(fp, 'read'):
277
if getattr(fp, 'read', None) is None:
299
278
fp = StringIO(fp)
301
280
mutter("FTP put: %s", abspath)
302
281
f = self._get_FTP()
304
283
f.storbinary('STOR '+tmp_abspath, fp)
305
f.rename(tmp_abspath, abspath)
284
self._rename_and_overwrite(tmp_abspath, abspath, f)
306
285
except (ftplib.error_temp,EOFError), e:
307
286
warning("Failure during ftp PUT. Deleting temporary file.")
315
294
except ftplib.error_perm, e:
316
self._translate_perm_error(e, abspath, extra='could not store')
295
self._translate_perm_error(e, abspath, extra='could not store',
296
unknown_exc=errors.NoSuchFile)
317
297
except ftplib.error_temp, e:
318
298
if retries > _number_of_retries:
319
299
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
320
300
% self.abspath(relpath), orig_error=e)
322
302
warning("FTP temporary error: %s. Retrying.", str(e))
323
self._FTP_instance = None
324
self.put(relpath, fp, mode, retries+1)
304
self.put_file(relpath, fp, mode, retries+1)
326
306
if retries > _number_of_retries:
327
307
raise errors.TransportError("FTP control connection closed during PUT %s."
409
389
mutter("FTP site chmod: setting permissions to %s on %s",
410
str(mode), self._abspath(relpath))
390
str(mode), self._remote_path(relpath))
411
391
ftp = self._get_FTP()
412
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
392
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
414
394
except ftplib.error_perm, e:
415
395
# Command probably not available on this server
416
396
warning("FTP Could not set permissions to %s on %s. %s",
417
str(mode), self._abspath(relpath), str(e))
397
str(mode), self._remote_path(relpath), str(e))
419
399
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
420
400
# to copy something to another machine. And you may be able
421
401
# to give it its own address as the 'to' location.
422
402
# So implement a fancier 'copy()'
404
def rename(self, rel_from, rel_to):
405
abs_from = self._remote_path(rel_from)
406
abs_to = self._remote_path(rel_to)
407
mutter("FTP rename: %s => %s", abs_from, abs_to)
409
return self._rename(abs_from, abs_to, f)
411
def _rename(self, abs_from, abs_to, f):
413
f.rename(abs_from, abs_to)
414
except ftplib.error_perm, e:
415
self._translate_perm_error(e, abs_from,
416
': unable to rename to %r' % (abs_to))
424
418
def move(self, rel_from, rel_to):
425
419
"""Move the item at rel_from to the location at rel_to"""
426
abs_from = self._abspath(rel_from)
427
abs_to = self._abspath(rel_to)
420
abs_from = self._remote_path(rel_from)
421
abs_to = self._remote_path(rel_to)
429
423
mutter("FTP mv: %s => %s", abs_from, abs_to)
430
424
f = self._get_FTP()
431
f.rename(abs_from, abs_to)
425
self._rename_and_overwrite(abs_from, abs_to, f)
432
426
except ftplib.error_perm, e:
433
427
self._translate_perm_error(e, abs_from,
434
428
extra='unable to rename to %r' % (rel_to,),
435
429
unknown_exc=errors.PathError)
431
def _rename_and_overwrite(self, abs_from, abs_to, f):
432
"""Do a fancy rename on the remote server.
434
Using the implementation provided by osutils.
436
osutils.fancy_rename(abs_from, abs_to,
437
rename_func=lambda p1, p2: self._rename(p1, p2, f),
438
unlink_func=lambda p: self._delete(p, f))
439
440
def delete(self, relpath):
440
441
"""Delete the item at relpath"""
441
abspath = self._abspath(relpath)
442
abspath = self._remote_path(relpath)
444
self._delete(abspath, f)
446
def _delete(self, abspath, f):
443
448
mutter("FTP rm: %s", abspath)
445
449
f.delete(abspath)
446
450
except ftplib.error_perm, e:
447
451
self._translate_perm_error(e, abspath, 'error deleting',
448
452
unknown_exc=errors.NoSuchFile)
454
def external_url(self):
455
"""See bzrlib.transport.Transport.external_url."""
456
# FTP URL's are externally usable.
450
459
def listable(self):
451
460
"""See Transport.listable."""
454
463
def list_dir(self, relpath):
455
464
"""See Transport.list_dir."""
465
basepath = self._remote_path(relpath)
466
mutter("FTP nlst: %s", basepath)
457
mutter("FTP nlst: %s", self._abspath(relpath))
459
basepath = self._abspath(relpath)
460
469
paths = f.nlst(basepath)
461
# If FTP.nlst returns paths prefixed by relpath, strip 'em
462
if paths and paths[0].startswith(basepath):
463
paths = [path[len(basepath)+1:] for path in paths]
464
# Remove . and .. if present, and return
465
return [path for path in paths if path not in (".", "..")]
466
470
except ftplib.error_perm, e:
467
471
self._translate_perm_error(e, relpath, extra='error with list_dir')
472
# If FTP.nlst returns paths prefixed by relpath, strip 'em
473
if paths and paths[0].startswith(basepath):
474
entries = [path[len(basepath)+1:] for path in paths]
477
# Remove . and .. if present
478
return [urlutils.escape(entry) for entry in entries
479
if entry not in ('.', '..')]
469
481
def iter_files_recursive(self):
470
482
"""See Transport.iter_files_recursive.