1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
1
# Copyright (C) 2005, 2006, 2007, 2008, 2009 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
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
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
"""Implementation of Transport over ftp.
18
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
65
59
class FtpStatResult(object):
66
def __init__(self, f, relpath):
61
def __init__(self, f, abspath):
68
self.st_size = f.size(relpath)
63
self.st_size = f.size(abspath)
69
64
self.st_mode = stat.S_IFREG
70
65
except ftplib.error_perm:
74
69
self.st_mode = stat.S_IFDIR
100
95
self.is_active = False
97
# Most modern FTP servers support the APPE command. If ours doesn't, we
98
# (re)set this flag accordingly later.
99
self._has_append = True
102
101
def _get_FTP(self):
103
102
"""Return the ftplib.FTP instance for this object."""
104
103
# Ensures that a connection is established
109
108
self._set_connection(connection, credentials)
110
109
return connection
111
connection_class = ftplib.FTP
112
113
def _create_connection(self, credentials=None):
113
114
"""Create a new connection with the provided credentials.
129
130
auth = config.AuthenticationConfig()
131
user = auth.get_user('ftp', self._host, port=self._port)
133
# Default to local user
134
user = getpass.getuser()
132
user = auth.get_user('ftp', self._host, port=self._port,
133
default=getpass.getuser())
136
134
mutter("Constructing FTP instance against %r" %
137
135
((self._host, self._port, user, '********',
138
136
self.is_active),))
140
connection = ftplib.FTP()
138
connection = self.connection_class()
141
139
connection.connect(host=self._host, port=self._port)
142
if user and user != 'anonymous' and \
143
password is None: # '' is a valid password
144
password = auth.get_password('ftp', self._host, user,
146
connection.login(user=user, passwd=password)
140
self._login(connection, auth, user, password)
147
141
connection.set_pasv(not self.is_active)
142
# binary mode is the default
143
connection.voidcmd('TYPE I')
148
144
except socket.error, e:
149
145
raise errors.SocketConnectionError(self._host, self._port,
150
146
msg='Unable to connect to',
154
150
" %s" % str(e), orig_error=e)
155
151
return connection, (user, password)
153
def _login(self, connection, auth, user, password):
154
# '' is a valid password
155
if user and user != 'anonymous' and password is None:
156
password = auth.get_password('ftp', self._host,
157
user, port=self._port)
158
connection.login(user=user, passwd=password)
157
160
def _reconnect(self):
158
161
"""Create a new connection with the previously used credentials"""
159
162
credentials = self._get_credentials()
197
200
raise unknown_exc(path, extra=extra)
198
# TODO: jam 20060516 Consider re-raising the error wrapped in
201
# TODO: jam 20060516 Consider re-raising the error wrapped in
199
202
# something like TransportError, but this loses the traceback
200
203
# Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
201
204
# to handle. Consider doing something like that here.
202
205
#raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
205
def _remote_path(self, relpath):
206
# XXX: It seems that ftplib does not handle Unicode paths
207
# at the same time, medusa won't handle utf8 paths So if
208
# we .encode(utf8) here (see ConnectedTransport
209
# implementation), then we get a Server failure. while
210
# if we use str(), we get a UnicodeError, and the test
211
# suite just skips testing UnicodePaths.
212
relative = str(urlutils.unescape(relpath))
213
remote_path = self._combine_paths(self._path, relative)
216
208
def has(self, relpath):
217
209
"""Does the target location exist?"""
218
210
# FIXME jam 20060516 We *do* ask about directories in the test suite
397
mutter("FTP appe to %s", abspath)
398
self._try_append(relpath, f.read(), mode)
391
mutter("FTP appe to %s", abspath)
392
self._try_append(relpath, text, mode)
394
self._fallback_append(relpath, text, mode)
402
398
def _try_append(self, relpath, text, mode=None, retries=0):
403
399
"""Try repeatedly to append the given text to the file at relpath.
405
401
This is a recursive function. On errors, it will be called until the
406
402
number of retries is exceeded.
409
405
abspath = self._remote_path(relpath)
410
406
mutter("FTP appe (try %d) to %s", retries, abspath)
411
407
ftp = self._get_FTP()
412
ftp.voidcmd("TYPE I")
413
408
cmd = "APPE %s" % abspath
414
409
conn = ftp.transfercmd(cmd)
415
410
conn.sendall(text)
417
412
self._setmode(relpath, mode)
419
414
except ftplib.error_perm, e:
420
self._translate_perm_error(e, abspath, extra='error appending',
421
unknown_exc=errors.NoSuchFile)
415
# Check whether the command is not supported (reply code 502)
416
if str(e).startswith('502 '):
417
warning("FTP server does not support file appending natively. "
418
"Performance may be severely degraded! (%s)", e)
419
self._has_append = False
420
self._fallback_append(relpath, text, mode)
422
self._translate_perm_error(e, abspath, extra='error appending',
423
unknown_exc=errors.NoSuchFile)
422
424
except ftplib.error_temp, e:
423
425
if retries > _number_of_retries:
424
raise errors.TransportError("FTP temporary error during APPEND %s." \
425
"Aborting." % abspath, orig_error=e)
426
raise errors.TransportError(
427
"FTP temporary error during APPEND %s. Aborting."
428
% abspath, orig_error=e)
427
430
warning("FTP temporary error: %s. Retrying.", str(e))
428
431
self._reconnect()
429
432
self._try_append(relpath, text, mode, retries+1)
434
def _fallback_append(self, relpath, text, mode = None):
435
remote = self.get(relpath)
436
remote.seek(0, os.SEEK_END)
439
return self.put_file(relpath, remote, mode)
431
441
def _setmode(self, relpath, mode):
432
442
"""Set permissions on a path.
439
449
mutter("FTP site chmod: setting permissions to %s on %s",
440
str(mode), self._remote_path(relpath))
450
oct(mode), self._remote_path(relpath))
441
451
ftp = self._get_FTP()
442
452
cmd = "SITE CHMOD %s %s" % (oct(mode),
443
453
self._remote_path(relpath))
445
455
except ftplib.error_perm, e:
446
456
# Command probably not available on this server
447
457
warning("FTP Could not set permissions to %s on %s. %s",
448
str(mode), self._remote_path(relpath), str(e))
458
oct(mode), self._remote_path(relpath), str(e))
450
460
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
451
461
# to copy something to another machine. And you may be able
476
486
self._rename_and_overwrite(abs_from, abs_to, f)
477
487
except ftplib.error_perm, e:
478
488
self._translate_perm_error(e, abs_from,
479
extra='unable to rename to %r' % (rel_to,),
489
extra='unable to rename to %r' % (rel_to,),
480
490
unknown_exc=errors.PathError)
482
492
def _rename_and_overwrite(self, abs_from, abs_to, f):
517
527
mutter("FTP nlst: %s", basepath)
518
528
f = self._get_FTP()
520
paths = f.nlst(basepath)
521
except ftplib.error_perm, e:
522
self._translate_perm_error(e, relpath, extra='error with list_dir')
523
except ftplib.error_temp, e:
524
# xs4all's ftp server raises a 450 temp error when listing an empty
525
# directory. Check for that and just return an empty list in that
526
# case. See bug #215522
527
if str(e).lower().startswith('450 no files found'):
528
mutter('FTP Server returned "%s" for nlst.'
529
' Assuming it means empty directory',
531
paths = f.nlst(basepath)
532
except ftplib.error_perm, e:
533
self._translate_perm_error(e, relpath,
534
extra='error with list_dir')
535
except ftplib.error_temp, e:
536
# xs4all's ftp server raises a 450 temp error when listing an
537
# empty directory. Check for that and just return an empty list
538
# in that case. See bug #215522
539
if str(e).lower().startswith('450 no files found'):
540
mutter('FTP Server returned "%s" for nlst.'
541
' Assuming it means empty directory',
546
# Restore binary mode as nlst switch to ascii mode to retrieve file
533
550
# If FTP.nlst returns paths prefixed by relpath, strip 'em
534
551
if paths and paths[0].startswith(basepath):
535
552
entries = [path[len(basepath)+1:] for path in paths]
589
606
def get_test_permutations():
590
607
"""Return the permutations to be used in testing."""
591
from bzrlib import tests
592
if tests.FTPServerFeature.available():
593
from bzrlib.tests import ftp_server
594
return [(FtpTransport, ftp_server.FTPServer)]
596
# Dummy server to have the test suite report the number of tests
597
# needing that feature. We raise UnavailableFeature from methods before
598
# the test server is being used. Doing so in the setUp method has bad
599
# side-effects (tearDown is never called).
600
class UnavailableFTPServer(object):
602
def setUp(self, vfs_server=None):
609
raise tests.UnavailableFeature(tests.FTPServerFeature)
611
def get_bogus_url(self):
612
raise tests.UnavailableFeature(tests.FTPServerFeature)
614
return [(FtpTransport, UnavailableFTPServer)]
608
from bzrlib.tests import ftp_server
609
return [(FtpTransport, ftp_server.FTPTestServer)]