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
98
94
super(FtpTransport, self).__init__(base,
99
95
_from_transport=_from_transport)
100
96
self._unqualified_scheme = 'ftp'
101
if self._parsed_url.scheme == 'aftp':
97
if self._scheme == 'aftp':
102
98
self.is_active = True
104
100
self.is_active = False
106
# Most modern FTP servers support the APPE command. If ours doesn't, we
107
# (re)set this flag accordingly later.
108
self._has_append = True
110
102
def _get_FTP(self):
111
103
"""Return the ftplib.FTP instance for this object."""
112
104
# Ensures that a connection is established
127
117
:return: The created connection and its associated credentials.
129
The input credentials are only the password as it may have been
130
entered interactively by the user and may be different from the one
131
provided in base url at transport creation time. The returned
132
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.
134
123
if credentials is None:
135
124
user, password = self._user, self._password
139
128
auth = config.AuthenticationConfig()
141
user = auth.get_user('ftp', self._host, port=self._port,
142
default=getpass.getuser())
130
user = auth.get_user('ftp', self._host, port=self._port)
132
# Default to local user
133
user = getpass.getuser()
143
135
mutter("Constructing FTP instance against %r" %
144
136
((self._host, self._port, user, '********',
145
137
self.is_active),))
147
connection = self.connection_class()
139
connection = ftplib.FTP()
148
140
connection.connect(host=self._host, port=self._port)
149
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)
150
146
connection.set_pasv(not self.is_active)
151
# binary mode is the default
152
connection.voidcmd('TYPE I')
153
147
except socket.error, e:
154
148
raise errors.SocketConnectionError(self._host, self._port,
155
149
msg='Unable to connect to',
159
153
" %s" % str(e), orig_error=e)
160
154
return connection, (user, password)
162
def _login(self, connection, auth, user, password):
163
# '' is a valid password
164
if user and user != 'anonymous' and password is None:
165
password = auth.get_password('ftp', self._host,
166
user, port=self._port)
167
connection.login(user=user, passwd=password)
169
156
def _reconnect(self):
170
157
"""Create a new connection with the previously used credentials"""
171
158
credentials = self._get_credentials()
172
159
connection, credentials = self._create_connection(credentials)
173
160
self._set_connection(connection, credentials)
175
def disconnect(self):
176
connection = self._get_connection()
177
if connection is not None:
180
def _translate_ftp_error(self, err, path, extra=None,
162
def _translate_perm_error(self, err, path, extra=None,
181
163
unknown_exc=FtpPathError):
182
"""Try to translate an ftplib exception to a bzrlib exception.
164
"""Try to translate an ftplib.error_perm exception.
184
166
:param err: The error to translate into a bzr error
185
167
:param path: The path which had problems
204
183
or 'file/directory not found' in s # filezilla server
205
184
# Microsoft FTP-Service RNFR reply if file not found
206
185
or (s.startswith('550 ') and 'unable to rename to' in extra)
207
# if containing directory doesn't exist, suggested by
208
# <https://bugs.launchpad.net/bzr/+bug/224373>
209
or (s.startswith('550 ') and "can't find folder" in s)
211
187
raise errors.NoSuchFile(path, extra=extra)
212
elif ('file exists' in s):
188
if ('file exists' in s):
213
189
raise errors.FileExists(path, extra=extra)
214
elif ('not a directory' in s):
190
if ('not a directory' in s):
215
191
raise errors.PathError(path, extra=extra)
216
elif 'directory not empty' in s:
217
raise errors.DirectoryNotEmpty(path, extra=extra)
219
193
mutter('unable to understand error for path: %s: %s', path, err)
222
196
raise unknown_exc(path, extra=extra)
223
# TODO: jam 20060516 Consider re-raising the error wrapped in
197
# TODO: jam 20060516 Consider re-raising the error wrapped in
224
198
# something like TransportError, but this loses the traceback
225
199
# Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
226
200
# to handle. Consider doing something like that here.
227
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)
230
215
def has(self, relpath):
231
216
"""Does the target location exist?"""
232
217
# FIXME jam 20060516 We *do* ask about directories in the test suite
325
311
f.storbinary('STOR '+tmp_abspath, fp)
326
312
self._rename_and_overwrite(tmp_abspath, abspath, f)
327
self._setmode(relpath, mode)
328
313
if bytes is not None:
329
314
return len(bytes)
331
316
return fp.counted_bytes
332
except (ftplib.error_temp, EOFError), e:
333
warning("Failure during ftp PUT of %s: %s. Deleting temporary file."
334
% (tmp_abspath, e, ))
317
except (ftplib.error_temp,EOFError), e:
318
warning("Failure during ftp PUT. Deleting temporary file.")
336
320
f.delete(tmp_abspath)
342
326
except ftplib.error_perm, e:
343
self._translate_ftp_error(e, abspath, extra='could not store',
327
self._translate_perm_error(e, abspath, extra='could not store',
344
328
unknown_exc=errors.NoSuchFile)
345
329
except ftplib.error_temp, e:
346
330
if retries > _number_of_retries:
347
raise errors.TransportError(
348
"FTP temporary error during PUT %s: %s. Aborting."
349
% (self.abspath(relpath), e), orig_error=e)
331
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
332
% self.abspath(relpath), orig_error=e)
351
334
warning("FTP temporary error: %s. Retrying.", str(e))
352
335
self._reconnect()
368
351
mutter("FTP mkd: %s", abspath)
369
352
f = self._get_FTP()
372
except ftplib.error_reply, e:
373
# <https://bugs.launchpad.net/bzr/+bug/224373> Microsoft FTP
374
# server returns "250 Directory created." which is kind of
375
# reasonable, 250 meaning "requested file action OK", but not what
376
# Python's ftplib expects.
377
if e[0][:3] == '250':
381
self._setmode(relpath, mode)
382
354
except ftplib.error_perm, e:
383
self._translate_ftp_error(e, abspath,
355
self._translate_perm_error(e, abspath,
384
356
unknown_exc=errors.FileExists)
386
358
def open_write_stream(self, relpath, mode=None):
424
mutter("FTP appe to %s", abspath)
425
self._try_append(relpath, text, mode)
427
self._fallback_append(relpath, text, mode)
394
mutter("FTP appe to %s", abspath)
395
self._try_append(relpath, f.read(), mode)
431
399
def _try_append(self, relpath, text, mode=None, retries=0):
432
400
"""Try repeatedly to append the given text to the file at relpath.
434
402
This is a recursive function. On errors, it will be called until the
435
403
number of retries is exceeded.
438
406
abspath = self._remote_path(relpath)
439
407
mutter("FTP appe (try %d) to %s", retries, abspath)
440
408
ftp = self._get_FTP()
409
ftp.voidcmd("TYPE I")
441
410
cmd = "APPE %s" % abspath
442
411
conn = ftp.transfercmd(cmd)
443
412
conn.sendall(text)
445
self._setmode(relpath, mode)
415
self._setmode(relpath, mode)
447
417
except ftplib.error_perm, e:
448
# Check whether the command is not supported (reply code 502)
449
if str(e).startswith('502 '):
450
warning("FTP server does not support file appending natively. "
451
"Performance may be severely degraded! (%s)", e)
452
self._has_append = False
453
self._fallback_append(relpath, text, mode)
455
self._translate_ftp_error(e, abspath, extra='error appending',
456
unknown_exc=errors.NoSuchFile)
418
self._translate_perm_error(e, abspath, extra='error appending',
419
unknown_exc=errors.NoSuchFile)
457
420
except ftplib.error_temp, e:
458
421
if retries > _number_of_retries:
459
raise errors.TransportError(
460
"FTP temporary error during APPEND %s. Aborting."
461
% abspath, orig_error=e)
422
raise errors.TransportError("FTP temporary error during APPEND %s." \
423
"Aborting." % abspath, orig_error=e)
463
425
warning("FTP temporary error: %s. Retrying.", str(e))
464
426
self._reconnect()
465
427
self._try_append(relpath, text, mode, retries+1)
467
def _fallback_append(self, relpath, text, mode = None):
468
remote = self.get(relpath)
469
remote.seek(0, os.SEEK_END)
472
return self.put_file(relpath, remote, mode)
474
429
def _setmode(self, relpath, mode):
475
430
"""Set permissions on a path.
477
432
Only set permissions if the FTP server supports the 'SITE CHMOD'
482
mutter("FTP site chmod: setting permissions to %s on %s",
483
oct(mode), self._remote_path(relpath))
484
ftp = self._get_FTP()
485
cmd = "SITE CHMOD %s %s" % (oct(mode),
486
self._remote_path(relpath))
488
except ftplib.error_perm, e:
489
# Command probably not available on this server
490
warning("FTP Could not set permissions to %s on %s. %s",
491
oct(mode), self._remote_path(relpath), str(e))
436
mutter("FTP site chmod: setting permissions to %s on %s",
437
str(mode), self._remote_path(relpath))
438
ftp = self._get_FTP()
439
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
441
except ftplib.error_perm, e:
442
# Command probably not available on this server
443
warning("FTP Could not set permissions to %s on %s. %s",
444
str(mode), self._remote_path(relpath), str(e))
493
446
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
494
447
# to copy something to another machine. And you may be able
505
458
def _rename(self, abs_from, abs_to, f):
507
460
f.rename(abs_from, abs_to)
508
except (ftplib.error_temp, ftplib.error_perm), e:
509
self._translate_ftp_error(e, abs_from,
461
except ftplib.error_perm, e:
462
self._translate_perm_error(e, abs_from,
510
463
': unable to rename to %r' % (abs_to))
512
465
def move(self, rel_from, rel_to):
518
471
f = self._get_FTP()
519
472
self._rename_and_overwrite(abs_from, abs_to, f)
520
473
except ftplib.error_perm, e:
521
self._translate_ftp_error(e, abs_from,
522
extra='unable to rename to %r' % (rel_to,),
474
self._translate_perm_error(e, abs_from,
475
extra='unable to rename to %r' % (rel_to,),
523
476
unknown_exc=errors.PathError)
525
478
def _rename_and_overwrite(self, abs_from, abs_to, f):
560
513
mutter("FTP nlst: %s", basepath)
561
514
f = self._get_FTP()
564
paths = f.nlst(basepath)
565
except ftplib.error_perm, e:
566
self._translate_ftp_error(e, relpath,
567
extra='error with list_dir')
568
except ftplib.error_temp, e:
569
# xs4all's ftp server raises a 450 temp error when listing an
570
# empty directory. Check for that and just return an empty list
571
# in that case. See bug #215522
572
if str(e).lower().startswith('450 no files found'):
573
mutter('FTP Server returned "%s" for nlst.'
574
' Assuming it means empty directory',
579
# Restore binary mode as nlst switch to ascii mode to retrieve file
516
paths = f.nlst(basepath)
517
except ftplib.error_perm, e:
518
self._translate_perm_error(e, relpath, extra='error with list_dir')
583
519
# If FTP.nlst returns paths prefixed by relpath, strip 'em
584
520
if paths and paths[0].startswith(basepath):
585
521
entries = [path[len(basepath)+1:] for path in paths]
639
575
def get_test_permutations():
640
576
"""Return the permutations to be used in testing."""
641
from bzrlib.tests import ftp_server
642
return [(FtpTransport, ftp_server.FTPTestServer)]
577
from bzrlib import tests
578
if tests.FTPServerFeature.available():
579
from bzrlib.tests import ftp_server
580
return [(FtpTransport, ftp_server.FTPServer)]
582
# Dummy server to have the test suite report the number of tests
583
# needing that feature. We raise UnavailableFeature from methods before
584
# the test server is being used. Doing so in the setUp method has bad
585
# side-effects (tearDown is never called).
586
class UnavailableFTPServer(object):
595
raise tests.UnavailableFeature(tests.FTPServerFeature)
597
def get_bogus_url(self):
598
raise tests.UnavailableFeature(tests.FTPServerFeature)
600
return [(FtpTransport, UnavailableFTPServer)]