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 _remote_path(self, relpath):
187
# XXX: It seems that ftplib does not handle Unicode paths
188
# at the same time, medusa won't handle utf8 paths So if
189
# we .encode(utf8) here (see ConnectedTransport
190
# implementation), then we get a Server failure. while
191
# if we use str(), we get a UnicodeError, and the test
192
# suite just skips testing UnicodePaths.
193
relative = str(urlutils.unescape(relpath))
194
remote_path = self._combine_paths(self._path, relative)
214
197
def has(self, relpath):
215
198
"""Does the target location exist?"""
216
199
# FIXME jam 20060516 We *do* ask about directories in the test suite
411
354
abspath = self._remote_path(relpath)
412
355
mutter("FTP appe (try %d) to %s", retries, abspath)
413
356
ftp = self._get_FTP()
357
ftp.voidcmd("TYPE I")
414
358
cmd = "APPE %s" % abspath
415
359
conn = ftp.transfercmd(cmd)
416
360
conn.sendall(text)
418
self._setmode(relpath, mode)
363
self._setmode(relpath, mode)
420
365
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)
366
self._translate_perm_error(e, abspath, extra='error appending',
367
unknown_exc=errors.NoSuchFile)
430
368
except ftplib.error_temp, e:
431
369
if retries > _number_of_retries:
432
raise errors.TransportError(
433
"FTP temporary error during APPEND %s. Aborting."
434
% abspath, orig_error=e)
370
raise errors.TransportError("FTP temporary error during APPEND %s." \
371
"Aborting." % abspath, orig_error=e)
436
373
warning("FTP temporary error: %s. Retrying.", str(e))
437
374
self._reconnect()
438
375
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
377
def _setmode(self, relpath, mode):
448
378
"""Set permissions on a path.
450
380
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))
384
mutter("FTP site chmod: setting permissions to %s on %s",
385
str(mode), self._remote_path(relpath))
386
ftp = self._get_FTP()
387
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
389
except ftplib.error_perm, e:
390
# Command probably not available on this server
391
warning("FTP Could not set permissions to %s on %s. %s",
392
str(mode), self._remote_path(relpath), str(e))
466
394
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
467
395
# to copy something to another machine. And you may be able
609
520
return self.lock_read(relpath)
523
class FtpServer(Server):
524
"""Common code for FTP server facilities."""
528
self._ftp_server = None
530
self._async_thread = None
535
"""Calculate an ftp url to this server."""
536
return 'ftp://foo:bar@localhost:%d/' % (self._port)
538
# def get_bogus_url(self):
539
# """Return a URL which cannot be connected to."""
540
# return 'ftp://127.0.0.1:1'
542
def log(self, message):
543
"""This is used by medusa.ftp_server to log connections, etc."""
544
self.logs.append(message)
546
def setUp(self, vfs_server=None):
548
raise RuntimeError('Must have medusa to run the FtpServer')
550
assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
551
"FtpServer currently assumes local transport, got %s" % vfs_server
553
self._root = os.getcwdu()
554
self._ftp_server = _ftp_server(
555
authorizer=_test_authorizer(root=self._root),
557
port=0, # bind to a random port
559
logger_object=self # Use FtpServer.log() for messages
561
self._port = self._ftp_server.getsockname()[1]
562
# Don't let it loop forever, or handle an infinite number of requests.
563
# In this case it will run for 1000s, or 10000 requests
564
self._async_thread = threading.Thread(
565
target=FtpServer._asyncore_loop_ignore_EBADF,
566
kwargs={'timeout':0.1, 'count':10000})
567
self._async_thread.setDaemon(True)
568
self._async_thread.start()
571
"""See bzrlib.transport.Server.tearDown."""
572
# have asyncore release the channel
573
self._ftp_server.del_channel()
575
self._async_thread.join()
578
def _asyncore_loop_ignore_EBADF(*args, **kwargs):
579
"""Ignore EBADF during server shutdown.
581
We close the socket to get the server to shutdown, but this causes
582
select.select() to raise EBADF.
585
asyncore.loop(*args, **kwargs)
586
# FIXME: If we reach that point, we should raise an exception
587
# explaining that the 'count' parameter in setUp is too low or
588
# testers may wonder why their test just sits there waiting for a
589
# server that is already dead. Note that if the tester waits too
590
# long under pdb the server will also die.
591
except select.error, e:
592
if e.args[0] != errno.EBADF:
598
_test_authorizer = None
602
global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
605
import medusa.filesys
606
import medusa.ftp_server
612
class test_authorizer(object):
613
"""A custom Authorizer object for running the test suite.
615
The reason we cannot use dummy_authorizer, is because it sets the
616
channel to readonly, which we don't always want to do.
619
def __init__(self, root):
622
def authorize(self, channel, username, password):
623
"""Return (success, reply_string, filesystem)"""
625
return 0, 'No Medusa.', None
627
channel.persona = -1, -1
628
if username == 'anonymous':
629
channel.read_only = 1
631
channel.read_only = 0
633
return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
636
class ftp_channel(medusa.ftp_server.ftp_channel):
637
"""Customized ftp channel"""
639
def log(self, message):
640
"""Redirect logging requests."""
641
mutter('_ftp_channel: %s', message)
643
def log_info(self, message, type='info'):
644
"""Redirect logging requests."""
645
mutter('_ftp_channel %s: %s', type, message)
647
def cmd_rnfr(self, line):
648
"""Prepare for renaming a file."""
649
self._renaming = line[1]
650
self.respond('350 Ready for RNTO')
651
# TODO: jam 20060516 in testing, the ftp server seems to
652
# check that the file already exists, or it sends
653
# 550 RNFR command failed
655
def cmd_rnto(self, line):
656
"""Rename a file based on the target given.
658
rnto must be called after calling rnfr.
660
if not self._renaming:
661
self.respond('503 RNFR required first.')
662
pfrom = self.filesystem.translate(self._renaming)
663
self._renaming = None
664
pto = self.filesystem.translate(line[1])
665
if os.path.exists(pto):
666
self.respond('550 RNTO failed: file exists')
669
os.rename(pfrom, pto)
670
except (IOError, OSError), e:
671
# TODO: jam 20060516 return custom responses based on
672
# why the command failed
673
# (bialix 20070418) str(e) on Python 2.5 @ Windows
674
# sometimes don't provide expected error message;
675
# so we obtain such message via os.strerror()
676
self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
678
self.respond('550 RNTO failed')
679
# For a test server, we will go ahead and just die
682
self.respond('250 Rename successful.')
684
def cmd_size(self, line):
685
"""Return the size of a file
687
This is overloaded to help the test suite determine if the
688
target is a directory.
691
if not self.filesystem.isfile(filename):
692
if self.filesystem.isdir(filename):
693
self.respond('550 "%s" is a directory' % (filename,))
695
self.respond('550 "%s" is not a file' % (filename,))
697
self.respond('213 %d'
698
% (self.filesystem.stat(filename)[stat.ST_SIZE]),)
700
def cmd_mkd(self, line):
701
"""Create a directory.
703
Overloaded because default implementation does not distinguish
704
*why* it cannot make a directory.
707
self.command_not_understood(''.join(line))
711
self.filesystem.mkdir (path)
712
self.respond ('257 MKD command successful.')
713
except (IOError, OSError), e:
714
# (bialix 20070418) str(e) on Python 2.5 @ Windows
715
# sometimes don't provide expected error message;
716
# so we obtain such message via os.strerror()
717
self.respond ('550 error creating directory: %s' %
718
os.strerror(e.errno))
720
self.respond ('550 error creating directory.')
723
class ftp_server(medusa.ftp_server.ftp_server):
724
"""Customize the behavior of the Medusa ftp_server.
726
There are a few warts on the ftp_server, based on how it expects
730
ftp_channel_class = ftp_channel
732
def __init__(self, *args, **kwargs):
733
mutter('Initializing _ftp_server: %r, %r', args, kwargs)
734
medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
736
def log(self, message):
737
"""Redirect logging requests."""
738
mutter('_ftp_server: %s', message)
740
def log_info(self, message, type='info'):
741
"""Override the asyncore.log_info so we don't stipple the screen."""
742
mutter('_ftp_server %s: %s', type, message)
744
_test_authorizer = test_authorizer
745
_ftp_channel = ftp_channel
746
_ftp_server = ftp_server
612
751
def get_test_permutations():
613
752
"""Return the permutations to be used in testing."""
614
from bzrlib.tests import ftp_server
615
return [(FtpTransport, ftp_server.FTPTestServer)]
753
if not _setup_medusa():
754
warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
757
return [(FtpTransport, FtpServer)]