60
61
"""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
64
class FtpStatResult(object):
86
65
def __init__(self, f, relpath):
99
78
_number_of_retries = 2
100
79
_sleep_between_retries = 5
102
class FtpTransport(Transport):
81
# FIXME: there are inconsistencies in the way temporary errors are
82
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
83
# be taken to analyze the implications for write operations (read operations
84
# are safe to retry). Overall even some read operations are never
85
# retried. --vila 20070720 (Bug #127164)
86
class FtpTransport(ConnectedTransport):
103
87
"""This is the transport agent for ftp:// access."""
105
def __init__(self, base, _provided_instance=None):
89
def __init__(self, base, _from_transport=None):
106
90
"""Set the base path where files will be stored."""
107
91
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, '', '', ''))
92
super(FtpTransport, self).__init__(base,
93
_from_transport=_from_transport)
94
self._unqualified_scheme = 'ftp'
95
if self._scheme == 'aftp':
98
self.is_active = False
137
100
def _get_FTP(self):
138
101
"""Return the ftplib.FTP instance for this object."""
139
if self._FTP_instance is not None:
140
return self._FTP_instance
102
# Ensures that a connection is established
103
connection = self._get_connection()
104
if connection is None:
105
# First connection ever
106
connection, credentials = self._create_connection()
107
self._set_connection(connection, credentials)
110
def _create_connection(self, credentials=None):
111
"""Create a new connection with the provided credentials.
113
:param credentials: The credentials needed to establish the connection.
115
:return: The created connection and its associated credentials.
117
The credentials are only the password as it may have been entered
118
interactively by the user and may be different from the one provided
119
in base url at transport creation time.
121
if credentials is None:
122
password = self._password
124
password = credentials
126
mutter("Constructing FTP instance against %r" %
127
((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
130
connection = ftplib.FTP()
131
connection.connect(host=self._host, port=self._port)
132
if self._user and self._user != 'anonymous' and \
133
password is None: # '' is a valid password
134
get_password = bzrlib.ui.ui_factory.get_password
135
password = get_password(prompt='FTP %(user)s@%(host)s password',
136
user=self._user, host=self._host)
137
connection.login(user=self._user, passwd=password)
138
connection.set_pasv(not self.is_active)
147
139
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):
140
raise errors.TransportError(msg="Error setting up connection:"
141
" %s" % str(e), orig_error=e)
142
return connection, password
144
def _reconnect(self):
145
"""Create a new connection with the previously used credentials"""
146
credentials = self.get_credentials()
147
connection, credentials = self._create_connection(credentials)
148
self._set_connection(connection, credentials)
150
def _translate_perm_error(self, err, path, extra=None,
151
unknown_exc=FtpPathError):
152
152
"""Try to translate an ftplib.error_perm exception.
154
154
:param err: The error to translate into a bzr error
185
185
#raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
188
def should_cache(self):
189
"""Return True if the data pulled across should be cached locally.
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.
188
def _remote_path(self, relpath):
226
189
# 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)
190
# at the same time, medusa won't handle utf8 paths So if
191
# we .encode(utf8) here (see ConnectedTransport
192
# implementation), then we get a Server failure. while
193
# if we use str(), we get a UnicodeError, and the test
194
# suite just skips testing UnicodePaths.
195
relative = str(urlutils.unescape(relpath))
196
remote_path = self._combine_paths(self._path, relative)
240
199
def has(self, relpath):
241
200
"""Does the target location exist?"""
361
320
self._translate_perm_error(e, abspath,
362
321
unknown_exc=errors.FileExists)
323
def open_write_stream(self, relpath, mode=None):
324
"""See Transport.open_write_stream."""
325
self.put_bytes(relpath, "", mode)
326
result = AppendBasedFileStream(self, relpath)
327
_file_streams[self.abspath(relpath)] = result
330
def recommended_page_size(self):
331
"""See Transport.recommended_page_size().
333
For FTP we suggest a large page size to reduce the overhead
334
introduced by latency.
364
338
def rmdir(self, rel_path):
365
339
"""Delete the directory at rel_path"""
366
abspath = self._abspath(rel_path)
340
abspath = self._remote_path(rel_path)
368
342
mutter("FTP rmd: %s", abspath)
369
343
f = self._get_FTP()
427
401
mutter("FTP site chmod: setting permissions to %s on %s",
428
str(mode), self._abspath(relpath))
402
str(mode), self._remote_path(relpath))
429
403
ftp = self._get_FTP()
430
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
404
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
432
406
except ftplib.error_perm, e:
433
407
# Command probably not available on this server
434
408
warning("FTP Could not set permissions to %s on %s. %s",
435
str(mode), self._abspath(relpath), str(e))
409
str(mode), self._remote_path(relpath), str(e))
437
411
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
438
412
# to copy something to another machine. And you may be able