77
79
# FIXME: there are inconsistencies in the way temporary errors are
78
80
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
79
81
# be taken to analyze the implications for write operations (read operations
80
# are safe to retry). Overall even some read operations are never
81
# retried. --vila 20070720 (Bug #127164)
82
# are safe to retry). Overall even some read operations are never retried.
82
83
class FtpTransport(ConnectedTransport):
83
84
"""This is the transport agent for ftp:// access."""
85
def __init__(self, base, _from_transport=None):
86
def __init__(self, base, from_transport=None):
86
87
"""Set the base path where files will be stored."""
87
if not (base.startswith('ftp://') or base.startswith('aftp://')):
88
raise ValueError(base)
89
super(FtpTransport, self).__init__(base,
90
_from_transport=_from_transport)
88
assert base.startswith('ftp://') or base.startswith('aftp://')
89
super(FtpTransport, self).__init__(base, from_transport)
91
90
self._unqualified_scheme = 'ftp'
92
91
if self._scheme == 'aftp':
93
92
self.is_active = True
95
94
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
101
96
def _get_FTP(self):
102
97
"""Return the ftplib.FTP instance for this object."""
103
98
# Ensures that a connection is established
118
111
:return: The created connection and its associated credentials.
120
The input credentials are only the password as it may have been
121
entered interactively by the user and may be different from the one
122
provided in base url at transport creation time. The returned
123
credentials are username, password.
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.
125
117
if credentials is None:
126
user, password = self._user, self._password
118
password = self._password
128
user, password = credentials
120
password = credentials
130
auth = config.AuthenticationConfig()
132
user = auth.get_user('ftp', self._host, port=self._port,
133
default=getpass.getuser())
134
122
mutter("Constructing FTP instance against %r" %
135
((self._host, self._port, user, '********',
123
((self._host, self._port, self._user, '********',
136
124
self.is_active),))
138
connection = self.connection_class()
126
connection = ftplib.FTP()
139
127
connection.connect(host=self._host, port=self._port)
140
self._login(connection, auth, user, password)
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)
141
134
connection.set_pasv(not self.is_active)
142
# binary mode is the default
143
connection.voidcmd('TYPE I')
144
except socket.error, e:
145
raise errors.SocketConnectionError(self._host, self._port,
146
msg='Unable to connect to',
148
135
except ftplib.error_perm, e:
149
136
raise errors.TransportError(msg="Error setting up connection:"
150
137
" %s" % str(e), orig_error=e)
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)
138
return connection, password
160
140
def _reconnect(self):
161
141
"""Create a new connection with the previously used credentials"""
162
credentials = self._get_credentials()
142
credentials = self.get_credentials()
163
143
connection, credentials = self._create_connection(credentials)
164
144
self._set_connection(connection, credentials)
405
355
abspath = self._remote_path(relpath)
406
356
mutter("FTP appe (try %d) to %s", retries, abspath)
407
357
ftp = self._get_FTP()
358
ftp.voidcmd("TYPE I")
408
359
cmd = "APPE %s" % abspath
409
360
conn = ftp.transfercmd(cmd)
410
361
conn.sendall(text)
412
self._setmode(relpath, mode)
364
self._setmode(relpath, mode)
414
366
except ftplib.error_perm, e:
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)
367
self._translate_perm_error(e, abspath, extra='error appending',
368
unknown_exc=errors.NoSuchFile)
424
369
except ftplib.error_temp, e:
425
370
if retries > _number_of_retries:
426
raise errors.TransportError(
427
"FTP temporary error during APPEND %s. Aborting."
428
% abspath, orig_error=e)
371
raise errors.TransportError("FTP temporary error during APPEND %s." \
372
"Aborting." % abspath, orig_error=e)
430
374
warning("FTP temporary error: %s. Retrying.", str(e))
431
375
self._reconnect()
432
376
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)
441
378
def _setmode(self, relpath, mode):
442
379
"""Set permissions on a path.
444
381
Only set permissions if the FTP server supports the 'SITE CHMOD'
449
mutter("FTP site chmod: setting permissions to %s on %s",
450
oct(mode), self._remote_path(relpath))
451
ftp = self._get_FTP()
452
cmd = "SITE CHMOD %s %s" % (oct(mode),
453
self._remote_path(relpath))
455
except ftplib.error_perm, e:
456
# Command probably not available on this server
457
warning("FTP Could not set permissions to %s on %s. %s",
458
oct(mode), self._remote_path(relpath), str(e))
385
mutter("FTP site chmod: setting permissions to %s on %s",
386
str(mode), self._remote_path(relpath))
387
ftp = self._get_FTP()
388
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
390
except ftplib.error_perm, e:
391
# Command probably not available on this server
392
warning("FTP Could not set permissions to %s on %s. %s",
393
str(mode), self._remote_path(relpath), str(e))
460
395
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
461
396
# to copy something to another machine. And you may be able
603
516
return self.lock_read(relpath)
519
class FtpServer(Server):
520
"""Common code for SFTP server facilities."""
524
self._ftp_server = None
526
self._async_thread = None
531
"""Calculate an ftp url to this server."""
532
return 'ftp://foo:bar@localhost:%d/' % (self._port)
534
# def get_bogus_url(self):
535
# """Return a URL which cannot be connected to."""
536
# return 'ftp://127.0.0.1:1'
538
def log(self, message):
539
"""This is used by medusa.ftp_server to log connections, etc."""
540
self.logs.append(message)
542
def setUp(self, vfs_server=None):
544
raise RuntimeError('Must have medusa to run the FtpServer')
546
assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
547
"FtpServer currently assumes local transport, got %s" % vfs_server
549
self._root = os.getcwdu()
550
self._ftp_server = _ftp_server(
551
authorizer=_test_authorizer(root=self._root),
553
port=0, # bind to a random port
555
logger_object=self # Use FtpServer.log() for messages
557
self._port = self._ftp_server.getsockname()[1]
558
# Don't let it loop forever, or handle an infinite number of requests.
559
# In this case it will run for 1000s, or 10000 requests
560
self._async_thread = threading.Thread(
561
target=FtpServer._asyncore_loop_ignore_EBADF,
562
kwargs={'timeout':0.1, 'count':10000})
563
self._async_thread.setDaemon(True)
564
self._async_thread.start()
567
"""See bzrlib.transport.Server.tearDown."""
568
# have asyncore release the channel
569
self._ftp_server.del_channel()
571
self._async_thread.join()
574
def _asyncore_loop_ignore_EBADF(*args, **kwargs):
575
"""Ignore EBADF during server shutdown.
577
We close the socket to get the server to shutdown, but this causes
578
select.select() to raise EBADF.
581
asyncore.loop(*args, **kwargs)
582
# FIXME: If we reach that point, we should raise an exception
583
# explaining that the 'count' parameter in setUp is too low or
584
# testers may wonder why their test just sits there waiting for a
585
# server that is already dead. Note that if the tester waits too
586
# long under pdb the server will also die.
587
except select.error, e:
588
if e.args[0] != errno.EBADF:
594
_test_authorizer = None
598
global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
601
import medusa.filesys
602
import medusa.ftp_server
608
class test_authorizer(object):
609
"""A custom Authorizer object for running the test suite.
611
The reason we cannot use dummy_authorizer, is because it sets the
612
channel to readonly, which we don't always want to do.
615
def __init__(self, root):
618
def authorize(self, channel, username, password):
619
"""Return (success, reply_string, filesystem)"""
621
return 0, 'No Medusa.', None
623
channel.persona = -1, -1
624
if username == 'anonymous':
625
channel.read_only = 1
627
channel.read_only = 0
629
return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
632
class ftp_channel(medusa.ftp_server.ftp_channel):
633
"""Customized ftp channel"""
635
def log(self, message):
636
"""Redirect logging requests."""
637
mutter('_ftp_channel: %s', message)
639
def log_info(self, message, type='info'):
640
"""Redirect logging requests."""
641
mutter('_ftp_channel %s: %s', type, message)
643
def cmd_rnfr(self, line):
644
"""Prepare for renaming a file."""
645
self._renaming = line[1]
646
self.respond('350 Ready for RNTO')
647
# TODO: jam 20060516 in testing, the ftp server seems to
648
# check that the file already exists, or it sends
649
# 550 RNFR command failed
651
def cmd_rnto(self, line):
652
"""Rename a file based on the target given.
654
rnto must be called after calling rnfr.
656
if not self._renaming:
657
self.respond('503 RNFR required first.')
658
pfrom = self.filesystem.translate(self._renaming)
659
self._renaming = None
660
pto = self.filesystem.translate(line[1])
661
if os.path.exists(pto):
662
self.respond('550 RNTO failed: file exists')
665
os.rename(pfrom, pto)
666
except (IOError, OSError), e:
667
# TODO: jam 20060516 return custom responses based on
668
# why the command failed
669
# (bialix 20070418) str(e) on Python 2.5 @ Windows
670
# sometimes don't provide expected error message;
671
# so we obtain such message via os.strerror()
672
self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
674
self.respond('550 RNTO failed')
675
# For a test server, we will go ahead and just die
678
self.respond('250 Rename successful.')
680
def cmd_size(self, line):
681
"""Return the size of a file
683
This is overloaded to help the test suite determine if the
684
target is a directory.
687
if not self.filesystem.isfile(filename):
688
if self.filesystem.isdir(filename):
689
self.respond('550 "%s" is a directory' % (filename,))
691
self.respond('550 "%s" is not a file' % (filename,))
693
self.respond('213 %d'
694
% (self.filesystem.stat(filename)[stat.ST_SIZE]),)
696
def cmd_mkd(self, line):
697
"""Create a directory.
699
Overloaded because default implementation does not distinguish
700
*why* it cannot make a directory.
703
self.command_not_understood(''.join(line))
707
self.filesystem.mkdir (path)
708
self.respond ('257 MKD command successful.')
709
except (IOError, OSError), e:
710
# (bialix 20070418) str(e) on Python 2.5 @ Windows
711
# sometimes don't provide expected error message;
712
# so we obtain such message via os.strerror()
713
self.respond ('550 error creating directory: %s' %
714
os.strerror(e.errno))
716
self.respond ('550 error creating directory.')
719
class ftp_server(medusa.ftp_server.ftp_server):
720
"""Customize the behavior of the Medusa ftp_server.
722
There are a few warts on the ftp_server, based on how it expects
726
ftp_channel_class = ftp_channel
728
def __init__(self, *args, **kwargs):
729
mutter('Initializing _ftp_server: %r, %r', args, kwargs)
730
medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
732
def log(self, message):
733
"""Redirect logging requests."""
734
mutter('_ftp_server: %s', message)
736
def log_info(self, message, type='info'):
737
"""Override the asyncore.log_info so we don't stipple the screen."""
738
mutter('_ftp_server %s: %s', type, message)
740
_test_authorizer = test_authorizer
741
_ftp_channel = ftp_channel
742
_ftp_server = ftp_server
606
747
def get_test_permutations():
607
748
"""Return the permutations to be used in testing."""
608
from bzrlib.tests import ftp_server
609
return [(FtpTransport, ftp_server.FTPTestServer)]
749
if not _setup_medusa():
750
warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
753
return [(FtpTransport, FtpServer)]