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 not 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"""
163
credentials = self._get_credentials()
146
credentials = self.get_credentials()
164
147
connection, credentials = self._create_connection(credentials)
165
148
self._set_connection(connection, credentials)
167
def _translate_ftp_error(self, err, path, extra=None,
150
def _translate_perm_error(self, err, path, extra=None,
168
151
unknown_exc=FtpPathError):
169
"""Try to translate an ftplib exception to a bzrlib exception.
152
"""Try to translate an ftplib.error_perm exception.
171
154
:param err: The error to translate into a bzr error
172
155
:param path: The path which had problems
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
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
171
raise errors.NoSuchFile(path, extra=extra)
196
elif ('file exists' in s):
172
if ('file exists' in s):
197
173
raise errors.FileExists(path, extra=extra)
198
elif ('not a directory' in s):
174
if ('not a directory' in s):
199
175
raise errors.PathError(path, extra=extra)
200
elif 'directory not empty' in s:
201
raise errors.DirectoryNotEmpty(path, extra=extra)
203
177
mutter('unable to understand error for path: %s: %s', path, err)
206
180
raise unknown_exc(path, extra=extra)
207
# TODO: jam 20060516 Consider re-raising the error wrapped in
181
# TODO: jam 20060516 Consider re-raising the error wrapped in
208
182
# something like TransportError, but this loses the traceback
209
183
# Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
210
184
# to handle. Consider doing something like that here.
211
185
#raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
188
def _remote_path(self, relpath):
189
# XXX: It seems that ftplib does not handle Unicode paths
190
# at the same time, medusa won't handle utf8 paths So if
191
# we .encode(utf8) here (see ConnectedTransport
192
# implementation), then we get a Server failure. while
193
# if we use str(), we get a UnicodeError, and the test
194
# suite just skips testing UnicodePaths.
195
relative = str(urlutils.unescape(relpath))
196
remote_path = self._combine_paths(self._path, relative)
214
199
def has(self, relpath):
215
200
"""Does the target location exist?"""
216
201
# FIXME jam 20060516 We *do* ask about directories in the test suite
411
371
abspath = self._remote_path(relpath)
412
372
mutter("FTP appe (try %d) to %s", retries, abspath)
413
373
ftp = self._get_FTP()
374
ftp.voidcmd("TYPE I")
414
375
cmd = "APPE %s" % abspath
415
376
conn = ftp.transfercmd(cmd)
416
377
conn.sendall(text)
418
self._setmode(relpath, mode)
380
self._setmode(relpath, mode)
420
382
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)
383
self._translate_perm_error(e, abspath, extra='error appending',
384
unknown_exc=errors.NoSuchFile)
430
385
except ftplib.error_temp, e:
431
386
if retries > _number_of_retries:
432
raise errors.TransportError(
433
"FTP temporary error during APPEND %s. Aborting."
434
% abspath, orig_error=e)
387
raise errors.TransportError("FTP temporary error during APPEND %s." \
388
"Aborting." % abspath, orig_error=e)
436
390
warning("FTP temporary error: %s. Retrying.", str(e))
437
391
self._reconnect()
438
392
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
394
def _setmode(self, relpath, mode):
448
395
"""Set permissions on a path.
450
397
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))
401
mutter("FTP site chmod: setting permissions to %s on %s",
402
str(mode), self._remote_path(relpath))
403
ftp = self._get_FTP()
404
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
406
except ftplib.error_perm, e:
407
# Command probably not available on this server
408
warning("FTP Could not set permissions to %s on %s. %s",
409
str(mode), self._remote_path(relpath), str(e))
466
411
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
467
412
# to copy something to another machine. And you may be able
609
537
return self.lock_read(relpath)
540
class FtpServer(Server):
541
"""Common code for FTP server facilities."""
545
self._ftp_server = None
547
self._async_thread = None
552
"""Calculate an ftp url to this server."""
553
return 'ftp://foo:bar@localhost:%d/' % (self._port)
555
# def get_bogus_url(self):
556
# """Return a URL which cannot be connected to."""
557
# return 'ftp://127.0.0.1:1'
559
def log(self, message):
560
"""This is used by medusa.ftp_server to log connections, etc."""
561
self.logs.append(message)
563
def setUp(self, vfs_server=None):
565
raise RuntimeError('Must have medusa to run the FtpServer')
567
assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
568
"FtpServer currently assumes local transport, got %s" % vfs_server
570
self._root = os.getcwdu()
571
self._ftp_server = _ftp_server(
572
authorizer=_test_authorizer(root=self._root),
574
port=0, # bind to a random port
576
logger_object=self # Use FtpServer.log() for messages
578
self._port = self._ftp_server.getsockname()[1]
579
# Don't let it loop forever, or handle an infinite number of requests.
580
# In this case it will run for 1000s, or 10000 requests
581
self._async_thread = threading.Thread(
582
target=FtpServer._asyncore_loop_ignore_EBADF,
583
kwargs={'timeout':0.1, 'count':10000})
584
self._async_thread.setDaemon(True)
585
self._async_thread.start()
588
"""See bzrlib.transport.Server.tearDown."""
589
# have asyncore release the channel
590
self._ftp_server.del_channel()
592
self._async_thread.join()
595
def _asyncore_loop_ignore_EBADF(*args, **kwargs):
596
"""Ignore EBADF during server shutdown.
598
We close the socket to get the server to shutdown, but this causes
599
select.select() to raise EBADF.
602
asyncore.loop(*args, **kwargs)
603
# FIXME: If we reach that point, we should raise an exception
604
# explaining that the 'count' parameter in setUp is too low or
605
# testers may wonder why their test just sits there waiting for a
606
# server that is already dead. Note that if the tester waits too
607
# long under pdb the server will also die.
608
except select.error, e:
609
if e.args[0] != errno.EBADF:
615
_test_authorizer = None
619
global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
622
import medusa.filesys
623
import medusa.ftp_server
629
class test_authorizer(object):
630
"""A custom Authorizer object for running the test suite.
632
The reason we cannot use dummy_authorizer, is because it sets the
633
channel to readonly, which we don't always want to do.
636
def __init__(self, root):
639
def authorize(self, channel, username, password):
640
"""Return (success, reply_string, filesystem)"""
642
return 0, 'No Medusa.', None
644
channel.persona = -1, -1
645
if username == 'anonymous':
646
channel.read_only = 1
648
channel.read_only = 0
650
return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
653
class ftp_channel(medusa.ftp_server.ftp_channel):
654
"""Customized ftp channel"""
656
def log(self, message):
657
"""Redirect logging requests."""
658
mutter('_ftp_channel: %s', message)
660
def log_info(self, message, type='info'):
661
"""Redirect logging requests."""
662
mutter('_ftp_channel %s: %s', type, message)
664
def cmd_rnfr(self, line):
665
"""Prepare for renaming a file."""
666
self._renaming = line[1]
667
self.respond('350 Ready for RNTO')
668
# TODO: jam 20060516 in testing, the ftp server seems to
669
# check that the file already exists, or it sends
670
# 550 RNFR command failed
672
def cmd_rnto(self, line):
673
"""Rename a file based on the target given.
675
rnto must be called after calling rnfr.
677
if not self._renaming:
678
self.respond('503 RNFR required first.')
679
pfrom = self.filesystem.translate(self._renaming)
680
self._renaming = None
681
pto = self.filesystem.translate(line[1])
682
if os.path.exists(pto):
683
self.respond('550 RNTO failed: file exists')
686
os.rename(pfrom, pto)
687
except (IOError, OSError), e:
688
# TODO: jam 20060516 return custom responses based on
689
# why the command failed
690
# (bialix 20070418) str(e) on Python 2.5 @ Windows
691
# sometimes don't provide expected error message;
692
# so we obtain such message via os.strerror()
693
self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
695
self.respond('550 RNTO failed')
696
# For a test server, we will go ahead and just die
699
self.respond('250 Rename successful.')
701
def cmd_size(self, line):
702
"""Return the size of a file
704
This is overloaded to help the test suite determine if the
705
target is a directory.
708
if not self.filesystem.isfile(filename):
709
if self.filesystem.isdir(filename):
710
self.respond('550 "%s" is a directory' % (filename,))
712
self.respond('550 "%s" is not a file' % (filename,))
714
self.respond('213 %d'
715
% (self.filesystem.stat(filename)[stat.ST_SIZE]),)
717
def cmd_mkd(self, line):
718
"""Create a directory.
720
Overloaded because default implementation does not distinguish
721
*why* it cannot make a directory.
724
self.command_not_understood(''.join(line))
728
self.filesystem.mkdir (path)
729
self.respond ('257 MKD command successful.')
730
except (IOError, OSError), e:
731
# (bialix 20070418) str(e) on Python 2.5 @ Windows
732
# sometimes don't provide expected error message;
733
# so we obtain such message via os.strerror()
734
self.respond ('550 error creating directory: %s' %
735
os.strerror(e.errno))
737
self.respond ('550 error creating directory.')
740
class ftp_server(medusa.ftp_server.ftp_server):
741
"""Customize the behavior of the Medusa ftp_server.
743
There are a few warts on the ftp_server, based on how it expects
747
ftp_channel_class = ftp_channel
749
def __init__(self, *args, **kwargs):
750
mutter('Initializing _ftp_server: %r, %r', args, kwargs)
751
medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
753
def log(self, message):
754
"""Redirect logging requests."""
755
mutter('_ftp_server: %s', message)
757
def log_info(self, message, type='info'):
758
"""Override the asyncore.log_info so we don't stipple the screen."""
759
mutter('_ftp_server %s: %s', type, message)
761
_test_authorizer = test_authorizer
762
_ftp_channel = ftp_channel
763
_ftp_server = ftp_server
612
768
def get_test_permutations():
613
769
"""Return the permutations to be used in testing."""
614
from bzrlib.tests import ftp_server
615
return [(FtpTransport, ftp_server.FTPTestServer)]
770
if not _setup_medusa():
771
warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
774
return [(FtpTransport, FtpServer)]