1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
1
# Copyright (C) 2005-2010 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17
"""Implementation of Transport over ftp.
18
19
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
65
60
class FtpStatResult(object):
66
def __init__(self, f, relpath):
62
def __init__(self, f, abspath):
68
self.st_size = f.size(relpath)
64
self.st_size = f.size(abspath)
69
65
self.st_mode = stat.S_IFREG
70
66
except ftplib.error_perm:
74
70
self.st_mode = stat.S_IFDIR
100
96
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
102
def _get_FTP(self):
103
103
"""Return the ftplib.FTP instance for this object."""
104
104
# Ensures that a connection is established
109
109
self._set_connection(connection, credentials)
110
110
return connection
112
connection_class = ftplib.FTP
112
114
def _create_connection(self, credentials=None):
113
115
"""Create a new connection with the provided credentials.
117
119
:return: The created connection and its associated credentials.
119
The credentials are only the password as it may have been entered
120
interactively by the user and may be different from the one provided
121
in base url at transport creation time.
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.
123
126
if credentials is None:
124
127
user, password = self._user, self._password
128
131
auth = config.AuthenticationConfig()
130
user = auth.get_user('ftp', self._host, port=self._port)
132
# Default to local user
133
user = getpass.getuser()
133
user = auth.get_user('ftp', self._host, port=self._port,
134
default=getpass.getuser())
135
135
mutter("Constructing FTP instance against %r" %
136
136
((self._host, self._port, user, '********',
137
137
self.is_active),))
139
connection = ftplib.FTP()
139
connection = self.connection_class()
140
140
connection.connect(host=self._host, port=self._port)
141
if user and user != 'anonymous' and \
142
password is None: # '' is a valid password
143
password = auth.get_password('ftp', self._host, user,
145
connection.login(user=user, passwd=password)
141
self._login(connection, auth, user, password)
146
142
connection.set_pasv(not self.is_active)
143
# binary mode is the default
144
connection.voidcmd('TYPE I')
147
145
except socket.error, e:
148
146
raise errors.SocketConnectionError(self._host, self._port,
149
147
msg='Unable to connect to',
153
151
" %s" % str(e), orig_error=e)
154
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)
156
161
def _reconnect(self):
157
162
"""Create a new connection with the previously used credentials"""
158
163
credentials = self._get_credentials()
159
164
connection, credentials = self._create_connection(credentials)
160
165
self._set_connection(connection, credentials)
162
def _translate_perm_error(self, err, path, extra=None,
167
def _translate_ftp_error(self, err, path, extra=None,
163
168
unknown_exc=FtpPathError):
164
"""Try to translate an ftplib.error_perm exception.
169
"""Try to translate an ftplib exception to a bzrlib exception.
166
171
:param err: The error to translate into a bzr error
167
172
:param path: The path which had problems
169
174
:param unknown_exc: If None, we will just raise the original exception
170
175
otherwise we raise unknown_exc(path, extra=extra)
177
# ftp error numbers are very generic, like "451: Requested action aborted,
178
# local error in processing" so unfortunately we have to match by
172
180
s = str(err).lower()
185
193
or (s.startswith('550 ') and 'unable to rename to' in extra)
187
195
raise errors.NoSuchFile(path, extra=extra)
188
if ('file exists' in s):
196
elif ('file exists' in s):
189
197
raise errors.FileExists(path, extra=extra)
190
if ('not a directory' in s):
198
elif ('not a directory' in s):
191
199
raise errors.PathError(path, extra=extra)
200
elif 'directory not empty' in s:
201
raise errors.DirectoryNotEmpty(path, extra=extra)
193
203
mutter('unable to understand error for path: %s: %s', path, err)
196
206
raise unknown_exc(path, extra=extra)
197
# TODO: jam 20060516 Consider re-raising the error wrapped in
207
# TODO: jam 20060516 Consider re-raising the error wrapped in
198
208
# something like TransportError, but this loses the traceback
199
209
# Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
200
210
# to handle. Consider doing something like that here.
201
211
#raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
204
def _remote_path(self, relpath):
205
# XXX: It seems that ftplib does not handle Unicode paths
206
# at the same time, medusa won't handle utf8 paths So if
207
# we .encode(utf8) here (see ConnectedTransport
208
# implementation), then we get a Server failure. while
209
# if we use str(), we get a UnicodeError, and the test
210
# suite just skips testing UnicodePaths.
211
relative = str(urlutils.unescape(relpath))
212
remote_path = self._combine_paths(self._path, relative)
215
214
def has(self, relpath):
216
215
"""Does the target location exist?"""
217
216
# FIXME jam 20060516 We *do* ask about directories in the test suite
327
326
except ftplib.error_perm, e:
328
self._translate_perm_error(e, abspath, extra='could not store',
327
self._translate_ftp_error(e, abspath, extra='could not store',
329
328
unknown_exc=errors.NoSuchFile)
330
329
except ftplib.error_temp, e:
331
330
if retries > _number_of_retries:
355
354
self._setmode(relpath, mode)
356
355
except ftplib.error_perm, e:
357
self._translate_perm_error(e, abspath,
356
self._translate_ftp_error(e, abspath,
358
357
unknown_exc=errors.FileExists)
360
359
def open_write_stream(self, relpath, mode=None):
380
379
f = self._get_FTP()
382
381
except ftplib.error_perm, e:
383
self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
382
self._translate_ftp_error(e, abspath, unknown_exc=errors.PathError)
385
384
def append_file(self, relpath, f, mode=None):
386
385
"""Append the text in the file-like object into the final
389
389
abspath = self._remote_path(relpath)
390
390
if self.has(relpath):
391
391
ftp = self._get_FTP()
396
mutter("FTP appe to %s", abspath)
397
self._try_append(relpath, f.read(), mode)
397
mutter("FTP appe to %s", abspath)
398
self._try_append(relpath, text, mode)
400
self._fallback_append(relpath, text, mode)
401
404
def _try_append(self, relpath, text, mode=None, retries=0):
402
405
"""Try repeatedly to append the given text to the file at relpath.
404
407
This is a recursive function. On errors, it will be called until the
405
408
number of retries is exceeded.
408
411
abspath = self._remote_path(relpath)
409
412
mutter("FTP appe (try %d) to %s", retries, abspath)
410
413
ftp = self._get_FTP()
411
ftp.voidcmd("TYPE I")
412
414
cmd = "APPE %s" % abspath
413
415
conn = ftp.transfercmd(cmd)
414
416
conn.sendall(text)
416
418
self._setmode(relpath, mode)
418
420
except ftplib.error_perm, e:
419
self._translate_perm_error(e, abspath, extra='error appending',
420
unknown_exc=errors.NoSuchFile)
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)
421
430
except ftplib.error_temp, e:
422
431
if retries > _number_of_retries:
423
raise errors.TransportError("FTP temporary error during APPEND %s." \
424
"Aborting." % abspath, orig_error=e)
432
raise errors.TransportError(
433
"FTP temporary error during APPEND %s. Aborting."
434
% abspath, orig_error=e)
426
436
warning("FTP temporary error: %s. Retrying.", str(e))
427
437
self._reconnect()
428
438
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)
430
447
def _setmode(self, relpath, mode):
431
448
"""Set permissions on a path.
438
455
mutter("FTP site chmod: setting permissions to %s on %s",
439
str(mode), self._remote_path(relpath))
456
oct(mode), self._remote_path(relpath))
440
457
ftp = self._get_FTP()
441
458
cmd = "SITE CHMOD %s %s" % (oct(mode),
442
459
self._remote_path(relpath))
444
461
except ftplib.error_perm, e:
445
462
# Command probably not available on this server
446
463
warning("FTP Could not set permissions to %s on %s. %s",
447
str(mode), self._remote_path(relpath), str(e))
464
oct(mode), self._remote_path(relpath), str(e))
449
466
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
450
467
# to copy something to another machine. And you may be able
461
478
def _rename(self, abs_from, abs_to, f):
463
480
f.rename(abs_from, abs_to)
464
except ftplib.error_perm, e:
465
self._translate_perm_error(e, abs_from,
481
except (ftplib.error_temp, ftplib.error_perm), e:
482
self._translate_ftp_error(e, abs_from,
466
483
': unable to rename to %r' % (abs_to))
468
485
def move(self, rel_from, rel_to):
474
491
f = self._get_FTP()
475
492
self._rename_and_overwrite(abs_from, abs_to, f)
476
493
except ftplib.error_perm, e:
477
self._translate_perm_error(e, abs_from,
478
extra='unable to rename to %r' % (rel_to,),
494
self._translate_ftp_error(e, abs_from,
495
extra='unable to rename to %r' % (rel_to,),
479
496
unknown_exc=errors.PathError)
481
498
def _rename_and_overwrite(self, abs_from, abs_to, f):
498
515
mutter("FTP rm: %s", abspath)
499
516
f.delete(abspath)
500
517
except ftplib.error_perm, e:
501
self._translate_perm_error(e, abspath, 'error deleting',
518
self._translate_ftp_error(e, abspath, 'error deleting',
502
519
unknown_exc=errors.NoSuchFile)
504
521
def external_url(self):
516
533
mutter("FTP nlst: %s", basepath)
517
534
f = self._get_FTP()
519
paths = f.nlst(basepath)
520
except ftplib.error_perm, e:
521
self._translate_perm_error(e, relpath, extra='error with list_dir')
522
except ftplib.error_temp, e:
523
# xs4all's ftp server raises a 450 temp error when listing an empty
524
# directory. Check for that and just return an empty list in that
525
# case. See bug #215522
526
if str(e).lower().startswith('450 no files found'):
527
mutter('FTP Server returned "%s" for nlst.'
528
' Assuming it means empty directory',
537
paths = f.nlst(basepath)
538
except ftplib.error_perm, e:
539
self._translate_ftp_error(e, relpath,
540
extra='error with list_dir')
541
except ftplib.error_temp, e:
542
# xs4all's ftp server raises a 450 temp error when listing an
543
# empty directory. Check for that and just return an empty list
544
# in that case. See bug #215522
545
if str(e).lower().startswith('450 no files found'):
546
mutter('FTP Server returned "%s" for nlst.'
547
' Assuming it means empty directory',
552
# Restore binary mode as nlst switch to ascii mode to retrieve file
532
556
# If FTP.nlst returns paths prefixed by relpath, strip 'em
533
557
if paths and paths[0].startswith(basepath):
534
558
entries = [path[len(basepath)+1:] for path in paths]
561
585
f = self._get_FTP()
562
586
return FtpStatResult(f, abspath)
563
587
except ftplib.error_perm, e:
564
self._translate_perm_error(e, abspath, extra='error w/ stat')
588
self._translate_ftp_error(e, abspath, extra='error w/ stat')
566
590
def lock_read(self, relpath):
567
591
"""Lock the given file for shared (read) access.
588
612
def get_test_permutations():
589
613
"""Return the permutations to be used in testing."""
590
from bzrlib import tests
591
if tests.FTPServerFeature.available():
592
from bzrlib.tests import ftp_server
593
return [(FtpTransport, ftp_server.FTPServer)]
595
# Dummy server to have the test suite report the number of tests
596
# needing that feature. We raise UnavailableFeature from methods before
597
# the test server is being used. Doing so in the setUp method has bad
598
# side-effects (tearDown is never called).
599
class UnavailableFTPServer(object):
608
raise tests.UnavailableFeature(tests.FTPServerFeature)
610
def get_bogus_url(self):
611
raise tests.UnavailableFeature(tests.FTPServerFeature)
613
return [(FtpTransport, UnavailableFTPServer)]
614
from bzrlib.tests import ftp_server
615
return [(FtpTransport, ftp_server.FTPTestServer)]