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 bzrlib.transport import Transport
29
from bzrlib.errors import (TransportNotPossible, NoSuchFile,
30
NonRelativePath, TransportError, ConnectionError)
33
from cStringIO import StringIO
39
from bzrlib.errors import BzrError, BzrCheckError
40
from bzrlib.branch import Branch
41
from bzrlib.trace import mutter
44
class FtpTransportError(TransportError):
48
class FtpStatResult(object):
49
def __init__(self, f, relpath):
51
self.st_size = f.size(relpath)
52
self.st_mode = stat.S_IFREG
53
except ftplib.error_perm:
57
self.st_mode = stat.S_IFDIR
62
class FtpTransport(Transport):
63
"""This is the transport agent for ftp:// access."""
65
def __init__(self, base, _provided_instance=None):
66
"""Set the base path where files will be stored."""
67
assert base.startswith('ftp://') or base.startswith('aftp://')
68
super(FtpTransport, self).__init__(base)
69
self.is_active = base.startswith('aftp://')
72
(self._proto, self._host,
73
self._path, self._parameters,
74
self._query, self._fragment) = urlparse.urlparse(self.base)
75
self._FTP_instance = _provided_instance
79
"""Return the ftplib.FTP instance for this object."""
80
if self._FTP_instance is not None:
81
return self._FTP_instance
88
username, hostname = hostname.split("@", 1)
90
username, password = username.split(":", 1)
92
mutter("Constructing FTP instance")
93
self._FTP_instance = ftplib.FTP(hostname, username, password)
94
self._FTP_instance.set_pasv(not self.is_active)
95
return self._FTP_instance
96
except ftplib.error_perm, e:
97
raise FtpTransportError(msg="Error setting up connection: %s"
98
% str(e), orig_error=e)
100
def should_cache(self):
101
"""Return True if the data pulled across should be cached locally.
105
def clone(self, offset=None):
106
"""Return a new FtpTransport with root at self.base + offset.
110
return FtpTransport(self.base, self._FTP_instance)
112
return FtpTransport(self.abspath(offset), self._FTP_instance)
114
def _abspath(self, relpath):
115
assert isinstance(relpath, basestring)
116
relpath = urllib.unquote(relpath)
117
if isinstance(relpath, basestring):
118
relpath_parts = relpath.split('/')
120
# TODO: Don't call this with an array - no magic interfaces
121
relpath_parts = relpath[:]
122
if len(relpath_parts) > 1:
123
if relpath_parts[0] == '':
124
raise ValueError("path %r within branch %r seems to be absolute"
125
% (relpath, self._path))
126
basepath = self._path.split('/')
127
if len(basepath) > 0 and basepath[-1] == '':
128
basepath = basepath[:-1]
129
for p in relpath_parts:
131
if len(basepath) == 0:
132
# In most filesystems, a request for the parent
133
# of root, just returns root.
136
elif p == '.' or p == '':
140
# Possibly, we could use urlparse.urljoin() here, but
141
# I'm concerned about when it chooses to strip the last
142
# portion of the path, and when it doesn't.
143
return '/'.join(basepath)
145
def abspath(self, relpath):
146
"""Return the full url to the given relative path.
147
This can be supplied with a string or a list
149
path = self._abspath(relpath)
150
return urlparse.urlunparse((self._proto,
151
self._host, path, '', '', ''))
153
def has(self, relpath):
154
"""Does the target location exist?
156
XXX: I assume we're never asked has(dirname) and thus I use
157
the FTP size command and assume that if it doesn't raise,
162
s = f.size(self._abspath(relpath))
163
mutter("FTP has: %s" % self._abspath(relpath))
165
except ftplib.error_perm:
166
mutter("FTP has not: %s" % self._abspath(relpath))
169
def get(self, relpath, decode=False):
170
"""Get the file at the given relative path.
172
:param relpath: The relative path to the file
174
We're meant to return a file-like object which bzr will
175
then read from. For now we do this via the magic of StringIO
178
mutter("FTP get: %s" % self._abspath(relpath))
181
f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
184
except ftplib.error_perm, e:
185
raise NoSuchFile(msg="Error retrieving %s: %s"
186
% (self.abspath(relpath), str(e)),
189
def put(self, relpath, fp):
190
"""Copy the file-like or string object into the location.
192
:param relpath: Location to put the contents, relative to base.
193
:param f: File-like or string object.
195
if not hasattr(fp, 'read'):
198
mutter("FTP put: %s" % self._abspath(relpath))
200
f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
201
except ftplib.error_perm, e:
202
raise FtpTransportError(orig_error=e)
204
def mkdir(self, relpath):
205
"""Create a directory at the given path."""
207
mutter("FTP mkd: %s" % self._abspath(relpath))
210
f.mkd(self._abspath(relpath))
211
except ftplib.error_perm, e:
213
if 'File exists' in s:
214
# Swallow attempts to mkdir something which is already
215
# present. Hopefully this will shush some errors.
219
except ftplib.error_perm, e:
220
raise FtpTransportError(orig_error=e)
222
def append(self, relpath, f):
223
"""Append the text in the file-like object into the final
226
raise TransportNotPossible('ftp does not support append()')
228
def copy(self, rel_from, rel_to):
229
"""Copy the item at rel_from to the location at rel_to"""
230
raise TransportNotPossible('ftp does not (yet) support copy()')
232
def move(self, rel_from, rel_to):
233
"""Move the item at rel_from to the location at rel_to"""
235
mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
236
self._abspath(rel_to)))
238
f.rename(self._abspath(rel_from), self._abspath(rel_to))
239
except ftplib.error_perm, e:
240
raise FtpTransportError(orig_error=e)
242
def delete(self, relpath):
243
"""Delete the item at relpath"""
245
mutter("FTP rm: %s" % self._abspath(relpath))
247
f.delete(self._abspath(relpath))
248
except ftplib.error_perm, e:
249
raise FtpTransportError(orig_error=e)
252
"""See Transport.listable."""
255
def list_dir(self, relpath):
256
"""See Transport.list_dir."""
258
mutter("FTP nlst: %s" % self._abspath(relpath))
260
basepath = self._abspath(relpath)
261
# FTP.nlst returns paths prefixed by relpath, strip 'em
262
the_list = f.nlst(basepath)
263
stripped = [path[len(basepath)+1:] for path in the_list]
264
# Remove . and .. if present, and return
265
return [path for path in stripped if path not in (".", "..")]
266
except ftplib.error_perm, e:
267
raise FtpTransportError(orig_error=e)
269
def iter_files_recursive(self):
270
"""See Transport.iter_files_recursive.
272
This is cargo-culted from the SFTP transport"""
273
mutter("FTP iter_files_recursive")
274
queue = list(self.list_dir("."))
276
relpath = urllib.quote(queue.pop(0))
277
st = self.stat(relpath)
278
if stat.S_ISDIR(st.st_mode):
279
for i, basename in enumerate(self.list_dir(relpath)):
280
queue.insert(i, relpath+"/"+basename)
284
def stat(self, relpath):
285
"""Return the stat information for a file.
288
mutter("FTP stat: %s" % self._abspath(relpath))
290
return FtpStatResult(f, self._abspath(relpath))
291
except ftplib.error_perm, e:
292
raise FtpTransportError(orig_error=e)
294
def lock_read(self, relpath):
295
"""Lock the given file for shared (read) access.
296
:return: A lock object, which should be passed to Transport.unlock()
298
# The old RemoteBranch ignore lock for reading, so we will
299
# continue that tradition and return a bogus lock object.
300
class BogusLock(object):
301
def __init__(self, path):
305
return BogusLock(relpath)
307
def lock_write(self, relpath):
308
"""Lock the given file for exclusive (write) access.
309
WARNING: many transports do not support this, so trying avoid using it
311
:return: A lock object, which should be passed to Transport.unlock()
313
return self.lock_read(relpath)