59
60
"""FTP failed for path: %(path)s%(extra)s"""
64
def _find_FTP(hostname, port, username, password, is_active):
65
"""Find an ftplib.FTP instance attached to this triplet."""
66
key = (hostname, port, username, password, is_active)
67
alt_key = (hostname, port, username, '********', is_active)
68
if key not in _FTP_cache:
69
mutter("Constructing FTP instance against %r" % (alt_key,))
72
conn.connect(host=hostname, port=port)
73
if username and username != 'anonymous' and not password:
74
password = bzrlib.ui.ui_factory.get_password(
75
prompt='FTP %(user)s@%(host)s password',
76
user=username, host=hostname)
77
conn.login(user=username, passwd=password)
78
conn.set_pasv(not is_active)
80
_FTP_cache[key] = conn
82
return _FTP_cache[key]
62
85
class FtpStatResult(object):
63
86
def __init__(self, f, relpath):
76
99
_number_of_retries = 2
77
100
_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):
102
class FtpTransport(Transport):
85
103
"""This is the transport agent for ftp:// access."""
87
def __init__(self, base, _from_transport=None):
105
def __init__(self, base, _provided_instance=None):
88
106
"""Set the base path where files will be stored."""
89
107
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
109
self.is_active = base.startswith('aftp://')
111
# urlparse won't handle aftp://
113
if not base.endswith('/'):
115
(self._proto, self._username,
116
self._password, self._host,
117
self._port, self._path) = split_url(base)
118
base = self._unparse_url()
120
super(FtpTransport, self).__init__(base)
121
self._FTP_instance = _provided_instance
123
def _unparse_url(self, path=None):
126
path = urllib.quote(path)
127
netloc = urllib.quote(self._host)
128
if self._username is not None:
129
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
130
if self._port is not None:
131
netloc = '%s:%d' % (netloc, self._port)
135
return urlparse.urlunparse((proto, netloc, path, '', '', ''))
98
137
def _get_FTP(self):
99
138
"""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, '********',
139
if self._FTP_instance is not None:
140
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)
143
self._FTP_instance = _find_FTP(self._host, self._port,
144
self._username, self._password,
146
return self._FTP_instance
137
147
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):
148
raise errors.TransportError(msg="Error setting up connection: %s"
149
% str(e), orig_error=e)
151
def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
150
152
"""Try to translate an ftplib.error_perm exception.
152
154
:param err: The error to translate into a bzr error
191
def _remote_path(self, relpath):
192
def clone(self, offset=None):
193
"""Return a new FtpTransport with root at self.base + offset.
197
return FtpTransport(self.base, self._FTP_instance)
199
return FtpTransport(self.abspath(offset), self._FTP_instance)
201
def _abspath(self, relpath):
202
assert isinstance(relpath, basestring)
203
relpath = urlutils.unescape(relpath)
204
if relpath.startswith('/'):
207
basepath = self._path.split('/')
208
if len(basepath) > 0 and basepath[-1] == '':
209
basepath = basepath[:-1]
210
for p in relpath.split('/'):
212
if len(basepath) == 0:
213
# In most filesystems, a request for the parent
214
# of root, just returns root.
217
elif p == '.' or p == '':
221
# Possibly, we could use urlparse.urljoin() here, but
222
# I'm concerned about when it chooses to strip the last
223
# portion of the path, and when it doesn't.
192
225
# 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)
226
# at the same time, medusa won't handle utf8 paths
227
# So if we .encode(utf8) here, then we get a Server failure.
228
# while if we use str(), we get a UnicodeError, and the test suite
229
# just skips testing UnicodePaths.
230
return str('/'.join(basepath) or '/')
232
def abspath(self, relpath):
233
"""Return the full url to the given relative path.
234
This can be supplied with a string or a list
236
path = self._abspath(relpath)
237
return self._unparse_url(path)
202
239
def has(self, relpath):
203
240
"""Does the target location exist?"""
294
331
except ftplib.error_perm, e:
295
self._translate_perm_error(e, abspath, extra='could not store',
296
unknown_exc=errors.NoSuchFile)
332
self._translate_perm_error(e, abspath, extra='could not store')
297
333
except ftplib.error_temp, e:
298
334
if retries > _number_of_retries:
299
335
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
300
336
% self.abspath(relpath), orig_error=e)
302
338
warning("FTP temporary error: %s. Retrying.", str(e))
339
self._FTP_instance = None
304
340
self.put_file(relpath, fp, mode, retries+1)
306
342
if retries > _number_of_retries:
389
425
mutter("FTP site chmod: setting permissions to %s on %s",
390
str(mode), self._remote_path(relpath))
426
str(mode), self._abspath(relpath))
391
427
ftp = self._get_FTP()
392
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
428
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
394
430
except ftplib.error_perm, e:
395
431
# Command probably not available on this server
396
432
warning("FTP Could not set permissions to %s on %s. %s",
397
str(mode), self._remote_path(relpath), str(e))
433
str(mode), self._abspath(relpath), str(e))
399
435
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
400
436
# to copy something to another machine. And you may be able