1
# Copyright (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16
"""Implementation of Transport over ftp.
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
19
cargo-culting from the sftp transport and the http transport.
21
It provides the ftp:// and aftp:// protocols where ftp:// is passive ftp
22
and aftp:// is active ftp. Most people will want passive ftp for traversing
23
NAT and other firewalls, so it's best to use it unless you explicitly want
24
active, in which case aftp:// will be your friend.
27
from cStringIO import StringIO
36
from warnings import warn
39
from bzrlib.transport import Transport
40
from bzrlib.errors import (TransportNotPossible, TransportError,
41
NoSuchFile, FileExists)
42
from bzrlib.trace import mutter, warning
46
def _find_FTP(hostname, username, password, is_active):
47
"""Find an ftplib.FTP instance attached to this triplet."""
48
key = "%s|%s|%s|%s" % (hostname, username, password, is_active)
49
if key not in _FTP_cache:
50
mutter("Constructing FTP instance against %r" % key)
51
_FTP_cache[key] = ftplib.FTP(hostname, username, password)
52
_FTP_cache[key].set_pasv(not is_active)
53
return _FTP_cache[key]
56
class FtpTransportError(TransportError):
60
class FtpStatResult(object):
61
def __init__(self, f, relpath):
63
self.st_size = f.size(relpath)
64
self.st_mode = stat.S_IFREG
65
except ftplib.error_perm:
69
self.st_mode = stat.S_IFDIR
74
_number_of_retries = 2
75
_sleep_between_retries = 5
77
class FtpTransport(Transport):
78
"""This is the transport agent for ftp:// access."""
80
def __init__(self, base, _provided_instance=None):
81
"""Set the base path where files will be stored."""
82
assert base.startswith('ftp://') or base.startswith('aftp://')
83
super(FtpTransport, self).__init__(base)
84
self.is_active = base.startswith('aftp://')
87
(self._proto, self._host,
88
self._path, self._parameters,
89
self._query, self._fragment) = urlparse.urlparse(self.base)
90
self._FTP_instance = _provided_instance
93
"""Return the ftplib.FTP instance for this object."""
94
if self._FTP_instance is not None:
95
return self._FTP_instance
100
hostname = self._host
102
username, hostname = hostname.split("@", 1)
104
username, password = username.split(":", 1)
106
self._FTP_instance = _find_FTP(hostname, username, password,
108
return self._FTP_instance
109
except ftplib.error_perm, e:
110
raise TransportError(msg="Error setting up connection: %s"
111
% str(e), orig_error=e)
113
def should_cache(self):
114
"""Return True if the data pulled across should be cached locally.
118
def clone(self, offset=None):
119
"""Return a new FtpTransport with root at self.base + offset.
123
return FtpTransport(self.base, self._FTP_instance)
125
return FtpTransport(self.abspath(offset), self._FTP_instance)
127
def _abspath(self, relpath):
128
assert isinstance(relpath, basestring)
129
relpath = urllib.unquote(relpath)
130
if isinstance(relpath, basestring):
131
relpath_parts = relpath.split('/')
133
# TODO: Don't call this with an array - no magic interfaces
134
relpath_parts = relpath[:]
135
if len(relpath_parts) > 1:
136
if relpath_parts[0] == '':
137
raise ValueError("path %r within branch %r seems to be absolute"
138
% (relpath, self._path))
139
basepath = self._path.split('/')
140
if len(basepath) > 0 and basepath[-1] == '':
141
basepath = basepath[:-1]
142
for p in relpath_parts:
144
if len(basepath) == 0:
145
# In most filesystems, a request for the parent
146
# of root, just returns root.
149
elif p == '.' or p == '':
153
# Possibly, we could use urlparse.urljoin() here, but
154
# I'm concerned about when it chooses to strip the last
155
# portion of the path, and when it doesn't.
156
return '/'.join(basepath)
158
def abspath(self, relpath):
159
"""Return the full url to the given relative path.
160
This can be supplied with a string or a list
162
path = self._abspath(relpath)
163
return urlparse.urlunparse((self._proto,
164
self._host, path, '', '', ''))
166
def has(self, relpath):
167
"""Does the target location exist?
169
XXX: I assume we're never asked has(dirname) and thus I use
170
the FTP size command and assume that if it doesn't raise,
175
s = f.size(self._abspath(relpath))
176
mutter("FTP has: %s" % self._abspath(relpath))
178
except ftplib.error_perm:
179
mutter("FTP has not: %s" % self._abspath(relpath))
182
def get(self, relpath, decode=False, retries=0):
183
"""Get the file at the given relative path.
185
:param relpath: The relative path to the file
186
:param retries: Number of retries after temporary failures so far
189
We're meant to return a file-like object which bzr will
190
then read from. For now we do this via the magic of StringIO
192
# TODO: decode should be deprecated
194
mutter("FTP get: %s" % self._abspath(relpath))
197
f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
200
except ftplib.error_perm, e:
201
raise NoSuchFile(self.abspath(relpath), extra=str(e))
202
except ftplib.error_temp, e:
203
if retries > _number_of_retries:
204
raise TransportError(msg="FTP temporary error during GET %s. Aborting."
205
% self.abspath(relpath),
208
warning("FTP temporary error: %s. Retrying." % str(e))
209
self._FTP_instance = None
210
return self.get(relpath, decode, retries+1)
212
if retries > _number_of_retries:
213
raise TransportError("FTP control connection closed during GET %s."
214
% self.abspath(relpath),
217
warning("FTP control connection closed. Trying to reopen.")
218
time.sleep(_sleep_between_retries)
219
self._FTP_instance = None
220
return self.get(relpath, decode, retries+1)
222
def put(self, relpath, fp, mode=None, retries=0):
223
"""Copy the file-like or string object into the location.
225
:param relpath: Location to put the contents, relative to base.
226
:param fp: File-like or string object.
227
:param retries: Number of retries after temporary failures so far
230
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
232
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (self._abspath(relpath), time.time(),
233
os.getpid(), random.randint(0,0x7FFFFFFF))
234
if not hasattr(fp, 'read'):
237
mutter("FTP put: %s" % self._abspath(relpath))
240
f.storbinary('STOR '+tmp_abspath, fp)
241
f.rename(tmp_abspath, self._abspath(relpath))
242
except (ftplib.error_temp,EOFError), e:
243
warning("Failure during ftp PUT. Deleting temporary file.")
245
f.delete(tmp_abspath)
247
warning("Failed to delete temporary file on the server.\nFile: %s"
251
except ftplib.error_perm, e:
252
if "no such file" in str(e).lower():
253
raise NoSuchFile("Error storing %s: %s"
254
% (self.abspath(relpath), str(e)), extra=e)
256
raise FtpTransportError(orig_error=e)
257
except ftplib.error_temp, e:
258
if retries > _number_of_retries:
259
raise TransportError("FTP temporary error during PUT %s. Aborting."
260
% self.abspath(relpath), orig_error=e)
262
warning("FTP temporary error: %s. Retrying." % str(e))
263
self._FTP_instance = None
264
self.put(relpath, fp, mode, retries+1)
266
if retries > _number_of_retries:
267
raise TransportError("FTP control connection closed during PUT %s."
268
% self.abspath(relpath), orig_error=e)
270
warning("FTP control connection closed. Trying to reopen.")
271
time.sleep(_sleep_between_retries)
272
self._FTP_instance = None
273
self.put(relpath, fp, mode, retries+1)
276
def mkdir(self, relpath, mode=None):
277
"""Create a directory at the given path."""
279
mutter("FTP mkd: %s" % self._abspath(relpath))
282
f.mkd(self._abspath(relpath))
283
except ftplib.error_perm, e:
285
if 'File exists' in s:
286
raise FileExists(self.abspath(relpath), extra=s)
289
except ftplib.error_perm, e:
290
raise TransportError(orig_error=e)
292
def append(self, relpath, f):
293
"""Append the text in the file-like object into the final
296
raise TransportNotPossible('ftp does not support append()')
298
def copy(self, rel_from, rel_to):
299
"""Copy the item at rel_from to the location at rel_to"""
300
raise TransportNotPossible('ftp does not (yet) support copy()')
302
def move(self, rel_from, rel_to):
303
"""Move the item at rel_from to the location at rel_to"""
305
mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
306
self._abspath(rel_to)))
308
f.rename(self._abspath(rel_from), self._abspath(rel_to))
309
except ftplib.error_perm, e:
310
raise TransportError(orig_error=e)
312
def delete(self, relpath):
313
"""Delete the item at relpath"""
315
mutter("FTP rm: %s" % self._abspath(relpath))
317
f.delete(self._abspath(relpath))
318
except ftplib.error_perm, e:
319
raise TransportError(orig_error=e)
322
"""See Transport.listable."""
325
def list_dir(self, relpath):
326
"""See Transport.list_dir."""
328
mutter("FTP nlst: %s" % self._abspath(relpath))
330
basepath = self._abspath(relpath)
331
# FTP.nlst returns paths prefixed by relpath, strip 'em
332
the_list = f.nlst(basepath)
333
stripped = [path[len(basepath)+1:] for path in the_list]
334
# Remove . and .. if present, and return
335
return [path for path in stripped if path not in (".", "..")]
336
except ftplib.error_perm, e:
337
raise TransportError(orig_error=e)
339
def iter_files_recursive(self):
340
"""See Transport.iter_files_recursive.
342
This is cargo-culted from the SFTP transport"""
343
mutter("FTP iter_files_recursive")
344
queue = list(self.list_dir("."))
346
relpath = urllib.quote(queue.pop(0))
347
st = self.stat(relpath)
348
if stat.S_ISDIR(st.st_mode):
349
for i, basename in enumerate(self.list_dir(relpath)):
350
queue.insert(i, relpath+"/"+basename)
354
def stat(self, relpath):
355
"""Return the stat information for a file.
358
mutter("FTP stat: %s" % self._abspath(relpath))
360
return FtpStatResult(f, self._abspath(relpath))
361
except ftplib.error_perm, e:
362
if "no such file" in str(e).lower():
363
raise NoSuchFile("Error storing %s: %s"
364
% (self.abspath(relpath), str(e)), extra=e)
366
raise FtpTransportError(orig_error=e)
368
def lock_read(self, relpath):
369
"""Lock the given file for shared (read) access.
370
:return: A lock object, which should be passed to Transport.unlock()
372
# The old RemoteBranch ignore lock for reading, so we will
373
# continue that tradition and return a bogus lock object.
374
class BogusLock(object):
375
def __init__(self, path):
379
return BogusLock(relpath)
381
def lock_write(self, relpath):
382
"""Lock the given file for exclusive (write) access.
383
WARNING: many transports do not support this, so trying avoid using it
385
:return: A lock object, which should be passed to Transport.unlock()
387
return self.lock_read(relpath)
390
def get_test_permutations():
391
"""Return the permutations to be used in testing."""
392
warn("There are no FTP transport provider tests yet.")