1
# Copyright (C) 2005-2010 Canonical Ltd
1
# Copyright (C) 2005, 2006, 2007, 2008, 2009 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
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
16
"""Implementation of Transport over ftp.
19
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
100
99
self.is_active = True
102
101
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.
103
# Most modern FTP servers support the APPE command. If ours doesn't, we (re)set this flag accordingly later.
106
104
self._has_append = True
108
106
def _get_FTP(self):
115
113
self._set_connection(connection, credentials)
116
114
return connection
118
connection_class = ftplib.FTP
120
116
def _create_connection(self, credentials=None):
121
117
"""Create a new connection with the provided credentials.
142
138
((self._host, self._port, user, '********',
143
139
self.is_active),))
145
connection = self.connection_class()
141
connection = ftplib.FTP()
146
142
connection.connect(host=self._host, port=self._port)
147
self._login(connection, auth, user, password)
143
if user and user != 'anonymous' and \
144
password is None: # '' is a valid password
145
password = auth.get_password('ftp', self._host, user,
147
connection.login(user=user, passwd=password)
148
148
connection.set_pasv(not self.is_active)
149
149
# binary mode is the default
150
150
connection.voidcmd('TYPE I')
157
157
" %s" % str(e), orig_error=e)
158
158
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
160
def _reconnect(self):
168
161
"""Create a new connection with the previously used credentials"""
169
162
credentials = self._get_credentials()
170
163
connection, credentials = self._create_connection(credentials)
171
164
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,
166
def _translate_perm_error(self, err, path, extra=None,
179
167
unknown_exc=FtpPathError):
180
"""Try to translate an ftplib exception to a bzrlib exception.
168
"""Try to translate an ftplib.error_perm exception.
182
170
:param err: The error to translate into a bzr error
183
171
:param path: The path which had problems
185
173
:param unknown_exc: If None, we will just raise the original exception
186
174
otherwise we raise unknown_exc(path, extra=extra)
188
# ftp error numbers are very generic, like "451: Requested action aborted,
189
# local error in processing" so unfortunately we have to match by
191
176
s = str(err).lower()
202
187
or 'file/directory not found' in s # filezilla server
203
188
# Microsoft FTP-Service RNFR reply if file not found
204
189
or (s.startswith('550 ') and 'unable to rename to' in extra)
205
# if containing directory doesn't exist, suggested by
206
# <https://bugs.edge.launchpad.net/bzr/+bug/224373>
207
or (s.startswith('550 ') and "can't find folder" in s)
209
191
raise errors.NoSuchFile(path, extra=extra)
210
elif ('file exists' in s):
192
if ('file exists' in s):
211
193
raise errors.FileExists(path, extra=extra)
212
elif ('not a directory' in s):
194
if ('not a directory' in s):
213
195
raise errors.PathError(path, extra=extra)
214
elif 'directory not empty' in s:
215
raise errors.DirectoryNotEmpty(path, extra=extra)
217
197
mutter('unable to understand error for path: %s: %s', path, err)
225
205
#raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
208
def _remote_path(self, relpath):
209
# XXX: It seems that ftplib does not handle Unicode paths
210
# at the same time, medusa won't handle utf8 paths So if
211
# we .encode(utf8) here (see ConnectedTransport
212
# implementation), then we get a Server failure. while
213
# if we use str(), we get a UnicodeError, and the test
214
# suite just skips testing UnicodePaths.
215
relative = str(urlutils.unescape(relpath))
216
remote_path = self._combine_paths(self._path, relative)
228
219
def has(self, relpath):
229
220
"""Does the target location exist?"""
230
221
# FIXME jam 20060516 We *do* ask about directories in the test suite
246
237
mutter("FTP has not: %s: %s", abspath, e)
249
def get(self, relpath, decode=DEPRECATED_PARAMETER, retries=0):
240
def get(self, relpath, decode=False, retries=0):
250
241
"""Get the file at the given relative path.
252
243
:param relpath: The relative path to the file
256
247
We're meant to return a file-like object which bzr will
257
248
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)
250
# TODO: decode should be deprecated
264
252
mutter("FTP get: %s", self._remote_path(relpath))
265
253
f = self._get_FTP()
331
319
return len(bytes)
333
321
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, ))
322
except (ftplib.error_temp,EOFError), e:
323
warning("Failure during ftp PUT. Deleting temporary file.")
338
325
f.delete(tmp_abspath)
344
331
except ftplib.error_perm, e:
345
self._translate_ftp_error(e, abspath, extra='could not store',
332
self._translate_perm_error(e, abspath, extra='could not store',
346
333
unknown_exc=errors.NoSuchFile)
347
334
except ftplib.error_temp, e:
348
335
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)
336
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
337
% self.abspath(relpath), orig_error=e)
353
339
warning("FTP temporary error: %s. Retrying.", str(e))
354
340
self._reconnect()
370
356
mutter("FTP mkd: %s", abspath)
371
357
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
359
self._setmode(relpath, mode)
384
360
except ftplib.error_perm, e:
385
self._translate_ftp_error(e, abspath,
361
self._translate_perm_error(e, abspath,
386
362
unknown_exc=errors.FileExists)
388
364
def open_write_stream(self, relpath, mode=None):
408
384
f = self._get_FTP()
410
386
except ftplib.error_perm, e:
411
self._translate_ftp_error(e, abspath, unknown_exc=errors.PathError)
387
self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
413
389
def append_file(self, relpath, f, mode=None):
414
390
"""Append the text in the file-like object into the final
418
395
abspath = self._remote_path(relpath)
419
396
if self.has(relpath):
420
397
ftp = self._get_FTP()
449
426
except ftplib.error_perm, e:
450
427
# Check whether the command is not supported (reply code 502)
451
428
if str(e).startswith('502 '):
452
warning("FTP server does not support file appending natively. "
453
"Performance may be severely degraded! (%s)", e)
429
warning("FTP server does not support file appending natively. " \
430
"Performance may be severely degraded! (%s)", e)
454
431
self._has_append = False
455
432
self._fallback_append(relpath, text, mode)
457
self._translate_ftp_error(e, abspath, extra='error appending',
434
self._translate_perm_error(e, abspath, extra='error appending',
458
435
unknown_exc=errors.NoSuchFile)
459
437
except ftplib.error_temp, e:
460
438
if retries > _number_of_retries:
461
raise errors.TransportError(
462
"FTP temporary error during APPEND %s. Aborting."
463
% abspath, orig_error=e)
439
raise errors.TransportError("FTP temporary error during APPEND %s." \
440
"Aborting." % abspath, orig_error=e)
465
442
warning("FTP temporary error: %s. Retrying.", str(e))
466
443
self._reconnect()
469
446
def _fallback_append(self, relpath, text, mode = None):
470
447
remote = self.get(relpath)
471
remote.seek(0, os.SEEK_END)
472
449
remote.write(text)
474
451
return self.put_file(relpath, remote, mode)
476
453
def _setmode(self, relpath, mode):
507
484
def _rename(self, abs_from, abs_to, f):
509
486
f.rename(abs_from, abs_to)
510
except (ftplib.error_temp, ftplib.error_perm), e:
511
self._translate_ftp_error(e, abs_from,
487
except ftplib.error_perm, e:
488
self._translate_perm_error(e, abs_from,
512
489
': unable to rename to %r' % (abs_to))
514
491
def move(self, rel_from, rel_to):
520
497
f = self._get_FTP()
521
498
self._rename_and_overwrite(abs_from, abs_to, f)
522
499
except ftplib.error_perm, e:
523
self._translate_ftp_error(e, abs_from,
500
self._translate_perm_error(e, abs_from,
524
501
extra='unable to rename to %r' % (rel_to,),
525
502
unknown_exc=errors.PathError)
544
521
mutter("FTP rm: %s", abspath)
545
522
f.delete(abspath)
546
523
except ftplib.error_perm, e:
547
self._translate_ftp_error(e, abspath, 'error deleting',
524
self._translate_perm_error(e, abspath, 'error deleting',
548
525
unknown_exc=errors.NoSuchFile)
550
527
def external_url(self):
566
543
paths = f.nlst(basepath)
567
544
except ftplib.error_perm, e:
568
self._translate_ftp_error(e, relpath,
545
self._translate_perm_error(e, relpath,
569
546
extra='error with list_dir')
570
547
except ftplib.error_temp, e:
571
548
# xs4all's ftp server raises a 450 temp error when listing an
614
591
f = self._get_FTP()
615
592
return FtpStatResult(f, abspath)
616
593
except ftplib.error_perm, e:
617
self._translate_ftp_error(e, abspath, extra='error w/ stat')
594
self._translate_perm_error(e, abspath, extra='error w/ stat')
619
596
def lock_read(self, relpath):
620
597
"""Lock the given file for shared (read) access.