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
193
mutter("FTP get: %s" % self._abspath(relpath))
196
f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
199
except ftplib.error_perm, e:
200
raise NoSuchFile(self.abspath(relpath), extra=str(e))
201
except ftplib.error_temp, e:
202
if retries > _number_of_retries:
203
raise TransportError(msg="FTP temporary error during GET %s. Aborting."
204
% self.abspath(relpath),
207
warning("FTP temporary error: %s. Retrying." % str(e))
208
self._FTP_instance = None
209
return self.get(relpath, decode, retries+1)
211
if retries > _number_of_retries:
212
raise TransportError("FTP control connection closed during GET %s."
213
% self.abspath(relpath),
216
warning("FTP control connection closed. Trying to reopen.")
217
time.sleep(_sleep_between_retries)
218
self._FTP_instance = None
219
return self.get(relpath, decode, retries+1)
221
def put(self, relpath, fp, mode=None, retries=0):
222
"""Copy the file-like or string object into the location.
224
:param relpath: Location to put the contents, relative to base.
225
:param fp: File-like or string object.
226
:param retries: Number of retries after temporary failures so far
229
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
231
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (self._abspath(relpath), time.time(),
232
os.getpid(), random.randint(0,0x7FFFFFFF))
233
if not hasattr(fp, 'read'):
236
mutter("FTP put: %s" % self._abspath(relpath))
239
f.storbinary('STOR '+tmp_abspath, fp)
240
f.rename(tmp_abspath, self._abspath(relpath))
241
except (ftplib.error_temp,EOFError), e:
242
warning("Failure during ftp PUT. Deleting temporary file.")
244
f.delete(tmp_abspath)
246
warning("Failed to delete temporary file on the server.\nFile: %s"
250
except ftplib.error_perm, e:
251
if "no such file" in str(e).lower():
252
raise NoSuchFile("Error storing %s: %s"
253
% (self.abspath(relpath), str(e)), extra=e)
255
raise FtpTransportError(orig_error=e)
256
except ftplib.error_temp, e:
257
if retries > _number_of_retries:
258
raise TransportError("FTP temporary error during PUT %s. Aborting."
259
% self.abspath(relpath), orig_error=e)
261
warning("FTP temporary error: %s. Retrying." % str(e))
262
self._FTP_instance = None
263
self.put(relpath, fp, mode, retries+1)
265
if retries > _number_of_retries:
266
raise TransportError("FTP control connection closed during PUT %s."
267
% self.abspath(relpath), orig_error=e)
269
warning("FTP control connection closed. Trying to reopen.")
270
time.sleep(_sleep_between_retries)
271
self._FTP_instance = None
272
self.put(relpath, fp, mode, retries+1)
275
def mkdir(self, relpath, mode=None):
276
"""Create a directory at the given path."""
278
mutter("FTP mkd: %s" % self._abspath(relpath))
281
f.mkd(self._abspath(relpath))
282
except ftplib.error_perm, e:
284
if 'File exists' in s:
285
raise FileExists(self.abspath(relpath), extra=s)
288
except ftplib.error_perm, e:
289
raise TransportError(orig_error=e)
291
def append(self, relpath, f):
292
"""Append the text in the file-like object into the final
295
raise TransportNotPossible('ftp does not support append()')
297
def copy(self, rel_from, rel_to):
298
"""Copy the item at rel_from to the location at rel_to"""
299
raise TransportNotPossible('ftp does not (yet) support copy()')
301
def move(self, rel_from, rel_to):
302
"""Move the item at rel_from to the location at rel_to"""
304
mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
305
self._abspath(rel_to)))
307
f.rename(self._abspath(rel_from), self._abspath(rel_to))
308
except ftplib.error_perm, e:
309
raise TransportError(orig_error=e)
311
def delete(self, relpath):
312
"""Delete the item at relpath"""
314
mutter("FTP rm: %s" % self._abspath(relpath))
316
f.delete(self._abspath(relpath))
317
except ftplib.error_perm, e:
318
raise TransportError(orig_error=e)
321
"""See Transport.listable."""
324
def list_dir(self, relpath):
325
"""See Transport.list_dir."""
327
mutter("FTP nlst: %s" % self._abspath(relpath))
329
basepath = self._abspath(relpath)
330
# FTP.nlst returns paths prefixed by relpath, strip 'em
331
the_list = f.nlst(basepath)
332
stripped = [path[len(basepath)+1:] for path in the_list]
333
# Remove . and .. if present, and return
334
return [path for path in stripped if path not in (".", "..")]
335
except ftplib.error_perm, e:
336
raise TransportError(orig_error=e)
338
def iter_files_recursive(self):
339
"""See Transport.iter_files_recursive.
341
This is cargo-culted from the SFTP transport"""
342
mutter("FTP iter_files_recursive")
343
queue = list(self.list_dir("."))
345
relpath = urllib.quote(queue.pop(0))
346
st = self.stat(relpath)
347
if stat.S_ISDIR(st.st_mode):
348
for i, basename in enumerate(self.list_dir(relpath)):
349
queue.insert(i, relpath+"/"+basename)
353
def stat(self, relpath):
354
"""Return the stat information for a file.
357
mutter("FTP stat: %s" % self._abspath(relpath))
359
return FtpStatResult(f, self._abspath(relpath))
360
except ftplib.error_perm, e:
361
if "no such file" in str(e).lower():
362
raise NoSuchFile("Error storing %s: %s"
363
% (self.abspath(relpath), str(e)), extra=e)
365
raise FtpTransportError(orig_error=e)
367
def lock_read(self, relpath):
368
"""Lock the given file for shared (read) access.
369
:return: A lock object, which should be passed to Transport.unlock()
371
# The old RemoteBranch ignore lock for reading, so we will
372
# continue that tradition and return a bogus lock object.
373
class BogusLock(object):
374
def __init__(self, path):
378
return BogusLock(relpath)
380
def lock_write(self, relpath):
381
"""Lock the given file for exclusive (write) access.
382
WARNING: many transports do not support this, so trying avoid using it
384
:return: A lock object, which should be passed to Transport.unlock()
386
return self.lock_read(relpath)
389
def get_test_permutations():
390
"""Return the permutations to be used in testing."""
391
warn("There are no FTP transport provider tests yet.")