1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
1
# Copyright (C) 2005 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
59
53
"""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]
62
78
class FtpStatResult(object):
63
79
def __init__(self, f, relpath):
76
92
_number_of_retries = 2
77
93
_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 retried.
83
class FtpTransport(ConnectedTransport):
95
class FtpTransport(Transport):
84
96
"""This is the transport agent for ftp:// access."""
86
def __init__(self, base, from_transport=None):
98
def __init__(self, base, _provided_instance=None):
87
99
"""Set the base path where files will be stored."""
88
100
assert base.startswith('ftp://') or base.startswith('aftp://')
89
super(FtpTransport, self).__init__(base, from_transport)
90
self._unqualified_scheme = 'ftp'
91
if self._scheme == 'aftp':
94
self.is_active = False
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, '', '', ''))
96
127
def _get_FTP(self):
97
128
"""Return the ftplib.FTP instance for this object."""
98
# Ensures that a connection is established
99
connection = self._get_connection()
100
if connection is None:
101
# First connection ever
102
connection, credentials = self._create_connection()
103
self._set_connection(connection, credentials)
106
def _create_connection(self, credentials=None):
107
"""Create a new connection with the provided credentials.
109
:param credentials: The credentials needed to establish the connection.
111
:return: The created connection and its associated credentials.
113
The credentials are only the password as it may have been entered
114
interactively by the user and may be different from the one provided
115
in base url at transport creation time.
117
if credentials is None:
118
password = self._password
120
password = credentials
122
mutter("Constructing FTP instance against %r" %
123
((self._host, self._port, self._user, '********',
129
if self._FTP_instance is not None:
130
return self._FTP_instance
126
connection = ftplib.FTP()
127
connection.connect(host=self._host, port=self._port)
128
if self._user and self._user != 'anonymous' and \
129
password is not None: # '' is a valid password
130
get_password = bzrlib.ui.ui_factory.get_password
131
password = get_password(prompt='FTP %(user)s@%(host)s password',
132
user=self._user, host=self._host)
133
connection.login(user=self._user, passwd=password)
134
connection.set_pasv(not self.is_active)
133
self._FTP_instance = _find_FTP(self._host, self._port,
134
self._username, self._password,
136
return self._FTP_instance
135
137
except ftplib.error_perm, e:
136
raise errors.TransportError(msg="Error setting up connection:"
137
" %s" % str(e), orig_error=e)
138
return connection, password
140
def _reconnect(self):
141
"""Create a new connection with the previously used credentials"""
142
credentials = self.get_credentials()
143
connection, credentials = self._create_connection(credentials)
144
self._set_connection(connection, credentials)
146
def _translate_perm_error(self, err, path, extra=None,
147
unknown_exc=FtpPathError):
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):
148
142
"""Try to translate an ftplib.error_perm exception.
150
144
:param err: The error to translate into a bzr error
189
def _remote_path(self, relpath):
190
# XXX: It seems that ftplib does not handle Unicode paths
191
# at the same time, medusa won't handle utf8 paths So if
192
# we .encode(utf8) here (see ConnectedTransport
193
# implementation), then we get a Server failure. while
194
# if we use str(), we get a UnicodeError, and the test
195
# suite just skips testing UnicodePaths.
196
relative = str(urlutils.unescape(relpath))
197
remote_path = self._combine_paths(self._path, relative)
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)
200
224
def has(self, relpath):
201
225
"""Does the target location exist?"""
266
290
:param retries: Number of retries after temporary failures so far
267
291
for this operation.
269
TODO: jam 20051215 ftp as a protocol seems to support chmod, but
293
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
272
abspath = self._remote_path(relpath)
295
abspath = self._abspath(relpath)
273
296
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
274
297
os.getpid(), random.randint(0,0x7FFFFFFF))
275
if getattr(fp, 'read', None) is None:
298
if not hasattr(fp, 'read'):
276
299
fp = StringIO(fp)
278
301
mutter("FTP put: %s", abspath)
279
302
f = self._get_FTP()
281
304
f.storbinary('STOR '+tmp_abspath, fp)
282
self._rename_and_overwrite(tmp_abspath, abspath, f)
305
f.rename(tmp_abspath, abspath)
283
306
except (ftplib.error_temp,EOFError), e:
284
307
warning("Failure during ftp PUT. Deleting temporary file.")
292
315
except ftplib.error_perm, e:
293
self._translate_perm_error(e, abspath, extra='could not store',
294
unknown_exc=errors.NoSuchFile)
316
self._translate_perm_error(e, abspath, extra='could not store')
295
317
except ftplib.error_temp, e:
296
318
if retries > _number_of_retries:
297
319
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
298
320
% self.abspath(relpath), orig_error=e)
300
322
warning("FTP temporary error: %s. Retrying.", str(e))
302
self.put_file(relpath, fp, mode, retries+1)
323
self._FTP_instance = None
324
self.put(relpath, fp, mode, retries+1)
304
326
if retries > _number_of_retries:
305
327
raise errors.TransportError("FTP control connection closed during PUT %s."
387
409
mutter("FTP site chmod: setting permissions to %s on %s",
388
str(mode), self._remote_path(relpath))
410
str(mode), self._abspath(relpath))
389
411
ftp = self._get_FTP()
390
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
412
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
392
414
except ftplib.error_perm, e:
393
415
# Command probably not available on this server
394
416
warning("FTP Could not set permissions to %s on %s. %s",
395
str(mode), self._remote_path(relpath), str(e))
417
str(mode), self._abspath(relpath), str(e))
397
419
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
398
420
# to copy something to another machine. And you may be able
399
421
# to give it its own address as the 'to' location.
400
422
# So implement a fancier 'copy()'
402
def rename(self, rel_from, rel_to):
403
abs_from = self._remote_path(rel_from)
404
abs_to = self._remote_path(rel_to)
405
mutter("FTP rename: %s => %s", abs_from, abs_to)
407
return self._rename(abs_from, abs_to, f)
409
def _rename(self, abs_from, abs_to, f):
411
f.rename(abs_from, abs_to)
412
except ftplib.error_perm, e:
413
self._translate_perm_error(e, abs_from,
414
': unable to rename to %r' % (abs_to))
416
424
def move(self, rel_from, rel_to):
417
425
"""Move the item at rel_from to the location at rel_to"""
418
abs_from = self._remote_path(rel_from)
419
abs_to = self._remote_path(rel_to)
426
abs_from = self._abspath(rel_from)
427
abs_to = self._abspath(rel_to)
421
429
mutter("FTP mv: %s => %s", abs_from, abs_to)
422
430
f = self._get_FTP()
423
self._rename_and_overwrite(abs_from, abs_to, f)
431
f.rename(abs_from, abs_to)
424
432
except ftplib.error_perm, e:
425
433
self._translate_perm_error(e, abs_from,
426
434
extra='unable to rename to %r' % (rel_to,),
427
435
unknown_exc=errors.PathError)
429
def _rename_and_overwrite(self, abs_from, abs_to, f):
430
"""Do a fancy rename on the remote server.
432
Using the implementation provided by osutils.
434
osutils.fancy_rename(abs_from, abs_to,
435
rename_func=lambda p1, p2: self._rename(p1, p2, f),
436
unlink_func=lambda p: self._delete(p, f))
438
439
def delete(self, relpath):
439
440
"""Delete the item at relpath"""
440
abspath = self._remote_path(relpath)
442
self._delete(abspath, f)
444
def _delete(self, abspath, f):
441
abspath = self._abspath(relpath)
446
443
mutter("FTP rm: %s", abspath)
447
445
f.delete(abspath)
448
446
except ftplib.error_perm, e:
449
447
self._translate_perm_error(e, abspath, 'error deleting',
456
454
def list_dir(self, relpath):
457
455
"""See Transport.list_dir."""
458
basepath = self._remote_path(relpath)
459
mutter("FTP nlst: %s", basepath)
457
mutter("FTP nlst: %s", self._abspath(relpath))
459
basepath = self._abspath(relpath)
462
460
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 (".", "..")]
463
466
except ftplib.error_perm, e:
464
467
self._translate_perm_error(e, relpath, extra='error with list_dir')
465
# If FTP.nlst returns paths prefixed by relpath, strip 'em
466
if paths and paths[0].startswith(basepath):
467
entries = [path[len(basepath)+1:] for path in paths]
470
# Remove . and .. if present
471
return [urlutils.escape(entry) for entry in entries
472
if entry not in ('.', '..')]
474
469
def iter_files_recursive(self):
475
470
"""See Transport.iter_files_recursive.