118
115
: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.
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.
125
121
if credentials is None:
126
user, password = self._user, self._password
122
password = self._password
128
user, password = credentials
124
password = credentials
130
auth = config.AuthenticationConfig()
132
user = auth.get_user('ftp', self._host, port=self._port,
133
default=getpass.getuser())
134
126
mutter("Constructing FTP instance against %r" %
135
((self._host, self._port, user, '********',
127
((self._host, self._port, self._user, '********',
136
128
self.is_active),))
138
connection = self.connection_class()
130
connection = ftplib.FTP()
139
131
connection.connect(host=self._host, port=self._port)
140
self._login(connection, auth, user, password)
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)
141
138
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
139
except ftplib.error_perm, e:
149
140
raise errors.TransportError(msg="Error setting up connection:"
150
141
" %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)
142
return connection, password
160
144
def _reconnect(self):
161
145
"""Create a new connection with the previously used credentials"""
405
391
abspath = self._remote_path(relpath)
406
392
mutter("FTP appe (try %d) to %s", retries, abspath)
407
393
ftp = self._get_FTP()
394
ftp.voidcmd("TYPE I")
408
395
cmd = "APPE %s" % abspath
409
396
conn = ftp.transfercmd(cmd)
410
397
conn.sendall(text)
412
self._setmode(relpath, mode)
400
self._setmode(relpath, mode)
414
402
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)
403
self._translate_perm_error(e, abspath, extra='error appending',
404
unknown_exc=errors.NoSuchFile)
424
405
except ftplib.error_temp, e:
425
406
if retries > _number_of_retries:
426
raise errors.TransportError(
427
"FTP temporary error during APPEND %s. Aborting."
428
% abspath, orig_error=e)
407
raise errors.TransportError("FTP temporary error during APPEND %s." \
408
"Aborting." % abspath, orig_error=e)
430
410
warning("FTP temporary error: %s. Retrying.", str(e))
431
411
self._reconnect()
432
412
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
414
def _setmode(self, relpath, mode):
442
415
"""Set permissions on a path.
444
417
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))
421
mutter("FTP site chmod: setting permissions to %s on %s",
422
str(mode), self._remote_path(relpath))
423
ftp = self._get_FTP()
424
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
426
except ftplib.error_perm, e:
427
# Command probably not available on this server
428
warning("FTP Could not set permissions to %s on %s. %s",
429
str(mode), self._remote_path(relpath), str(e))
460
431
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
461
432
# to copy something to another machine. And you may be able
603
557
return self.lock_read(relpath)
560
class FtpServer(Server):
561
"""Common code for FTP server facilities."""
565
self._ftp_server = None
567
self._async_thread = None
572
"""Calculate an ftp url to this server."""
573
return 'ftp://foo:bar@localhost:%d/' % (self._port)
575
# def get_bogus_url(self):
576
# """Return a URL which cannot be connected to."""
577
# return 'ftp://127.0.0.1:1'
579
def log(self, message):
580
"""This is used by medusa.ftp_server to log connections, etc."""
581
self.logs.append(message)
583
def setUp(self, vfs_server=None):
585
raise RuntimeError('Must have medusa to run the FtpServer')
587
assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
588
"FtpServer currently assumes local transport, got %s" % vfs_server
590
self._root = os.getcwdu()
591
self._ftp_server = _ftp_server(
592
authorizer=_test_authorizer(root=self._root),
594
port=0, # bind to a random port
596
logger_object=self # Use FtpServer.log() for messages
598
self._port = self._ftp_server.getsockname()[1]
599
# Don't let it loop forever, or handle an infinite number of requests.
600
# In this case it will run for 1000s, or 10000 requests
601
self._async_thread = threading.Thread(
602
target=FtpServer._asyncore_loop_ignore_EBADF,
603
kwargs={'timeout':0.1, 'count':10000})
604
self._async_thread.setDaemon(True)
605
self._async_thread.start()
608
"""See bzrlib.transport.Server.tearDown."""
609
self._ftp_server.close()
611
self._async_thread.join()
614
def _asyncore_loop_ignore_EBADF(*args, **kwargs):
615
"""Ignore EBADF during server shutdown.
617
We close the socket to get the server to shutdown, but this causes
618
select.select() to raise EBADF.
621
asyncore.loop(*args, **kwargs)
622
# FIXME: If we reach that point, we should raise an exception
623
# explaining that the 'count' parameter in setUp is too low or
624
# testers may wonder why their test just sits there waiting for a
625
# server that is already dead. Note that if the tester waits too
626
# long under pdb the server will also die.
627
except select.error, e:
628
if e.args[0] != errno.EBADF:
634
_test_authorizer = None
638
global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
641
import medusa.filesys
642
import medusa.ftp_server
648
class test_authorizer(object):
649
"""A custom Authorizer object for running the test suite.
651
The reason we cannot use dummy_authorizer, is because it sets the
652
channel to readonly, which we don't always want to do.
655
def __init__(self, root):
657
# If secured_user is set secured_password will be checked
658
self.secured_user = None
659
self.secured_password = None
661
def authorize(self, channel, username, password):
662
"""Return (success, reply_string, filesystem)"""
664
return 0, 'No Medusa.', None
666
channel.persona = -1, -1
667
if username == 'anonymous':
668
channel.read_only = 1
670
channel.read_only = 0
672
# Check secured_user if set
673
if (self.secured_user is not None
674
and username == self.secured_user
675
and password != self.secured_password):
676
return 0, 'Password invalid.', None
678
return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
681
class ftp_channel(medusa.ftp_server.ftp_channel):
682
"""Customized ftp channel"""
684
def log(self, message):
685
"""Redirect logging requests."""
686
mutter('_ftp_channel: %s', message)
688
def log_info(self, message, type='info'):
689
"""Redirect logging requests."""
690
mutter('_ftp_channel %s: %s', type, message)
692
def cmd_rnfr(self, line):
693
"""Prepare for renaming a file."""
694
self._renaming = line[1]
695
self.respond('350 Ready for RNTO')
696
# TODO: jam 20060516 in testing, the ftp server seems to
697
# check that the file already exists, or it sends
698
# 550 RNFR command failed
700
def cmd_rnto(self, line):
701
"""Rename a file based on the target given.
703
rnto must be called after calling rnfr.
705
if not self._renaming:
706
self.respond('503 RNFR required first.')
707
pfrom = self.filesystem.translate(self._renaming)
708
self._renaming = None
709
pto = self.filesystem.translate(line[1])
710
if os.path.exists(pto):
711
self.respond('550 RNTO failed: file exists')
714
os.rename(pfrom, pto)
715
except (IOError, OSError), e:
716
# TODO: jam 20060516 return custom responses based on
717
# why the command failed
718
# (bialix 20070418) str(e) on Python 2.5 @ Windows
719
# sometimes don't provide expected error message;
720
# so we obtain such message via os.strerror()
721
self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
723
self.respond('550 RNTO failed')
724
# For a test server, we will go ahead and just die
727
self.respond('250 Rename successful.')
729
def cmd_size(self, line):
730
"""Return the size of a file
732
This is overloaded to help the test suite determine if the
733
target is a directory.
736
if not self.filesystem.isfile(filename):
737
if self.filesystem.isdir(filename):
738
self.respond('550 "%s" is a directory' % (filename,))
740
self.respond('550 "%s" is not a file' % (filename,))
742
self.respond('213 %d'
743
% (self.filesystem.stat(filename)[stat.ST_SIZE]),)
745
def cmd_mkd(self, line):
746
"""Create a directory.
748
Overloaded because default implementation does not distinguish
749
*why* it cannot make a directory.
752
self.command_not_understood(''.join(line))
756
self.filesystem.mkdir (path)
757
self.respond ('257 MKD command successful.')
758
except (IOError, OSError), e:
759
# (bialix 20070418) str(e) on Python 2.5 @ Windows
760
# sometimes don't provide expected error message;
761
# so we obtain such message via os.strerror()
762
self.respond ('550 error creating directory: %s' %
763
os.strerror(e.errno))
765
self.respond ('550 error creating directory.')
768
class ftp_server(medusa.ftp_server.ftp_server):
769
"""Customize the behavior of the Medusa ftp_server.
771
There are a few warts on the ftp_server, based on how it expects
775
ftp_channel_class = ftp_channel
777
def __init__(self, *args, **kwargs):
778
mutter('Initializing _ftp_server: %r, %r', args, kwargs)
779
medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
781
def log(self, message):
782
"""Redirect logging requests."""
783
mutter('_ftp_server: %s', message)
785
def log_info(self, message, type='info'):
786
"""Override the asyncore.log_info so we don't stipple the screen."""
787
mutter('_ftp_server %s: %s', type, message)
789
_test_authorizer = test_authorizer
790
_ftp_channel = ftp_channel
791
_ftp_server = ftp_server
606
796
def get_test_permutations():
607
797
"""Return the permutations to be used in testing."""
608
from bzrlib.tests import ftp_server
609
return [(FtpTransport, ftp_server.FTPTestServer)]
798
if not _setup_medusa():
799
warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
802
return [(FtpTransport, FtpServer)]