75
76
_number_of_retries = 2
76
77
_sleep_between_retries = 5
78
# FIXME: there are inconsistencies in the way temporary errors are
79
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
80
# be taken to analyze the implications for write operations (read operations
81
# are safe to retry). Overall even some read operations are never
82
# retried. --vila 20070720 (Bug #127164)
83
79
class FtpTransport(ConnectedTransport):
84
80
"""This is the transport agent for ftp:// access."""
86
def __init__(self, base, _from_transport=None):
82
def __init__(self, base, from_transport=None):
87
83
"""Set the base path where files will be stored."""
88
if not (base.startswith('ftp://') or base.startswith('aftp://')):
89
raise ValueError(base)
90
super(FtpTransport, self).__init__(base,
91
_from_transport=_from_transport)
84
assert base.startswith('ftp://') or base.startswith('aftp://')
85
super(FtpTransport, self).__init__(base, from_transport)
92
86
self._unqualified_scheme = 'ftp'
93
87
if self._scheme == 'aftp':
94
88
self.is_active = True
96
90
self.is_active = False
98
# Most modern FTP servers support the APPE command. If ours doesn't, we
99
# (re)set this flag accordingly later.
100
self._has_append = True
102
92
def _get_FTP(self):
103
93
"""Return the ftplib.FTP instance for this object."""
104
94
# Ensures that a connection is established
119
107
: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.
109
The credentials are only the password as it may have been entered
110
interactively by the user and may be different from the one provided
111
in base url at transport creation time.
126
113
if credentials is None:
127
user, password = self._user, self._password
114
password = self._password
129
user, password = credentials
116
password = credentials
131
auth = config.AuthenticationConfig()
133
user = auth.get_user('ftp', self._host, port=self._port,
134
default=getpass.getuser())
135
118
mutter("Constructing FTP instance against %r" %
136
((self._host, self._port, user, '********',
119
((self._host, self._port, self._user, '********',
137
120
self.is_active),))
139
connection = self.connection_class()
122
connection = ftplib.FTP()
140
123
connection.connect(host=self._host, port=self._port)
141
self._login(connection, auth, user, password)
124
if self._user and self._user != 'anonymous' and \
125
password is not None: # '' is a valid password
126
get_password = bzrlib.ui.ui_factory.get_password
127
password = get_password(prompt='FTP %(user)s@%(host)s password',
128
user=self._user, host=self._host)
129
connection.login(user=self._user, passwd=password)
142
130
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
131
except ftplib.error_perm, e:
150
132
raise errors.TransportError(msg="Error setting up connection:"
151
133
" %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)
134
return connection, password
161
136
def _reconnect(self):
162
137
"""Create a new connection with the previously used credentials"""
163
credentials = self._get_credentials()
138
credentials = self.get_credentials()
164
139
connection, credentials = self._create_connection(credentials)
165
140
self._set_connection(connection, credentials)
167
def _translate_ftp_error(self, err, path, extra=None,
142
def _translate_perm_error(self, err, path, extra=None,
168
143
unknown_exc=FtpPathError):
169
"""Try to translate an ftplib exception to a bzrlib exception.
144
"""Try to translate an ftplib.error_perm exception.
171
146
:param err: The error to translate into a bzr error
172
147
:param path: The path which had problems
186
158
or 'could not open' in s
187
159
or 'no such dir' in s
188
160
or 'could not create file' in s # vsftpd
189
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
162
raise errors.NoSuchFile(path, extra=extra)
196
elif ('file exists' in s):
163
if ('file exists' in s):
197
164
raise errors.FileExists(path, extra=extra)
198
elif ('not a directory' in s):
165
if ('not a directory' in s):
199
166
raise errors.PathError(path, extra=extra)
200
elif 'directory not empty' in s:
201
raise errors.DirectoryNotEmpty(path, extra=extra)
203
168
mutter('unable to understand error for path: %s: %s', path, err)
206
171
raise unknown_exc(path, extra=extra)
207
# TODO: jam 20060516 Consider re-raising the error wrapped in
172
# TODO: jam 20060516 Consider re-raising the error wrapped in
208
173
# something like TransportError, but this loses the traceback
209
174
# Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
210
175
# to handle. Consider doing something like that here.
211
176
#raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
179
def should_cache(self):
180
"""Return True if the data pulled across should be cached locally.
184
def _remote_path(self, relpath):
185
# XXX: It seems that ftplib does not handle Unicode paths
186
# at the same time, medusa won't handle utf8 paths So if
187
# we .encode(utf8) here (see ConnectedTransport
188
# implementation), then we get a Server failure. while
189
# if we use str(), we get a UnicodeError, and the test
190
# suite just skips testing UnicodePaths.
191
relative = str(urlutils.unescape(relpath))
192
remote_path = self._combine_paths(self._path, relative)
214
195
def has(self, relpath):
215
196
"""Does the target location exist?"""
216
197
# FIXME jam 20060516 We *do* ask about directories in the test suite
411
351
abspath = self._remote_path(relpath)
412
352
mutter("FTP appe (try %d) to %s", retries, abspath)
413
353
ftp = self._get_FTP()
354
ftp.voidcmd("TYPE I")
414
355
cmd = "APPE %s" % abspath
415
356
conn = ftp.transfercmd(cmd)
416
357
conn.sendall(text)
418
self._setmode(relpath, mode)
360
self._setmode(relpath, mode)
420
362
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)
363
self._translate_perm_error(e, abspath, extra='error appending',
364
unknown_exc=errors.NoSuchFile)
430
365
except ftplib.error_temp, e:
431
366
if retries > _number_of_retries:
432
raise errors.TransportError(
433
"FTP temporary error during APPEND %s. Aborting."
434
% abspath, orig_error=e)
367
raise errors.TransportError("FTP temporary error during APPEND %s." \
368
"Aborting." % abspath, orig_error=e)
436
370
warning("FTP temporary error: %s. Retrying.", str(e))
437
371
self._reconnect()
438
372
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
374
def _setmode(self, relpath, mode):
448
375
"""Set permissions on a path.
450
377
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))
381
mutter("FTP site chmod: setting permissions to %s on %s",
382
str(mode), self._remote_path(relpath))
383
ftp = self._get_FTP()
384
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
386
except ftplib.error_perm, e:
387
# Command probably not available on this server
388
warning("FTP Could not set permissions to %s on %s. %s",
389
str(mode), self._remote_path(relpath), str(e))
466
391
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
467
392
# to copy something to another machine. And you may be able
609
512
return self.lock_read(relpath)
515
class FtpServer(Server):
516
"""Common code for SFTP server facilities."""
520
self._ftp_server = None
522
self._async_thread = None
527
"""Calculate an ftp url to this server."""
528
return 'ftp://foo:bar@localhost:%d/' % (self._port)
530
# def get_bogus_url(self):
531
# """Return a URL which cannot be connected to."""
532
# return 'ftp://127.0.0.1:1'
534
def log(self, message):
535
"""This is used by medusa.ftp_server to log connections, etc."""
536
self.logs.append(message)
538
def setUp(self, vfs_server=None):
540
raise RuntimeError('Must have medusa to run the FtpServer')
542
assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
543
"FtpServer currently assumes local transport, got %s" % vfs_server
545
self._root = os.getcwdu()
546
self._ftp_server = _ftp_server(
547
authorizer=_test_authorizer(root=self._root),
549
port=0, # bind to a random port
551
logger_object=self # Use FtpServer.log() for messages
553
self._port = self._ftp_server.getsockname()[1]
554
# Don't let it loop forever, or handle an infinite number of requests.
555
# In this case it will run for 1000s, or 10000 requests
556
self._async_thread = threading.Thread(
557
target=FtpServer._asyncore_loop_ignore_EBADF,
558
kwargs={'timeout':0.1, 'count':10000})
559
self._async_thread.setDaemon(True)
560
self._async_thread.start()
563
"""See bzrlib.transport.Server.tearDown."""
564
# have asyncore release the channel
565
self._ftp_server.del_channel()
567
self._async_thread.join()
570
def _asyncore_loop_ignore_EBADF(*args, **kwargs):
571
"""Ignore EBADF during server shutdown.
573
We close the socket to get the server to shutdown, but this causes
574
select.select() to raise EBADF.
577
asyncore.loop(*args, **kwargs)
578
# FIXME: If we reach that point, we should raise an exception
579
# explaining that the 'count' parameter in setUp is too low or
580
# testers may wonder why their test just sits there waiting for a
581
# server that is already dead. Note that if the tester waits too
582
# long under pdb the server will also die.
583
except select.error, e:
584
if e.args[0] != errno.EBADF:
590
_test_authorizer = None
594
global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
597
import medusa.filesys
598
import medusa.ftp_server
604
class test_authorizer(object):
605
"""A custom Authorizer object for running the test suite.
607
The reason we cannot use dummy_authorizer, is because it sets the
608
channel to readonly, which we don't always want to do.
611
def __init__(self, root):
614
def authorize(self, channel, username, password):
615
"""Return (success, reply_string, filesystem)"""
617
return 0, 'No Medusa.', None
619
channel.persona = -1, -1
620
if username == 'anonymous':
621
channel.read_only = 1
623
channel.read_only = 0
625
return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
628
class ftp_channel(medusa.ftp_server.ftp_channel):
629
"""Customized ftp channel"""
631
def log(self, message):
632
"""Redirect logging requests."""
633
mutter('_ftp_channel: %s', message)
635
def log_info(self, message, type='info'):
636
"""Redirect logging requests."""
637
mutter('_ftp_channel %s: %s', type, message)
639
def cmd_rnfr(self, line):
640
"""Prepare for renaming a file."""
641
self._renaming = line[1]
642
self.respond('350 Ready for RNTO')
643
# TODO: jam 20060516 in testing, the ftp server seems to
644
# check that the file already exists, or it sends
645
# 550 RNFR command failed
647
def cmd_rnto(self, line):
648
"""Rename a file based on the target given.
650
rnto must be called after calling rnfr.
652
if not self._renaming:
653
self.respond('503 RNFR required first.')
654
pfrom = self.filesystem.translate(self._renaming)
655
self._renaming = None
656
pto = self.filesystem.translate(line[1])
657
if os.path.exists(pto):
658
self.respond('550 RNTO failed: file exists')
661
os.rename(pfrom, pto)
662
except (IOError, OSError), e:
663
# TODO: jam 20060516 return custom responses based on
664
# why the command failed
665
# (bialix 20070418) str(e) on Python 2.5 @ Windows
666
# sometimes don't provide expected error message;
667
# so we obtain such message via os.strerror()
668
self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
670
self.respond('550 RNTO failed')
671
# For a test server, we will go ahead and just die
674
self.respond('250 Rename successful.')
676
def cmd_size(self, line):
677
"""Return the size of a file
679
This is overloaded to help the test suite determine if the
680
target is a directory.
683
if not self.filesystem.isfile(filename):
684
if self.filesystem.isdir(filename):
685
self.respond('550 "%s" is a directory' % (filename,))
687
self.respond('550 "%s" is not a file' % (filename,))
689
self.respond('213 %d'
690
% (self.filesystem.stat(filename)[stat.ST_SIZE]),)
692
def cmd_mkd(self, line):
693
"""Create a directory.
695
Overloaded because default implementation does not distinguish
696
*why* it cannot make a directory.
699
self.command_not_understood(''.join(line))
703
self.filesystem.mkdir (path)
704
self.respond ('257 MKD command successful.')
705
except (IOError, OSError), e:
706
# (bialix 20070418) str(e) on Python 2.5 @ Windows
707
# sometimes don't provide expected error message;
708
# so we obtain such message via os.strerror()
709
self.respond ('550 error creating directory: %s' %
710
os.strerror(e.errno))
712
self.respond ('550 error creating directory.')
715
class ftp_server(medusa.ftp_server.ftp_server):
716
"""Customize the behavior of the Medusa ftp_server.
718
There are a few warts on the ftp_server, based on how it expects
722
ftp_channel_class = ftp_channel
724
def __init__(self, *args, **kwargs):
725
mutter('Initializing _ftp_server: %r, %r', args, kwargs)
726
medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
728
def log(self, message):
729
"""Redirect logging requests."""
730
mutter('_ftp_server: %s', message)
732
def log_info(self, message, type='info'):
733
"""Override the asyncore.log_info so we don't stipple the screen."""
734
mutter('_ftp_server %s: %s', type, message)
736
_test_authorizer = test_authorizer
737
_ftp_channel = ftp_channel
738
_ftp_server = ftp_server
612
743
def get_test_permutations():
613
744
"""Return the permutations to be used in testing."""
614
from bzrlib.tests import ftp_server
615
return [(FtpTransport, ftp_server.FTPTestServer)]
745
if not _setup_medusa():
746
warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
749
return [(FtpTransport, FtpServer)]