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
92
90
def __init__(self, base, _from_transport=None):
93
91
"""Set the base path where files will be stored."""
94
if not (base.startswith('ftp://') or base.startswith('aftp://')):
95
raise ValueError(base)
92
assert base.startswith('ftp://') or base.startswith('aftp://')
96
93
super(FtpTransport, self).__init__(base,
97
94
_from_transport=_from_transport)
98
95
self._unqualified_scheme = 'ftp'
99
if self._parsed_url.scheme == 'aftp':
96
if self._scheme == 'aftp':
100
97
self.is_active = True
102
99
self.is_active = False
104
# Most modern FTP servers support the APPE command. If ours doesn't, we
105
# (re)set this flag accordingly later.
106
self._has_append = True
108
101
def _get_FTP(self):
109
102
"""Return the ftplib.FTP instance for this object."""
110
103
# Ensures that a connection is established
125
116
:return: The created connection and its associated credentials.
127
The input credentials are only the password as it may have been
128
entered interactively by the user and may be different from the one
129
provided in base url at transport creation time. The returned
130
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.
132
122
if credentials is None:
133
123
user, password = self._user, self._password
137
127
auth = config.AuthenticationConfig()
139
user = auth.get_user('ftp', self._host, port=self._port,
140
default=getpass.getuser())
129
user = auth.get_user('ftp', self._host, port=self._port)
131
# Default to local user
132
user = getpass.getuser()
141
134
mutter("Constructing FTP instance against %r" %
142
135
((self._host, self._port, user, '********',
143
136
self.is_active),))
145
connection = self.connection_class()
138
connection = ftplib.FTP()
146
139
connection.connect(host=self._host, port=self._port)
147
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)
148
145
connection.set_pasv(not self.is_active)
149
# binary mode is the default
150
connection.voidcmd('TYPE I')
151
146
except socket.error, e:
152
147
raise errors.SocketConnectionError(self._host, self._port,
153
148
msg='Unable to connect to',
157
152
" %s" % str(e), orig_error=e)
158
153
return connection, (user, password)
160
def _login(self, connection, auth, user, password):
161
# '' is a valid password
162
if user and user != 'anonymous' and password is None:
163
password = auth.get_password('ftp', self._host,
164
user, port=self._port)
165
connection.login(user=user, passwd=password)
167
155
def _reconnect(self):
168
156
"""Create a new connection with the previously used credentials"""
169
157
credentials = self._get_credentials()
170
158
connection, credentials = self._create_connection(credentials)
171
159
self._set_connection(connection, credentials)
173
def disconnect(self):
174
connection = self._get_connection()
175
if connection is not None:
178
def _translate_ftp_error(self, err, path, extra=None,
161
def _translate_perm_error(self, err, path, extra=None,
179
162
unknown_exc=FtpPathError):
180
"""Try to translate an ftplib exception to a bzrlib exception.
163
"""Try to translate an ftplib.error_perm exception.
182
165
:param err: The error to translate into a bzr error
183
166
:param path: The path which had problems
198
178
or 'no such dir' in s
199
179
or 'could not create file' in s # vsftpd
200
180
or 'file doesn\'t exist' in s
201
or 'rnfr command failed.' in s # vsftpd RNFR reply if file not found
202
181
or 'file/directory not found' in s # filezilla server
203
# Microsoft FTP-Service RNFR reply if file not found
204
or (s.startswith('550 ') and 'unable to rename to' in extra)
205
# if containing directory doesn't exist, suggested by
206
# <https://bugs.launchpad.net/bzr/+bug/224373>
207
or (s.startswith('550 ') and "can't find folder" in s)
209
183
raise errors.NoSuchFile(path, extra=extra)
210
elif ('file exists' in s):
184
if ('file exists' in s):
211
185
raise errors.FileExists(path, extra=extra)
212
elif ('not a directory' in s):
186
if ('not a directory' in s):
213
187
raise errors.PathError(path, extra=extra)
214
elif 'directory not empty' in s:
215
raise errors.DirectoryNotEmpty(path, extra=extra)
217
189
mutter('unable to understand error for path: %s: %s', path, err)
220
192
raise unknown_exc(path, extra=extra)
221
# TODO: jam 20060516 Consider re-raising the error wrapped in
193
# TODO: jam 20060516 Consider re-raising the error wrapped in
222
194
# something like TransportError, but this loses the traceback
223
195
# Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
224
196
# to handle. Consider doing something like that here.
225
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)
228
211
def has(self, relpath):
229
212
"""Does the target location exist?"""
230
213
# FIXME jam 20060516 We *do* ask about directories in the test suite
256
239
We're meant to return a file-like object which bzr will
257
240
then read from. For now we do this via the magic of StringIO
259
if deprecated_passed(decode):
260
warn(deprecated_in((2,3,0)) %
261
'"decode" parameter to FtpTransport.get()',
262
DeprecationWarning, stacklevel=2)
242
# TODO: decode should be deprecated
264
244
mutter("FTP get: %s", self._remote_path(relpath))
265
245
f = self._get_FTP()
327
307
f.storbinary('STOR '+tmp_abspath, fp)
328
308
self._rename_and_overwrite(tmp_abspath, abspath, f)
329
self._setmode(relpath, mode)
330
309
if bytes is not None:
331
310
return len(bytes)
333
312
return fp.counted_bytes
334
except (ftplib.error_temp, EOFError), e:
335
warning("Failure during ftp PUT of %s: %s. Deleting temporary file."
336
% (tmp_abspath, e, ))
313
except (ftplib.error_temp,EOFError), e:
314
warning("Failure during ftp PUT. Deleting temporary file.")
338
316
f.delete(tmp_abspath)
344
322
except ftplib.error_perm, e:
345
self._translate_ftp_error(e, abspath, extra='could not store',
323
self._translate_perm_error(e, abspath, extra='could not store',
346
324
unknown_exc=errors.NoSuchFile)
347
325
except ftplib.error_temp, e:
348
326
if retries > _number_of_retries:
349
raise errors.TransportError(
350
"FTP temporary error during PUT %s: %s. Aborting."
351
% (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)
353
330
warning("FTP temporary error: %s. Retrying.", str(e))
354
331
self._reconnect()
370
347
mutter("FTP mkd: %s", abspath)
371
348
f = self._get_FTP()
374
except ftplib.error_reply, e:
375
# <https://bugs.launchpad.net/bzr/+bug/224373> Microsoft FTP
376
# server returns "250 Directory created." which is kind of
377
# reasonable, 250 meaning "requested file action OK", but not what
378
# Python's ftplib expects.
379
if e[0][:3] == '250':
383
self._setmode(relpath, mode)
384
350
except ftplib.error_perm, e:
385
self._translate_ftp_error(e, abspath,
351
self._translate_perm_error(e, abspath,
386
352
unknown_exc=errors.FileExists)
388
354
def open_write_stream(self, relpath, mode=None):
426
mutter("FTP appe to %s", abspath)
427
self._try_append(relpath, text, mode)
429
self._fallback_append(relpath, text, mode)
390
mutter("FTP appe to %s", abspath)
391
self._try_append(relpath, f.read(), mode)
433
395
def _try_append(self, relpath, text, mode=None, retries=0):
434
396
"""Try repeatedly to append the given text to the file at relpath.
436
398
This is a recursive function. On errors, it will be called until the
437
399
number of retries is exceeded.
440
402
abspath = self._remote_path(relpath)
441
403
mutter("FTP appe (try %d) to %s", retries, abspath)
442
404
ftp = self._get_FTP()
405
ftp.voidcmd("TYPE I")
443
406
cmd = "APPE %s" % abspath
444
407
conn = ftp.transfercmd(cmd)
445
408
conn.sendall(text)
447
self._setmode(relpath, mode)
411
self._setmode(relpath, mode)
449
413
except ftplib.error_perm, e:
450
# Check whether the command is not supported (reply code 502)
451
if str(e).startswith('502 '):
452
warning("FTP server does not support file appending natively. "
453
"Performance may be severely degraded! (%s)", e)
454
self._has_append = False
455
self._fallback_append(relpath, text, mode)
457
self._translate_ftp_error(e, abspath, extra='error appending',
458
unknown_exc=errors.NoSuchFile)
414
self._translate_perm_error(e, abspath, extra='error appending',
415
unknown_exc=errors.NoSuchFile)
459
416
except ftplib.error_temp, e:
460
417
if retries > _number_of_retries:
461
raise errors.TransportError(
462
"FTP temporary error during APPEND %s. Aborting."
463
% abspath, orig_error=e)
418
raise errors.TransportError("FTP temporary error during APPEND %s." \
419
"Aborting." % abspath, orig_error=e)
465
421
warning("FTP temporary error: %s. Retrying.", str(e))
466
422
self._reconnect()
467
423
self._try_append(relpath, text, mode, retries+1)
469
def _fallback_append(self, relpath, text, mode = None):
470
remote = self.get(relpath)
471
remote.seek(0, os.SEEK_END)
474
return self.put_file(relpath, remote, mode)
476
425
def _setmode(self, relpath, mode):
477
426
"""Set permissions on a path.
479
428
Only set permissions if the FTP server supports the 'SITE CHMOD'
484
mutter("FTP site chmod: setting permissions to %s on %s",
485
oct(mode), self._remote_path(relpath))
486
ftp = self._get_FTP()
487
cmd = "SITE CHMOD %s %s" % (oct(mode),
488
self._remote_path(relpath))
490
except ftplib.error_perm, e:
491
# Command probably not available on this server
492
warning("FTP Could not set permissions to %s on %s. %s",
493
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))
495
442
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
496
443
# to copy something to another machine. And you may be able
507
454
def _rename(self, abs_from, abs_to, f):
509
456
f.rename(abs_from, abs_to)
510
except (ftplib.error_temp, ftplib.error_perm), e:
511
self._translate_ftp_error(e, abs_from,
457
except ftplib.error_perm, e:
458
self._translate_perm_error(e, abs_from,
512
459
': unable to rename to %r' % (abs_to))
514
461
def move(self, rel_from, rel_to):
520
467
f = self._get_FTP()
521
468
self._rename_and_overwrite(abs_from, abs_to, f)
522
469
except ftplib.error_perm, e:
523
self._translate_ftp_error(e, abs_from,
524
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,),
525
472
unknown_exc=errors.PathError)
527
474
def _rename_and_overwrite(self, abs_from, abs_to, f):
562
509
mutter("FTP nlst: %s", basepath)
563
510
f = self._get_FTP()
566
paths = f.nlst(basepath)
567
except ftplib.error_perm, e:
568
self._translate_ftp_error(e, relpath,
569
extra='error with list_dir')
570
except ftplib.error_temp, e:
571
# xs4all's ftp server raises a 450 temp error when listing an
572
# empty directory. Check for that and just return an empty list
573
# in that case. See bug #215522
574
if str(e).lower().startswith('450 no files found'):
575
mutter('FTP Server returned "%s" for nlst.'
576
' Assuming it means empty directory',
581
# 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')
585
515
# If FTP.nlst returns paths prefixed by relpath, strip 'em
586
516
if paths and paths[0].startswith(basepath):
587
517
entries = [path[len(basepath)+1:] for path in paths]
641
571
def get_test_permutations():
642
572
"""Return the permutations to be used in testing."""
643
from bzrlib.tests import ftp_server
644
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. We raise UnavailableFeature from methods before
580
# the test server is being used. Doing so in the setUp method has bad
581
# side-effects (tearDown is never called).
582
class UnavailableFTPServer(object):
591
raise tests.UnavailableFeature(tests.FTPServerFeature)
593
def get_bogus_url(self):
594
raise tests.UnavailableFeature(tests.FTPServerFeature)
596
return [(FtpTransport, UnavailableFTPServer)]