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
68
65
class FtpStatResult(object):
70
def __init__(self, f, abspath):
66
def __init__(self, f, relpath):
72
self.st_size = f.size(abspath)
68
self.st_size = f.size(relpath)
73
69
self.st_mode = stat.S_IFREG
74
70
except ftplib.error_perm:
78
74
self.st_mode = stat.S_IFDIR
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
187
169
:param unknown_exc: If None, we will just raise the original exception
188
170
otherwise we raise unknown_exc(path, extra=extra)
190
# ftp error numbers are very generic, like "451: Requested action aborted,
191
# local error in processing" so unfortunately we have to match by
193
172
s = str(err).lower()
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
248
233
mutter("FTP has not: %s: %s", abspath, e)
251
def get(self, relpath, retries=0):
236
def get(self, relpath, decode=False, retries=0):
252
237
"""Get the file at the given relative path.
254
239
:param relpath: The relative path to the file
276
262
warning("FTP temporary error: %s. Retrying.", str(e))
277
263
self._reconnect()
278
return self.get(relpath, retries+1)
264
return self.get(relpath, decode, retries+1)
279
265
except EOFError, e:
280
266
if retries > _number_of_retries:
281
267
raise errors.TransportError("FTP control connection closed during GET %s."
285
271
warning("FTP control connection closed. Trying to reopen.")
286
272
time.sleep(_sleep_between_retries)
287
273
self._reconnect()
288
return self.get(relpath, retries+1)
274
return self.get(relpath, decode, retries+1)
290
276
def put_file(self, relpath, fp, mode=None, retries=0):
291
277
"""Copy the file-like or string object into the location.
329
315
return len(bytes)
331
317
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, ))
318
except (ftplib.error_temp,EOFError), e:
319
warning("Failure during ftp PUT. Deleting temporary file.")
336
321
f.delete(tmp_abspath)
342
327
except ftplib.error_perm, e:
343
self._translate_ftp_error(e, abspath, extra='could not store',
328
self._translate_perm_error(e, abspath, extra='could not store',
344
329
unknown_exc=errors.NoSuchFile)
345
330
except ftplib.error_temp, e:
346
331
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)
332
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
333
% self.abspath(relpath), orig_error=e)
351
335
warning("FTP temporary error: %s. Retrying.", str(e))
352
336
self._reconnect()
368
352
mutter("FTP mkd: %s", abspath)
369
353
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
355
self._setmode(relpath, mode)
382
356
except ftplib.error_perm, e:
383
self._translate_ftp_error(e, abspath,
357
self._translate_perm_error(e, abspath,
384
358
unknown_exc=errors.FileExists)
386
360
def open_write_stream(self, relpath, mode=None):
406
380
f = self._get_FTP()
408
382
except ftplib.error_perm, e:
409
self._translate_ftp_error(e, abspath, unknown_exc=errors.PathError)
383
self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
411
385
def append_file(self, relpath, f, mode=None):
412
386
"""Append the text in the file-like object into the final
416
389
abspath = self._remote_path(relpath)
417
390
if self.has(relpath):
418
391
ftp = self._get_FTP()
424
mutter("FTP appe to %s", abspath)
425
self._try_append(relpath, text, mode)
427
self._fallback_append(relpath, text, mode)
396
mutter("FTP appe to %s", abspath)
397
self._try_append(relpath, f.read(), mode)
431
401
def _try_append(self, relpath, text, mode=None, retries=0):
432
402
"""Try repeatedly to append the given text to the file at relpath.
434
404
This is a recursive function. On errors, it will be called until the
435
405
number of retries is exceeded.
445
416
self._setmode(relpath, mode)
447
418
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)
419
self._translate_perm_error(e, abspath, extra='error appending',
420
unknown_exc=errors.NoSuchFile)
457
421
except ftplib.error_temp, e:
458
422
if retries > _number_of_retries:
459
raise errors.TransportError(
460
"FTP temporary error during APPEND %s. Aborting."
461
% abspath, orig_error=e)
423
raise errors.TransportError("FTP temporary error during APPEND %s." \
424
"Aborting." % abspath, orig_error=e)
463
426
warning("FTP temporary error: %s. Retrying.", str(e))
464
427
self._reconnect()
465
428
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
430
def _setmode(self, relpath, mode):
475
431
"""Set permissions on a path.
482
438
mutter("FTP site chmod: setting permissions to %s on %s",
483
oct(mode), self._remote_path(relpath))
439
str(mode), self._remote_path(relpath))
484
440
ftp = self._get_FTP()
485
441
cmd = "SITE CHMOD %s %s" % (oct(mode),
486
442
self._remote_path(relpath))
488
444
except ftplib.error_perm, e:
489
445
# Command probably not available on this server
490
446
warning("FTP Could not set permissions to %s on %s. %s",
491
oct(mode), self._remote_path(relpath), str(e))
447
str(mode), self._remote_path(relpath), str(e))
493
449
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
494
450
# to copy something to another machine. And you may be able
505
461
def _rename(self, abs_from, abs_to, f):
507
463
f.rename(abs_from, abs_to)
508
except (ftplib.error_temp, ftplib.error_perm), e:
509
self._translate_ftp_error(e, abs_from,
464
except ftplib.error_perm, e:
465
self._translate_perm_error(e, abs_from,
510
466
': unable to rename to %r' % (abs_to))
512
468
def move(self, rel_from, rel_to):
518
474
f = self._get_FTP()
519
475
self._rename_and_overwrite(abs_from, abs_to, f)
520
476
except ftplib.error_perm, e:
521
self._translate_ftp_error(e, abs_from,
522
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,),
523
479
unknown_exc=errors.PathError)
525
481
def _rename_and_overwrite(self, abs_from, abs_to, f):
542
498
mutter("FTP rm: %s", abspath)
543
499
f.delete(abspath)
544
500
except ftplib.error_perm, e:
545
self._translate_ftp_error(e, abspath, 'error deleting',
501
self._translate_perm_error(e, abspath, 'error deleting',
546
502
unknown_exc=errors.NoSuchFile)
548
504
def external_url(self):
560
516
mutter("FTP nlst: %s", basepath)
561
517
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
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',
583
532
# If FTP.nlst returns paths prefixed by relpath, strip 'em
584
533
if paths and paths[0].startswith(basepath):
585
534
entries = [path[len(basepath)+1:] for path in paths]
612
561
f = self._get_FTP()
613
562
return FtpStatResult(f, abspath)
614
563
except ftplib.error_perm, e:
615
self._translate_ftp_error(e, abspath, extra='error w/ stat')
564
self._translate_perm_error(e, abspath, extra='error w/ stat')
617
566
def lock_read(self, relpath):
618
567
"""Lock the given file for shared (read) access.
639
588
def get_test_permutations():
640
589
"""Return the permutations to be used in testing."""
641
from bzrlib.tests import ftp_server
642
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)]