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
94
90
def __init__(self, base, _from_transport=None):
95
91
"""Set the base path where files will be stored."""
96
if not (base.startswith('ftp://') or base.startswith('aftp://')):
97
raise ValueError(base)
92
assert base.startswith('ftp://') or base.startswith('aftp://')
98
93
super(FtpTransport, self).__init__(base,
99
94
_from_transport=_from_transport)
100
95
self._unqualified_scheme = 'ftp'
101
if self._parsed_url.scheme == 'aftp':
96
if self._scheme == 'aftp':
102
97
self.is_active = True
104
99
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
101
def _get_FTP(self):
111
102
"""Return the ftplib.FTP instance for this object."""
112
103
# Ensures that a connection is established
127
116
: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.
118
The credentials are only the password as it may have been entered
119
interactively by the user and may be different from the one provided
120
in base url at transport creation time.
134
122
if credentials is None:
135
123
user, password = self._user, self._password
139
127
auth = config.AuthenticationConfig()
141
user = auth.get_user('ftp', self._host, port=self._port,
142
default=getpass.getuser())
129
user = auth.get_user('ftp', self._host, port=self._port)
131
# Default to local user
132
user = getpass.getuser()
143
134
mutter("Constructing FTP instance against %r" %
144
135
((self._host, self._port, user, '********',
145
136
self.is_active),))
147
connection = self.connection_class()
138
connection = ftplib.FTP()
148
139
connection.connect(host=self._host, port=self._port)
149
self._login(connection, auth, user, password)
140
if user and user != 'anonymous' and \
141
password is None: # '' is a valid password
142
password = auth.get_password('ftp', self._host, user,
144
connection.login(user=user, passwd=password)
150
145
connection.set_pasv(not self.is_active)
151
# binary mode is the default
152
connection.voidcmd('TYPE I')
153
146
except socket.error, e:
154
147
raise errors.SocketConnectionError(self._host, self._port,
155
148
msg='Unable to connect to',
159
152
" %s" % str(e), orig_error=e)
160
153
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
155
def _reconnect(self):
170
156
"""Create a new connection with the previously used credentials"""
171
157
credentials = self._get_credentials()
172
158
connection, credentials = self._create_connection(credentials)
173
159
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,
161
def _translate_perm_error(self, err, path, extra=None,
181
162
unknown_exc=FtpPathError):
182
"""Try to translate an ftplib exception to a bzrlib exception.
163
"""Try to translate an ftplib.error_perm exception.
184
165
:param err: The error to translate into a bzr error
185
166
:param path: The path which had problems
200
178
or 'no such dir' in s
201
179
or 'could not create file' in s # vsftpd
202
180
or 'file doesn\'t exist' in s
203
or 'rnfr command failed.' in s # vsftpd RNFR reply if file not found
204
181
or 'file/directory not found' in s # filezilla server
205
# Microsoft FTP-Service RNFR reply if file not found
206
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
183
raise errors.NoSuchFile(path, extra=extra)
212
elif ('file exists' in s):
184
if ('file exists' in s):
213
185
raise errors.FileExists(path, extra=extra)
214
elif ('not a directory' in s):
186
if ('not a directory' in s):
215
187
raise errors.PathError(path, extra=extra)
216
elif 'directory not empty' in s:
217
raise errors.DirectoryNotEmpty(path, extra=extra)
219
189
mutter('unable to understand error for path: %s: %s', path, err)
222
192
raise unknown_exc(path, extra=extra)
223
# TODO: jam 20060516 Consider re-raising the error wrapped in
193
# TODO: jam 20060516 Consider re-raising the error wrapped in
224
194
# something like TransportError, but this loses the traceback
225
195
# Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
226
196
# to handle. Consider doing something like that here.
227
197
#raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
200
def _remote_path(self, relpath):
201
# XXX: It seems that ftplib does not handle Unicode paths
202
# at the same time, medusa won't handle utf8 paths So if
203
# we .encode(utf8) here (see ConnectedTransport
204
# implementation), then we get a Server failure. while
205
# if we use str(), we get a UnicodeError, and the test
206
# suite just skips testing UnicodePaths.
207
relative = str(urlutils.unescape(relpath))
208
remote_path = self._combine_paths(self._path, relative)
230
211
def has(self, relpath):
231
212
"""Does the target location exist?"""
232
213
# FIXME jam 20060516 We *do* ask about directories in the test suite
325
307
f.storbinary('STOR '+tmp_abspath, fp)
326
308
self._rename_and_overwrite(tmp_abspath, abspath, f)
327
self._setmode(relpath, mode)
328
309
if bytes is not None:
329
310
return len(bytes)
331
312
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, ))
313
except (ftplib.error_temp,EOFError), e:
314
warning("Failure during ftp PUT. Deleting temporary file.")
336
316
f.delete(tmp_abspath)
342
322
except ftplib.error_perm, e:
343
self._translate_ftp_error(e, abspath, extra='could not store',
323
self._translate_perm_error(e, abspath, extra='could not store',
344
324
unknown_exc=errors.NoSuchFile)
345
325
except ftplib.error_temp, e:
346
326
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)
327
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
328
% self.abspath(relpath), orig_error=e)
351
330
warning("FTP temporary error: %s. Retrying.", str(e))
352
331
self._reconnect()
368
347
mutter("FTP mkd: %s", abspath)
369
348
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
350
except ftplib.error_perm, e:
383
self._translate_ftp_error(e, abspath,
351
self._translate_perm_error(e, abspath,
384
352
unknown_exc=errors.FileExists)
386
354
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)
390
mutter("FTP appe to %s", abspath)
391
self._try_append(relpath, f.read(), mode)
431
395
def _try_append(self, relpath, text, mode=None, retries=0):
432
396
"""Try repeatedly to append the given text to the file at relpath.
434
398
This is a recursive function. On errors, it will be called until the
435
399
number of retries is exceeded.
438
402
abspath = self._remote_path(relpath)
439
403
mutter("FTP appe (try %d) to %s", retries, abspath)
440
404
ftp = self._get_FTP()
405
ftp.voidcmd("TYPE I")
441
406
cmd = "APPE %s" % abspath
442
407
conn = ftp.transfercmd(cmd)
443
408
conn.sendall(text)
445
self._setmode(relpath, mode)
411
self._setmode(relpath, mode)
447
413
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)
414
self._translate_perm_error(e, abspath, extra='error appending',
415
unknown_exc=errors.NoSuchFile)
457
416
except ftplib.error_temp, e:
458
417
if retries > _number_of_retries:
459
raise errors.TransportError(
460
"FTP temporary error during APPEND %s. Aborting."
461
% abspath, orig_error=e)
418
raise errors.TransportError("FTP temporary error during APPEND %s." \
419
"Aborting." % abspath, orig_error=e)
463
421
warning("FTP temporary error: %s. Retrying.", str(e))
464
422
self._reconnect()
465
423
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
425
def _setmode(self, relpath, mode):
475
426
"""Set permissions on a path.
477
428
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))
432
mutter("FTP site chmod: setting permissions to %s on %s",
433
str(mode), self._remote_path(relpath))
434
ftp = self._get_FTP()
435
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
437
except ftplib.error_perm, e:
438
# Command probably not available on this server
439
warning("FTP Could not set permissions to %s on %s. %s",
440
str(mode), self._remote_path(relpath), str(e))
493
442
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
494
443
# to copy something to another machine. And you may be able
505
454
def _rename(self, abs_from, abs_to, f):
507
456
f.rename(abs_from, abs_to)
508
except (ftplib.error_temp, ftplib.error_perm), e:
509
self._translate_ftp_error(e, abs_from,
457
except ftplib.error_perm, e:
458
self._translate_perm_error(e, abs_from,
510
459
': unable to rename to %r' % (abs_to))
512
461
def move(self, rel_from, rel_to):
518
467
f = self._get_FTP()
519
468
self._rename_and_overwrite(abs_from, abs_to, f)
520
469
except ftplib.error_perm, e:
521
self._translate_ftp_error(e, abs_from,
522
extra='unable to rename to %r' % (rel_to,),
470
self._translate_perm_error(e, abs_from,
471
extra='unable to rename to %r' % (rel_to,),
523
472
unknown_exc=errors.PathError)
525
474
def _rename_and_overwrite(self, abs_from, abs_to, f):
560
509
mutter("FTP nlst: %s", basepath)
561
510
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
512
paths = f.nlst(basepath)
513
except ftplib.error_perm, e:
514
self._translate_perm_error(e, relpath, extra='error with list_dir')
583
515
# If FTP.nlst returns paths prefixed by relpath, strip 'em
584
516
if paths and paths[0].startswith(basepath):
585
517
entries = [path[len(basepath)+1:] for path in paths]
639
571
def get_test_permutations():
640
572
"""Return the permutations to be used in testing."""
641
from bzrlib.tests import ftp_server
642
return [(FtpTransport, ftp_server.FTPTestServer)]
573
from bzrlib import tests
574
if tests.FTPServerFeature.available():
575
from bzrlib.tests import ftp_server
576
return [(FtpTransport, ftp_server.FTPServer)]
578
# Dummy server to have the test suite report the number of tests
579
# needing that feature.
580
class UnavailableFTPServer(object):
582
raise tests.UnavailableFeature(tests.FTPServerFeature)
584
return [(FtpTransport, UnavailableFTPServer)]