53
61
"""FTP failed for path: %(path)s%(extra)s"""
57
def _find_FTP(hostname, port, username, password, is_active):
58
"""Find an ftplib.FTP instance attached to this triplet."""
59
key = (hostname, port, username, password, is_active)
60
alt_key = (hostname, port, username, '********', is_active)
61
if key not in _FTP_cache:
62
mutter("Constructing FTP instance against %r" % (alt_key,))
65
conn.connect(host=hostname, port=port)
66
if username and username != 'anonymous' and not password:
67
password = bzrlib.ui.ui_factory.get_password(
68
prompt='FTP %(user)s@%(host)s password',
69
user=username, host=hostname)
70
conn.login(user=username, passwd=password)
71
conn.set_pasv(not is_active)
73
_FTP_cache[key] = conn
75
return _FTP_cache[key]
78
64
class FtpStatResult(object):
79
65
def __init__(self, f, relpath):
92
78
_number_of_retries = 2
93
79
_sleep_between_retries = 5
95
class FtpTransport(Transport):
81
# FIXME: there are inconsistencies in the way temporary errors are
82
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
83
# be taken to analyze the implications for write operations (read operations
84
# are safe to retry). Overall even some read operations are never
85
# retried. --vila 20070720 (Bug #127164)
86
class FtpTransport(ConnectedTransport):
96
87
"""This is the transport agent for ftp:// access."""
98
def __init__(self, base, _provided_instance=None):
89
def __init__(self, base, _from_transport=None):
99
90
"""Set the base path where files will be stored."""
100
91
assert base.startswith('ftp://') or base.startswith('aftp://')
102
self.is_active = base.startswith('aftp://')
104
# urlparse won't handle aftp://
106
if not base.endswith('/'):
108
(self._proto, self._username,
109
self._password, self._host,
110
self._port, self._path) = split_url(base)
111
base = self._unparse_url()
113
super(FtpTransport, self).__init__(base)
114
self._FTP_instance = _provided_instance
116
def _unparse_url(self, path=None):
119
path = urllib.quote(path)
120
netloc = urllib.quote(self._host)
121
if self._username is not None:
122
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
123
if self._port is not None:
124
netloc = '%s:%d' % (netloc, self._port)
125
return urlparse.urlunparse(('ftp', netloc, path, '', '', ''))
92
super(FtpTransport, self).__init__(base,
93
_from_transport=_from_transport)
94
self._unqualified_scheme = 'ftp'
95
if self._scheme == 'aftp':
98
self.is_active = False
127
100
def _get_FTP(self):
128
101
"""Return the ftplib.FTP instance for this object."""
129
if self._FTP_instance is not None:
130
return self._FTP_instance
102
# Ensures that a connection is established
103
connection = self._get_connection()
104
if connection is None:
105
# First connection ever
106
connection, credentials = self._create_connection()
107
self._set_connection(connection, credentials)
110
def _create_connection(self, credentials=None):
111
"""Create a new connection with the provided credentials.
113
:param credentials: The credentials needed to establish the connection.
115
:return: The created connection and its associated credentials.
117
The credentials are only the password as it may have been entered
118
interactively by the user and may be different from the one provided
119
in base url at transport creation time.
121
if credentials is None:
122
password = self._password
124
password = credentials
126
mutter("Constructing FTP instance against %r" %
127
((self._host, self._port, self._user, '********',
133
self._FTP_instance = _find_FTP(self._host, self._port,
134
self._username, self._password,
136
return self._FTP_instance
130
connection = ftplib.FTP()
131
connection.connect(host=self._host, port=self._port)
132
if self._user and self._user != 'anonymous' and \
133
password is None: # '' is a valid password
134
get_password = bzrlib.ui.ui_factory.get_password
135
password = get_password(prompt='FTP %(user)s@%(host)s password',
136
user=self._user, host=self._host)
137
connection.login(user=self._user, passwd=password)
138
connection.set_pasv(not self.is_active)
137
139
except ftplib.error_perm, e:
138
raise errors.TransportError(msg="Error setting up connection: %s"
139
% str(e), orig_error=e)
141
def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
140
raise errors.TransportError(msg="Error setting up connection:"
141
" %s" % str(e), orig_error=e)
142
return connection, password
144
def _reconnect(self):
145
"""Create a new connection with the previously used credentials"""
146
credentials = self._get_credentials()
147
connection, credentials = self._create_connection(credentials)
148
self._set_connection(connection, credentials)
150
def _translate_perm_error(self, err, path, extra=None,
151
unknown_exc=FtpPathError):
142
152
"""Try to translate an ftplib.error_perm exception.
144
154
:param err: The error to translate into a bzr error
173
186
#raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
176
def should_cache(self):
177
"""Return True if the data pulled across should be cached locally.
181
def clone(self, offset=None):
182
"""Return a new FtpTransport with root at self.base + offset.
186
return FtpTransport(self.base, self._FTP_instance)
188
return FtpTransport(self.abspath(offset), self._FTP_instance)
190
def _abspath(self, relpath):
191
assert isinstance(relpath, basestring)
192
relpath = urllib.unquote(relpath)
193
relpath_parts = relpath.split('/')
194
if len(relpath_parts) > 1:
195
if relpath_parts[0] == '':
196
raise ValueError("path %r within branch %r seems to be absolute"
197
% (relpath, self._path))
198
basepath = self._path.split('/')
199
if len(basepath) > 0 and basepath[-1] == '':
200
basepath = basepath[:-1]
201
for p in relpath_parts:
203
if len(basepath) == 0:
204
# In most filesystems, a request for the parent
205
# of root, just returns root.
208
elif p == '.' or p == '':
212
# Possibly, we could use urlparse.urljoin() here, but
213
# I'm concerned about when it chooses to strip the last
214
# portion of the path, and when it doesn't.
215
return '/'.join(basepath) or '/'
217
def abspath(self, relpath):
218
"""Return the full url to the given relative path.
219
This can be supplied with a string or a list
221
path = self._abspath(relpath)
222
return self._unparse_url(path)
189
def _remote_path(self, relpath):
190
# XXX: It seems that ftplib does not handle Unicode paths
191
# at the same time, medusa won't handle utf8 paths So if
192
# we .encode(utf8) here (see ConnectedTransport
193
# implementation), then we get a Server failure. while
194
# if we use str(), we get a UnicodeError, and the test
195
# suite just skips testing UnicodePaths.
196
relative = str(urlutils.unescape(relpath))
197
remote_path = self._combine_paths(self._path, relative)
224
200
def has(self, relpath):
225
201
"""Does the target location exist?"""
290
266
:param retries: Number of retries after temporary failures so far
291
267
for this operation.
293
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
269
TODO: jam 20051215 ftp as a protocol seems to support chmod, but
295
abspath = self._abspath(relpath)
272
abspath = self._remote_path(relpath)
296
273
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
297
274
os.getpid(), random.randint(0,0x7FFFFFFF))
298
if not hasattr(fp, 'read'):
276
if getattr(fp, 'read', None) is None:
277
# hand in a string IO
281
# capture the byte count; .read() may be read only so
283
class byte_counter(object):
284
def __init__(self, fp):
286
self.counted_bytes = 0
287
def read(self, count):
288
result = self.fp.read(count)
289
self.counted_bytes += len(result)
291
fp = byte_counter(fp)
301
293
mutter("FTP put: %s", abspath)
302
294
f = self._get_FTP()
304
296
f.storbinary('STOR '+tmp_abspath, fp)
305
f.rename(tmp_abspath, abspath)
297
self._rename_and_overwrite(tmp_abspath, abspath, f)
298
if bytes is not None:
301
return fp.counted_bytes
306
302
except (ftplib.error_temp,EOFError), e:
307
303
warning("Failure during ftp PUT. Deleting temporary file.")
315
311
except ftplib.error_perm, e:
316
self._translate_perm_error(e, abspath, extra='could not store')
312
self._translate_perm_error(e, abspath, extra='could not store',
313
unknown_exc=errors.NoSuchFile)
317
314
except ftplib.error_temp, e:
318
315
if retries > _number_of_retries:
319
316
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
320
317
% self.abspath(relpath), orig_error=e)
322
319
warning("FTP temporary error: %s. Retrying.", str(e))
323
self._FTP_instance = None
324
self.put(relpath, fp, mode, retries+1)
321
self.put_file(relpath, fp, mode, retries+1)
326
323
if retries > _number_of_retries:
327
324
raise errors.TransportError("FTP control connection closed during PUT %s."
343
340
self._translate_perm_error(e, abspath,
344
341
unknown_exc=errors.FileExists)
343
def open_write_stream(self, relpath, mode=None):
344
"""See Transport.open_write_stream."""
345
self.put_bytes(relpath, "", mode)
346
result = AppendBasedFileStream(self, relpath)
347
_file_streams[self.abspath(relpath)] = result
350
def recommended_page_size(self):
351
"""See Transport.recommended_page_size().
353
For FTP we suggest a large page size to reduce the overhead
354
introduced by latency.
346
358
def rmdir(self, rel_path):
347
359
"""Delete the directory at rel_path"""
348
abspath = self._abspath(rel_path)
360
abspath = self._remote_path(rel_path)
350
362
mutter("FTP rmd: %s", abspath)
351
363
f = self._get_FTP()
409
421
mutter("FTP site chmod: setting permissions to %s on %s",
410
str(mode), self._abspath(relpath))
422
str(mode), self._remote_path(relpath))
411
423
ftp = self._get_FTP()
412
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
424
cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
414
426
except ftplib.error_perm, e:
415
427
# Command probably not available on this server
416
428
warning("FTP Could not set permissions to %s on %s. %s",
417
str(mode), self._abspath(relpath), str(e))
429
str(mode), self._remote_path(relpath), str(e))
419
431
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
420
432
# to copy something to another machine. And you may be able
421
433
# to give it its own address as the 'to' location.
422
434
# So implement a fancier 'copy()'
436
def rename(self, rel_from, rel_to):
437
abs_from = self._remote_path(rel_from)
438
abs_to = self._remote_path(rel_to)
439
mutter("FTP rename: %s => %s", abs_from, abs_to)
441
return self._rename(abs_from, abs_to, f)
443
def _rename(self, abs_from, abs_to, f):
445
f.rename(abs_from, abs_to)
446
except ftplib.error_perm, e:
447
self._translate_perm_error(e, abs_from,
448
': unable to rename to %r' % (abs_to))
424
450
def move(self, rel_from, rel_to):
425
451
"""Move the item at rel_from to the location at rel_to"""
426
abs_from = self._abspath(rel_from)
427
abs_to = self._abspath(rel_to)
452
abs_from = self._remote_path(rel_from)
453
abs_to = self._remote_path(rel_to)
429
455
mutter("FTP mv: %s => %s", abs_from, abs_to)
430
456
f = self._get_FTP()
431
f.rename(abs_from, abs_to)
457
self._rename_and_overwrite(abs_from, abs_to, f)
432
458
except ftplib.error_perm, e:
433
459
self._translate_perm_error(e, abs_from,
434
460
extra='unable to rename to %r' % (rel_to,),
435
461
unknown_exc=errors.PathError)
463
def _rename_and_overwrite(self, abs_from, abs_to, f):
464
"""Do a fancy rename on the remote server.
466
Using the implementation provided by osutils.
468
osutils.fancy_rename(abs_from, abs_to,
469
rename_func=lambda p1, p2: self._rename(p1, p2, f),
470
unlink_func=lambda p: self._delete(p, f))
439
472
def delete(self, relpath):
440
473
"""Delete the item at relpath"""
441
abspath = self._abspath(relpath)
474
abspath = self._remote_path(relpath)
476
self._delete(abspath, f)
478
def _delete(self, abspath, f):
443
480
mutter("FTP rm: %s", abspath)
445
481
f.delete(abspath)
446
482
except ftplib.error_perm, e:
447
483
self._translate_perm_error(e, abspath, 'error deleting',
448
484
unknown_exc=errors.NoSuchFile)
486
def external_url(self):
487
"""See bzrlib.transport.Transport.external_url."""
488
# FTP URL's are externally usable.
450
491
def listable(self):
451
492
"""See Transport.listable."""
454
495
def list_dir(self, relpath):
455
496
"""See Transport.list_dir."""
497
basepath = self._remote_path(relpath)
498
mutter("FTP nlst: %s", basepath)
457
mutter("FTP nlst: %s", self._abspath(relpath))
459
basepath = self._abspath(relpath)
460
501
paths = f.nlst(basepath)
461
# If FTP.nlst returns paths prefixed by relpath, strip 'em
462
if paths and paths[0].startswith(basepath):
463
paths = [path[len(basepath)+1:] for path in paths]
464
# Remove . and .. if present, and return
465
return [path for path in paths if path not in (".", "..")]
466
502
except ftplib.error_perm, e:
467
503
self._translate_perm_error(e, relpath, extra='error with list_dir')
504
# If FTP.nlst returns paths prefixed by relpath, strip 'em
505
if paths and paths[0].startswith(basepath):
506
entries = [path[len(basepath)+1:] for path in paths]
509
# Remove . and .. if present
510
return [urlutils.escape(entry) for entry in entries
511
if entry not in ('.', '..')]
469
513
def iter_files_recursive(self):
470
514
"""See Transport.iter_files_recursive.
552
598
self._port = self._ftp_server.getsockname()[1]
553
599
# Don't let it loop forever, or handle an infinite number of requests.
554
# In this case it will run for 100s, or 1000 requests
555
self._async_thread = threading.Thread(target=asyncore.loop,
556
kwargs={'timeout':0.1, 'count':1000})
600
# In this case it will run for 1000s, or 10000 requests
601
self._async_thread = threading.Thread(
602
target=FtpServer._asyncore_loop_ignore_EBADF,
603
kwargs={'timeout':0.1, 'count':10000})
557
604
self._async_thread.setDaemon(True)
558
605
self._async_thread.start()
560
607
def tearDown(self):
561
608
"""See bzrlib.transport.Server.tearDown."""
562
# have asyncore release the channel
563
self._ftp_server.del_channel()
609
self._ftp_server.close()
564
610
asyncore.close_all()
565
611
self._async_thread.join()
614
def _asyncore_loop_ignore_EBADF(*args, **kwargs):
615
"""Ignore EBADF during server shutdown.
617
We close the socket to get the server to shutdown, but this causes
618
select.select() to raise EBADF.
621
asyncore.loop(*args, **kwargs)
622
# FIXME: If we reach that point, we should raise an exception
623
# explaining that the 'count' parameter in setUp is too low or
624
# testers may wonder why their test just sits there waiting for a
625
# server that is already dead. Note that if the tester waits too
626
# long under pdb the server will also die.
627
except select.error, e:
628
if e.args[0] != errno.EBADF:
568
632
_ftp_channel = None
569
633
_ftp_server = None