59
56
"""FTP failed for path: %(path)s%(extra)s"""
60
def _find_FTP(hostname, port, username, password, is_active):
61
"""Find an ftplib.FTP instance attached to this triplet."""
62
key = (hostname, port, username, password, is_active)
63
alt_key = (hostname, port, username, '********', is_active)
64
if key not in _FTP_cache:
65
mutter("Constructing FTP instance against %r" % (alt_key,))
68
conn.connect(host=hostname, port=port)
69
if username and username != 'anonymous' and not password:
70
password = bzrlib.ui.ui_factory.get_password(
71
prompt='FTP %(user)s@%(host)s password',
72
user=username, host=hostname)
73
conn.login(user=username, passwd=password)
74
conn.set_pasv(not is_active)
76
_FTP_cache[key] = conn
78
return _FTP_cache[key]
62
81
class FtpStatResult(object):
63
82
def __init__(self, f, relpath):
76
95
_number_of_retries = 2
77
96
_sleep_between_retries = 5
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):
98
class FtpTransport(Transport):
85
99
"""This is the transport agent for ftp:// access."""
87
def __init__(self, base, _from_transport=None):
101
def __init__(self, base, _provided_instance=None):
88
102
"""Set the base path where files will be stored."""
89
103
assert base.startswith('ftp://') or base.startswith('aftp://')
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
105
self.is_active = base.startswith('aftp://')
107
# urlparse won't handle aftp://
109
if not base.endswith('/'):
111
(self._proto, self._username,
112
self._password, self._host,
113
self._port, self._path) = split_url(base)
114
base = self._unparse_url()
116
super(FtpTransport, self).__init__(base)
117
self._FTP_instance = _provided_instance
119
def _unparse_url(self, path=None):
122
path = urllib.quote(path)
123
netloc = urllib.quote(self._host)
124
if self._username is not None:
125
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
126
if self._port is not None:
127
netloc = '%s:%d' % (netloc, self._port)
131
return urlparse.urlunparse((proto, netloc, path, '', '', ''))
98
133
def _get_FTP(self):
99
134
"""Return the ftplib.FTP instance for this object."""
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, '********',
135
if self._FTP_instance is not None:
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)
139
self._FTP_instance = _find_FTP(self._host, self._port,
140
self._username, self._password,
142
return self._FTP_instance
137
143
except ftplib.error_perm, e:
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):
144
raise errors.TransportError(msg="Error setting up connection: %s"
145
% str(e), orig_error=e)
147
def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
150
148
"""Try to translate an ftplib.error_perm exception.
152
150
:param err: The error to translate into a bzr error
191
def _remote_path(self, relpath):
187
def clone(self, offset=None):
188
"""Return a new FtpTransport with root at self.base + offset.
192
return FtpTransport(self.base, self._FTP_instance)
194
return FtpTransport(self.abspath(offset), self._FTP_instance)
196
def _abspath(self, relpath):
197
assert isinstance(relpath, basestring)
198
relpath = urlutils.unescape(relpath)
199
if relpath.startswith('/'):
202
basepath = self._path.split('/')
203
if len(basepath) > 0 and basepath[-1] == '':
204
basepath = basepath[:-1]
205
for p in relpath.split('/'):
207
if len(basepath) == 0:
208
# In most filesystems, a request for the parent
209
# of root, just returns root.
212
elif p == '.' or p == '':
216
# Possibly, we could use urlparse.urljoin() here, but
217
# I'm concerned about when it chooses to strip the last
218
# portion of the path, and when it doesn't.
192
220
# 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)
221
# at the same time, medusa won't handle utf8 paths
222
# So if we .encode(utf8) here, then we get a Server failure.
223
# while if we use str(), we get a UnicodeError, and the test suite
224
# just skips testing UnicodePaths.
225
return str('/'.join(basepath) or '/')
227
def abspath(self, relpath):
228
"""Return the full url to the given relative path.
229
This can be supplied with a string or a list
231
path = self._abspath(relpath)
232
return self._unparse_url(path)
202
234
def has(self, relpath):
203
235
"""Does the target location exist?"""
294
325
except ftplib.error_perm, e:
295
self._translate_perm_error(e, abspath, extra='could not store',
296
unknown_exc=errors.NoSuchFile)
326
self._translate_perm_error(e, abspath, extra='could not store')
297
327
except ftplib.error_temp, e:
298
328
if retries > _number_of_retries:
299
329
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
300
330
% self.abspath(relpath), orig_error=e)
302
332
warning("FTP temporary error: %s. Retrying.", str(e))
333
self._FTP_instance = None
304
334
self.put_file(relpath, fp, mode, retries+1)
306
336
if retries > _number_of_retries:
389
419
mutter("FTP site chmod: setting permissions to %s on %s",
390
str(mode), self._remote_path(relpath))
420
str(mode), self._abspath(relpath))
391
421
ftp = self._get_FTP()
392
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
422
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
394
424
except ftplib.error_perm, e:
395
425
# Command probably not available on this server
396
426
warning("FTP Could not set permissions to %s on %s. %s",
397
str(mode), self._remote_path(relpath), str(e))
427
str(mode), self._abspath(relpath), str(e))
399
429
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
400
430
# to copy something to another machine. And you may be able
401
431
# to give it its own address as the 'to' location.
402
432
# 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))
418
434
def move(self, rel_from, rel_to):
419
435
"""Move the item at rel_from to the location at rel_to"""
420
abs_from = self._remote_path(rel_from)
421
abs_to = self._remote_path(rel_to)
436
abs_from = self._abspath(rel_from)
437
abs_to = self._abspath(rel_to)
423
439
mutter("FTP mv: %s => %s", abs_from, abs_to)
424
440
f = self._get_FTP()
425
self._rename_and_overwrite(abs_from, abs_to, f)
441
f.rename(abs_from, abs_to)
426
442
except ftplib.error_perm, e:
427
443
self._translate_perm_error(e, abs_from,
428
444
extra='unable to rename to %r' % (rel_to,),
429
445
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))
440
449
def delete(self, relpath):
441
450
"""Delete the item at relpath"""
442
abspath = self._remote_path(relpath)
444
self._delete(abspath, f)
446
def _delete(self, abspath, f):
451
abspath = self._abspath(relpath)
448
453
mutter("FTP rm: %s", abspath)
449
455
f.delete(abspath)
450
456
except ftplib.error_perm, e:
451
457
self._translate_perm_error(e, abspath, 'error deleting',
452
458
unknown_exc=errors.NoSuchFile)
454
def external_url(self):
455
"""See bzrlib.transport.Transport.external_url."""
456
# FTP URL's are externally usable.
459
460
def listable(self):
460
461
"""See Transport.listable."""
463
464
def list_dir(self, relpath):
464
465
"""See Transport.list_dir."""
465
basepath = self._remote_path(relpath)
466
basepath = self._abspath(relpath)
466
467
mutter("FTP nlst: %s", basepath)
467
468
f = self._get_FTP()