60
59
"""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]
85
62
class FtpStatResult(object):
86
63
def __init__(self, f, relpath):
99
76
_number_of_retries = 2
100
77
_sleep_between_retries = 5
102
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):
103
85
"""This is the transport agent for ftp:// access."""
105
def __init__(self, base, _provided_instance=None):
87
def __init__(self, base, _from_transport=None):
106
88
"""Set the base path where files will be stored."""
107
89
assert base.startswith('ftp://') or base.startswith('aftp://')
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, '', '', ''))
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
137
98
def _get_FTP(self):
138
99
"""Return the ftplib.FTP instance for this object."""
139
if self._FTP_instance is not None:
140
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, '********',
143
self._FTP_instance = _find_FTP(self._host, self._port,
144
self._username, self._password,
146
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)
147
137
except ftplib.error_perm, e:
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):
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):
152
150
"""Try to translate an ftplib.error_perm exception.
154
152
:param err: The error to translate into a bzr error
193
def clone(self, offset=None):
194
"""Return a new FtpTransport with root at self.base + offset.
198
return FtpTransport(self.base, self._FTP_instance)
200
return FtpTransport(self.abspath(offset), self._FTP_instance)
202
def _abspath(self, relpath):
203
assert isinstance(relpath, basestring)
204
relpath = urlutils.unescape(relpath)
205
if relpath.startswith('/'):
208
basepath = self._path.split('/')
209
if len(basepath) > 0 and basepath[-1] == '':
210
basepath = basepath[:-1]
211
for p in relpath.split('/'):
213
if len(basepath) == 0:
214
# In most filesystems, a request for the parent
215
# of root, just returns root.
218
elif p == '.' or p == '':
222
# Possibly, we could use urlparse.urljoin() here, but
223
# I'm concerned about when it chooses to strip the last
224
# portion of the path, and when it doesn't.
191
def _remote_path(self, relpath):
226
192
# XXX: It seems that ftplib does not handle Unicode paths
227
# at the same time, medusa won't handle utf8 paths
228
# So if we .encode(utf8) here, then we get a Server failure.
229
# while if we use str(), we get a UnicodeError, and the test suite
230
# just skips testing UnicodePaths.
231
return str('/'.join(basepath) or '/')
233
def abspath(self, relpath):
234
"""Return the full url to the given relative path.
235
This can be supplied with a string or a list
237
path = self._abspath(relpath)
238
return self._unparse_url(path)
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)
240
202
def has(self, relpath):
241
203
"""Does the target location exist?"""
427
389
mutter("FTP site chmod: setting permissions to %s on %s",
428
str(mode), self._abspath(relpath))
390
str(mode), self._remote_path(relpath))
429
391
ftp = self._get_FTP()
430
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
392
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
432
394
except ftplib.error_perm, e:
433
395
# Command probably not available on this server
434
396
warning("FTP Could not set permissions to %s on %s. %s",
435
str(mode), self._abspath(relpath), str(e))
397
str(mode), self._remote_path(relpath), str(e))
437
399
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
438
400
# to copy something to another machine. And you may be able