119
113
: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.
115
The credentials are only the password as it may have been entered
116
interactively by the user and may be different from the one provided
117
in base url at transport creation time.
126
119
if credentials is None:
127
user, password = self._user, self._password
120
password = self._password
129
user, password = credentials
122
password = credentials
131
auth = config.AuthenticationConfig()
133
user = auth.get_user('ftp', self._host, port=self._port,
134
default=getpass.getuser())
135
124
mutter("Constructing FTP instance against %r" %
136
((self._host, self._port, user, '********',
125
((self._host, self._port, self._user, '********',
137
126
self.is_active),))
139
connection = self.connection_class()
128
connection = ftplib.FTP()
140
129
connection.connect(host=self._host, port=self._port)
141
self._login(connection, auth, user, password)
130
if self._user and self._user != 'anonymous' and \
131
password is not None: # '' is a valid password
132
get_password = bzrlib.ui.ui_factory.get_password
133
password = get_password(prompt='FTP %(user)s@%(host)s password',
134
user=self._user, host=self._host)
135
connection.login(user=self._user, passwd=password)
142
136
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
137
except ftplib.error_perm, e:
150
138
raise errors.TransportError(msg="Error setting up connection:"
151
139
" %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)
140
return connection, password
161
142
def _reconnect(self):
162
143
"""Create a new connection with the previously used credentials"""
163
credentials = self._get_credentials()
144
credentials = self.get_credentials()
164
145
connection, credentials = self._create_connection(credentials)
165
146
self._set_connection(connection, credentials)
167
def _translate_ftp_error(self, err, path, extra=None,
148
def _translate_perm_error(self, err, path, extra=None,
168
149
unknown_exc=FtpPathError):
169
"""Try to translate an ftplib exception to a bzrlib exception.
150
"""Try to translate an ftplib.error_perm exception.
171
152
:param err: The error to translate into a bzr error
172
153
:param path: The path which had problems
187
165
or 'no such dir' in s
188
166
or 'could not create file' in s # vsftpd
189
167
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
169
raise errors.NoSuchFile(path, extra=extra)
196
elif ('file exists' in s):
170
if ('file exists' in s):
197
171
raise errors.FileExists(path, extra=extra)
198
elif ('not a directory' in s):
172
if ('not a directory' in s):
199
173
raise errors.PathError(path, extra=extra)
200
elif 'directory not empty' in s:
201
raise errors.DirectoryNotEmpty(path, extra=extra)
203
175
mutter('unable to understand error for path: %s: %s', path, err)
206
178
raise unknown_exc(path, extra=extra)
207
# TODO: jam 20060516 Consider re-raising the error wrapped in
179
# TODO: jam 20060516 Consider re-raising the error wrapped in
208
180
# something like TransportError, but this loses the traceback
209
181
# Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
210
182
# to handle. Consider doing something like that here.
211
183
#raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
186
def should_cache(self):
187
"""Return True if the data pulled across should be cached locally.
191
def _remote_path(self, relpath):
192
# XXX: It seems that ftplib does not handle Unicode paths
193
# at the same time, medusa won't handle utf8 paths So if
194
# we .encode(utf8) here (see ConnectedTransport
195
# implementation), then we get a Server failure. while
196
# if we use str(), we get a UnicodeError, and the test
197
# suite just skips testing UnicodePaths.
198
relative = str(urlutils.unescape(relpath))
199
remote_path = self._combine_paths(self._path, relative)
214
202
def has(self, relpath):
215
203
"""Does the target location exist?"""
216
204
# FIXME jam 20060516 We *do* ask about directories in the test suite
411
359
abspath = self._remote_path(relpath)
412
360
mutter("FTP appe (try %d) to %s", retries, abspath)
413
361
ftp = self._get_FTP()
362
ftp.voidcmd("TYPE I")
414
363
cmd = "APPE %s" % abspath
415
364
conn = ftp.transfercmd(cmd)
416
365
conn.sendall(text)
418
self._setmode(relpath, mode)
368
self._setmode(relpath, mode)
420
370
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)
371
self._translate_perm_error(e, abspath, extra='error appending',
372
unknown_exc=errors.NoSuchFile)
430
373
except ftplib.error_temp, e:
431
374
if retries > _number_of_retries:
432
raise errors.TransportError(
433
"FTP temporary error during APPEND %s. Aborting."
434
% abspath, orig_error=e)
375
raise errors.TransportError("FTP temporary error during APPEND %s." \
376
"Aborting." % abspath, orig_error=e)
436
378
warning("FTP temporary error: %s. Retrying.", str(e))
437
379
self._reconnect()
438
380
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
382
def _setmode(self, relpath, mode):
448
383
"""Set permissions on a path.
450
385
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))
389
mutter("FTP site chmod: setting permissions to %s on %s",
390
str(mode), self._remote_path(relpath))
391
ftp = self._get_FTP()
392
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
394
except ftplib.error_perm, e:
395
# Command probably not available on this server
396
warning("FTP Could not set permissions to %s on %s. %s",
397
str(mode), self._remote_path(relpath), str(e))
466
399
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
467
400
# to copy something to another machine. And you may be able
609
525
return self.lock_read(relpath)
528
class FtpServer(Server):
529
"""Common code for FTP server facilities."""
533
self._ftp_server = None
535
self._async_thread = None
540
"""Calculate an ftp url to this server."""
541
return 'ftp://foo:bar@localhost:%d/' % (self._port)
543
# def get_bogus_url(self):
544
# """Return a URL which cannot be connected to."""
545
# return 'ftp://127.0.0.1:1'
547
def log(self, message):
548
"""This is used by medusa.ftp_server to log connections, etc."""
549
self.logs.append(message)
551
def setUp(self, vfs_server=None):
553
raise RuntimeError('Must have medusa to run the FtpServer')
555
assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
556
"FtpServer currently assumes local transport, got %s" % vfs_server
558
self._root = os.getcwdu()
559
self._ftp_server = _ftp_server(
560
authorizer=_test_authorizer(root=self._root),
562
port=0, # bind to a random port
564
logger_object=self # Use FtpServer.log() for messages
566
self._port = self._ftp_server.getsockname()[1]
567
# Don't let it loop forever, or handle an infinite number of requests.
568
# In this case it will run for 1000s, or 10000 requests
569
self._async_thread = threading.Thread(
570
target=FtpServer._asyncore_loop_ignore_EBADF,
571
kwargs={'timeout':0.1, 'count':10000})
572
self._async_thread.setDaemon(True)
573
self._async_thread.start()
576
"""See bzrlib.transport.Server.tearDown."""
577
# have asyncore release the channel
578
self._ftp_server.del_channel()
580
self._async_thread.join()
583
def _asyncore_loop_ignore_EBADF(*args, **kwargs):
584
"""Ignore EBADF during server shutdown.
586
We close the socket to get the server to shutdown, but this causes
587
select.select() to raise EBADF.
590
asyncore.loop(*args, **kwargs)
591
# FIXME: If we reach that point, we should raise an exception
592
# explaining that the 'count' parameter in setUp is too low or
593
# testers may wonder why their test just sits there waiting for a
594
# server that is already dead. Note that if the tester waits too
595
# long under pdb the server will also die.
596
except select.error, e:
597
if e.args[0] != errno.EBADF:
603
_test_authorizer = None
607
global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
610
import medusa.filesys
611
import medusa.ftp_server
617
class test_authorizer(object):
618
"""A custom Authorizer object for running the test suite.
620
The reason we cannot use dummy_authorizer, is because it sets the
621
channel to readonly, which we don't always want to do.
624
def __init__(self, root):
627
def authorize(self, channel, username, password):
628
"""Return (success, reply_string, filesystem)"""
630
return 0, 'No Medusa.', None
632
channel.persona = -1, -1
633
if username == 'anonymous':
634
channel.read_only = 1
636
channel.read_only = 0
638
return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
641
class ftp_channel(medusa.ftp_server.ftp_channel):
642
"""Customized ftp channel"""
644
def log(self, message):
645
"""Redirect logging requests."""
646
mutter('_ftp_channel: %s', message)
648
def log_info(self, message, type='info'):
649
"""Redirect logging requests."""
650
mutter('_ftp_channel %s: %s', type, message)
652
def cmd_rnfr(self, line):
653
"""Prepare for renaming a file."""
654
self._renaming = line[1]
655
self.respond('350 Ready for RNTO')
656
# TODO: jam 20060516 in testing, the ftp server seems to
657
# check that the file already exists, or it sends
658
# 550 RNFR command failed
660
def cmd_rnto(self, line):
661
"""Rename a file based on the target given.
663
rnto must be called after calling rnfr.
665
if not self._renaming:
666
self.respond('503 RNFR required first.')
667
pfrom = self.filesystem.translate(self._renaming)
668
self._renaming = None
669
pto = self.filesystem.translate(line[1])
670
if os.path.exists(pto):
671
self.respond('550 RNTO failed: file exists')
674
os.rename(pfrom, pto)
675
except (IOError, OSError), e:
676
# TODO: jam 20060516 return custom responses based on
677
# why the command failed
678
# (bialix 20070418) str(e) on Python 2.5 @ Windows
679
# sometimes don't provide expected error message;
680
# so we obtain such message via os.strerror()
681
self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
683
self.respond('550 RNTO failed')
684
# For a test server, we will go ahead and just die
687
self.respond('250 Rename successful.')
689
def cmd_size(self, line):
690
"""Return the size of a file
692
This is overloaded to help the test suite determine if the
693
target is a directory.
696
if not self.filesystem.isfile(filename):
697
if self.filesystem.isdir(filename):
698
self.respond('550 "%s" is a directory' % (filename,))
700
self.respond('550 "%s" is not a file' % (filename,))
702
self.respond('213 %d'
703
% (self.filesystem.stat(filename)[stat.ST_SIZE]),)
705
def cmd_mkd(self, line):
706
"""Create a directory.
708
Overloaded because default implementation does not distinguish
709
*why* it cannot make a directory.
712
self.command_not_understood(''.join(line))
716
self.filesystem.mkdir (path)
717
self.respond ('257 MKD command successful.')
718
except (IOError, OSError), e:
719
# (bialix 20070418) str(e) on Python 2.5 @ Windows
720
# sometimes don't provide expected error message;
721
# so we obtain such message via os.strerror()
722
self.respond ('550 error creating directory: %s' %
723
os.strerror(e.errno))
725
self.respond ('550 error creating directory.')
728
class ftp_server(medusa.ftp_server.ftp_server):
729
"""Customize the behavior of the Medusa ftp_server.
731
There are a few warts on the ftp_server, based on how it expects
735
ftp_channel_class = ftp_channel
737
def __init__(self, *args, **kwargs):
738
mutter('Initializing _ftp_server: %r, %r', args, kwargs)
739
medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
741
def log(self, message):
742
"""Redirect logging requests."""
743
mutter('_ftp_server: %s', message)
745
def log_info(self, message, type='info'):
746
"""Override the asyncore.log_info so we don't stipple the screen."""
747
mutter('_ftp_server %s: %s', type, message)
749
_test_authorizer = test_authorizer
750
_ftp_channel = ftp_channel
751
_ftp_server = ftp_server
612
756
def get_test_permutations():
613
757
"""Return the permutations to be used in testing."""
614
from bzrlib.tests import ftp_server
615
return [(FtpTransport, ftp_server.FTPTestServer)]
758
if not _setup_medusa():
759
warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
762
return [(FtpTransport, FtpServer)]