1
# Copyright (C) 2005-2010 Canonical Ltd
1
# Copyright (C) 2005, 2006, 2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
16
"""Implementation of Transport over ftp.
19
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
60
65
class FtpStatResult(object):
62
def __init__(self, f, abspath):
66
def __init__(self, f, relpath):
64
self.st_size = f.size(abspath)
68
self.st_size = f.size(relpath)
65
69
self.st_mode = stat.S_IFREG
66
70
except ftplib.error_perm:
70
74
self.st_mode = stat.S_IFDIR
96
100
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
114
112
def _create_connection(self, credentials=None):
115
113
"""Create a new connection with the provided credentials.
119
117
: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.
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.
126
123
if credentials is None:
127
124
user, password = self._user, self._password
131
128
auth = config.AuthenticationConfig()
133
user = auth.get_user('ftp', self._host, port=self._port,
134
default=getpass.getuser())
130
user = auth.get_user('ftp', self._host, port=self._port)
132
# Default to local user
133
user = getpass.getuser()
135
135
mutter("Constructing FTP instance against %r" %
136
136
((self._host, self._port, user, '********',
137
137
self.is_active),))
139
connection = self.connection_class()
139
connection = ftplib.FTP()
140
140
connection.connect(host=self._host, port=self._port)
141
self._login(connection, auth, user, password)
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)
142
146
connection.set_pasv(not self.is_active)
143
# binary mode is the default
144
connection.voidcmd('TYPE I')
145
147
except socket.error, e:
146
148
raise errors.SocketConnectionError(self._host, self._port,
147
149
msg='Unable to connect to',
151
153
" %s" % str(e), orig_error=e)
152
154
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)
161
156
def _reconnect(self):
162
157
"""Create a new connection with the previously used credentials"""
163
158
credentials = self._get_credentials()
164
159
connection, credentials = self._create_connection(credentials)
165
160
self._set_connection(connection, credentials)
167
def _translate_ftp_error(self, err, path, extra=None,
162
def _translate_perm_error(self, err, path, extra=None,
168
163
unknown_exc=FtpPathError):
169
"""Try to translate an ftplib exception to a bzrlib exception.
164
"""Try to translate an ftplib.error_perm exception.
171
166
:param err: The error to translate into a bzr error
172
167
:param path: The path which had problems
174
169
:param unknown_exc: If None, we will just raise the original exception
175
170
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
180
172
s = str(err).lower()
193
185
or (s.startswith('550 ') and 'unable to rename to' in extra)
195
187
raise errors.NoSuchFile(path, extra=extra)
196
elif ('file exists' in s):
188
if ('file exists' in s):
197
189
raise errors.FileExists(path, extra=extra)
198
elif ('not a directory' in s):
190
if ('not a directory' in s):
199
191
raise errors.PathError(path, extra=extra)
200
elif 'directory not empty' in s:
201
raise errors.DirectoryNotEmpty(path, extra=extra)
203
193
mutter('unable to understand error for path: %s: %s', path, err)
206
196
raise unknown_exc(path, extra=extra)
207
# TODO: jam 20060516 Consider re-raising the error wrapped in
197
# TODO: jam 20060516 Consider re-raising the error wrapped in
208
198
# something like TransportError, but this loses the traceback
209
199
# Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
210
200
# to handle. Consider doing something like that here.
211
201
#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)
214
215
def has(self, relpath):
215
216
"""Does the target location exist?"""
216
217
# FIXME jam 20060516 We *do* ask about directories in the test suite
326
327
except ftplib.error_perm, e:
327
self._translate_ftp_error(e, abspath, extra='could not store',
328
self._translate_perm_error(e, abspath, extra='could not store',
328
329
unknown_exc=errors.NoSuchFile)
329
330
except ftplib.error_temp, e:
330
331
if retries > _number_of_retries:
354
355
self._setmode(relpath, mode)
355
356
except ftplib.error_perm, e:
356
self._translate_ftp_error(e, abspath,
357
self._translate_perm_error(e, abspath,
357
358
unknown_exc=errors.FileExists)
359
360
def open_write_stream(self, relpath, mode=None):
379
380
f = self._get_FTP()
381
382
except ftplib.error_perm, e:
382
self._translate_ftp_error(e, abspath, unknown_exc=errors.PathError)
383
self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
384
385
def append_file(self, relpath, f, mode=None):
385
386
"""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()
397
mutter("FTP appe to %s", abspath)
398
self._try_append(relpath, text, mode)
400
self._fallback_append(relpath, text, mode)
396
mutter("FTP appe to %s", abspath)
397
self._try_append(relpath, f.read(), mode)
404
401
def _try_append(self, relpath, text, mode=None, retries=0):
405
402
"""Try repeatedly to append the given text to the file at relpath.
407
404
This is a recursive function. On errors, it will be called until the
408
405
number of retries is exceeded.
411
408
abspath = self._remote_path(relpath)
412
409
mutter("FTP appe (try %d) to %s", retries, abspath)
413
410
ftp = self._get_FTP()
411
ftp.voidcmd("TYPE I")
414
412
cmd = "APPE %s" % abspath
415
413
conn = ftp.transfercmd(cmd)
416
414
conn.sendall(text)
418
416
self._setmode(relpath, mode)
420
418
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)
419
self._translate_perm_error(e, abspath, extra='error appending',
420
unknown_exc=errors.NoSuchFile)
430
421
except ftplib.error_temp, e:
431
422
if retries > _number_of_retries:
432
raise errors.TransportError(
433
"FTP temporary error during APPEND %s. Aborting."
434
% abspath, orig_error=e)
423
raise errors.TransportError("FTP temporary error during APPEND %s." \
424
"Aborting." % abspath, orig_error=e)
436
426
warning("FTP temporary error: %s. Retrying.", str(e))
437
427
self._reconnect()
438
428
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
430
def _setmode(self, relpath, mode):
448
431
"""Set permissions on a path.
455
438
mutter("FTP site chmod: setting permissions to %s on %s",
456
oct(mode), self._remote_path(relpath))
439
str(mode), self._remote_path(relpath))
457
440
ftp = self._get_FTP()
458
441
cmd = "SITE CHMOD %s %s" % (oct(mode),
459
442
self._remote_path(relpath))
461
444
except ftplib.error_perm, e:
462
445
# Command probably not available on this server
463
446
warning("FTP Could not set permissions to %s on %s. %s",
464
oct(mode), self._remote_path(relpath), str(e))
447
str(mode), self._remote_path(relpath), str(e))
466
449
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
467
450
# to copy something to another machine. And you may be able
478
461
def _rename(self, abs_from, abs_to, f):
480
463
f.rename(abs_from, abs_to)
481
except (ftplib.error_temp, ftplib.error_perm), e:
482
self._translate_ftp_error(e, abs_from,
464
except ftplib.error_perm, e:
465
self._translate_perm_error(e, abs_from,
483
466
': unable to rename to %r' % (abs_to))
485
468
def move(self, rel_from, rel_to):
491
474
f = self._get_FTP()
492
475
self._rename_and_overwrite(abs_from, abs_to, f)
493
476
except ftplib.error_perm, e:
494
self._translate_ftp_error(e, abs_from,
495
extra='unable to rename to %r' % (rel_to,),
477
self._translate_perm_error(e, abs_from,
478
extra='unable to rename to %r' % (rel_to,),
496
479
unknown_exc=errors.PathError)
498
481
def _rename_and_overwrite(self, abs_from, abs_to, f):
515
498
mutter("FTP rm: %s", abspath)
516
499
f.delete(abspath)
517
500
except ftplib.error_perm, e:
518
self._translate_ftp_error(e, abspath, 'error deleting',
501
self._translate_perm_error(e, abspath, 'error deleting',
519
502
unknown_exc=errors.NoSuchFile)
521
504
def external_url(self):
533
516
mutter("FTP nlst: %s", basepath)
534
517
f = self._get_FTP()
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
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',
556
532
# If FTP.nlst returns paths prefixed by relpath, strip 'em
557
533
if paths and paths[0].startswith(basepath):
558
534
entries = [path[len(basepath)+1:] for path in paths]
585
561
f = self._get_FTP()
586
562
return FtpStatResult(f, abspath)
587
563
except ftplib.error_perm, e:
588
self._translate_ftp_error(e, abspath, extra='error w/ stat')
564
self._translate_perm_error(e, abspath, extra='error w/ stat')
590
566
def lock_read(self, relpath):
591
567
"""Lock the given file for shared (read) access.
612
588
def get_test_permutations():
613
589
"""Return the permutations to be used in testing."""
614
from bzrlib.tests import ftp_server
615
return [(FtpTransport, ftp_server.FTPTestServer)]
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)]