119
115
:return: The created connection and its associated credentials.
121
The input credentials are only the password as it may have been
122
entered interactively by the user and may be different from the one
123
provided in base url at transport creation time. The returned
124
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.
126
121
if credentials is None:
127
user, password = self._user, self._password
122
password = self._password
129
user, password = credentials
124
password = credentials
131
auth = config.AuthenticationConfig()
133
user = auth.get_user('ftp', self._host, port=self._port,
134
default=getpass.getuser())
135
126
mutter("Constructing FTP instance against %r" %
136
((self._host, self._port, user, '********',
127
((self._host, self._port, self._user, '********',
137
128
self.is_active),))
139
connection = self.connection_class()
130
connection = ftplib.FTP()
140
131
connection.connect(host=self._host, port=self._port)
141
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)
142
138
connection.set_pasv(not self.is_active)
143
# binary mode is the default
144
connection.voidcmd('TYPE I')
145
except socket.error, e:
146
raise errors.SocketConnectionError(self._host, self._port,
147
msg='Unable to connect to',
149
139
except ftplib.error_perm, e:
150
140
raise errors.TransportError(msg="Error setting up connection:"
151
141
" %s" % str(e), orig_error=e)
152
return connection, (user, password)
154
def _login(self, connection, auth, user, password):
155
# '' is a valid password
156
if user and user != 'anonymous' and password is None:
157
password = auth.get_password('ftp', self._host,
158
user, port=self._port)
159
connection.login(user=user, passwd=password)
142
return connection, password
161
144
def _reconnect(self):
162
145
"""Create a new connection with the previously used credentials"""
187
167
or 'no such dir' in s
188
168
or 'could not create file' in s # vsftpd
189
169
or 'file doesn\'t exist' in s
190
or 'rnfr command failed.' in s # vsftpd RNFR reply if file not found
191
170
or 'file/directory not found' in s # filezilla server
192
# Microsoft FTP-Service RNFR reply if file not found
193
or (s.startswith('550 ') and 'unable to rename to' in extra)
195
172
raise errors.NoSuchFile(path, extra=extra)
196
elif ('file exists' in s):
173
if ('file exists' in s):
197
174
raise errors.FileExists(path, extra=extra)
198
elif ('not a directory' in s):
175
if ('not a directory' in s):
199
176
raise errors.PathError(path, extra=extra)
200
elif 'directory not empty' in s:
201
raise errors.DirectoryNotEmpty(path, extra=extra)
203
178
mutter('unable to understand error for path: %s: %s', path, err)
206
181
raise unknown_exc(path, extra=extra)
207
# TODO: jam 20060516 Consider re-raising the error wrapped in
182
# TODO: jam 20060516 Consider re-raising the error wrapped in
208
183
# something like TransportError, but this loses the traceback
209
184
# Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
210
185
# to handle. Consider doing something like that here.
211
186
#raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
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)
214
200
def has(self, relpath):
215
201
"""Does the target location exist?"""
216
202
# FIXME jam 20060516 We *do* ask about directories in the test suite
411
391
abspath = self._remote_path(relpath)
412
392
mutter("FTP appe (try %d) to %s", retries, abspath)
413
393
ftp = self._get_FTP()
394
ftp.voidcmd("TYPE I")
414
395
cmd = "APPE %s" % abspath
415
396
conn = ftp.transfercmd(cmd)
416
397
conn.sendall(text)
418
self._setmode(relpath, mode)
400
self._setmode(relpath, mode)
420
402
except ftplib.error_perm, e:
421
# Check whether the command is not supported (reply code 502)
422
if str(e).startswith('502 '):
423
warning("FTP server does not support file appending natively. "
424
"Performance may be severely degraded! (%s)", e)
425
self._has_append = False
426
self._fallback_append(relpath, text, mode)
428
self._translate_ftp_error(e, abspath, extra='error appending',
429
unknown_exc=errors.NoSuchFile)
403
self._translate_perm_error(e, abspath, extra='error appending',
404
unknown_exc=errors.NoSuchFile)
430
405
except ftplib.error_temp, e:
431
406
if retries > _number_of_retries:
432
raise errors.TransportError(
433
"FTP temporary error during APPEND %s. Aborting."
434
% abspath, orig_error=e)
407
raise errors.TransportError("FTP temporary error during APPEND %s." \
408
"Aborting." % abspath, orig_error=e)
436
410
warning("FTP temporary error: %s. Retrying.", str(e))
437
411
self._reconnect()
438
412
self._try_append(relpath, text, mode, retries+1)
440
def _fallback_append(self, relpath, text, mode = None):
441
remote = self.get(relpath)
442
remote.seek(0, os.SEEK_END)
445
return self.put_file(relpath, remote, mode)
447
414
def _setmode(self, relpath, mode):
448
415
"""Set permissions on a path.
450
417
Only set permissions if the FTP server supports the 'SITE CHMOD'
455
mutter("FTP site chmod: setting permissions to %s on %s",
456
oct(mode), self._remote_path(relpath))
457
ftp = self._get_FTP()
458
cmd = "SITE CHMOD %s %s" % (oct(mode),
459
self._remote_path(relpath))
461
except ftplib.error_perm, e:
462
# Command probably not available on this server
463
warning("FTP Could not set permissions to %s on %s. %s",
464
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))
466
431
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
467
432
# to copy something to another machine. And you may be able
609
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
612
796
def get_test_permutations():
613
797
"""Return the permutations to be used in testing."""
614
from bzrlib.tests import ftp_server
615
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)]