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, TransportError,
30
NoSuchFile, FileExists)
33
from cStringIO import StringIO
39
from bzrlib.branch import Branch
40
from bzrlib.trace import mutter
43
class FtpStatResult(object):
44
def __init__(self, f, relpath):
46
self.st_size = f.size(relpath)
47
self.st_mode = stat.S_IFREG
48
except ftplib.error_perm:
52
self.st_mode = stat.S_IFDIR
57
class FtpTransport(Transport):
58
"""This is the transport agent for ftp:// access."""
60
def __init__(self, base, _provided_instance=None):
61
"""Set the base path where files will be stored."""
62
assert base.startswith('ftp://') or base.startswith('aftp://')
63
super(FtpTransport, self).__init__(base)
64
self.is_active = base.startswith('aftp://')
67
(self._proto, self._host,
68
self._path, self._parameters,
69
self._query, self._fragment) = urlparse.urlparse(self.base)
70
self._FTP_instance = _provided_instance
74
"""Return the ftplib.FTP instance for this object."""
75
if self._FTP_instance is not None:
76
return self._FTP_instance
83
username, hostname = hostname.split("@", 1)
85
username, password = username.split(":", 1)
87
mutter("Constructing FTP instance")
88
self._FTP_instance = ftplib.FTP(hostname, username, password)
89
self._FTP_instance.set_pasv(not self.is_active)
90
return self._FTP_instance
91
except ftplib.error_perm, e:
92
raise TransportError(msg="Error setting up connection: %s"
93
% str(e), orig_error=e)
95
def should_cache(self):
96
"""Return True if the data pulled across should be cached locally.
100
def clone(self, offset=None):
101
"""Return a new FtpTransport with root at self.base + offset.
105
return FtpTransport(self.base, self._FTP_instance)
107
return FtpTransport(self.abspath(offset), self._FTP_instance)
109
def _abspath(self, relpath):
110
assert isinstance(relpath, basestring)
111
relpath = urllib.unquote(relpath)
112
if isinstance(relpath, basestring):
113
relpath_parts = relpath.split('/')
115
# TODO: Don't call this with an array - no magic interfaces
116
relpath_parts = relpath[:]
117
if len(relpath_parts) > 1:
118
if relpath_parts[0] == '':
119
raise ValueError("path %r within branch %r seems to be absolute"
120
% (relpath, self._path))
121
basepath = self._path.split('/')
122
if len(basepath) > 0 and basepath[-1] == '':
123
basepath = basepath[:-1]
124
for p in relpath_parts:
126
if len(basepath) == 0:
127
# In most filesystems, a request for the parent
128
# of root, just returns root.
131
elif p == '.' or p == '':
135
# Possibly, we could use urlparse.urljoin() here, but
136
# I'm concerned about when it chooses to strip the last
137
# portion of the path, and when it doesn't.
138
return '/'.join(basepath)
140
def abspath(self, relpath):
141
"""Return the full url to the given relative path.
142
This can be supplied with a string or a list
144
path = self._abspath(relpath)
145
return urlparse.urlunparse((self._proto,
146
self._host, path, '', '', ''))
148
def has(self, relpath):
149
"""Does the target location exist?
151
XXX: I assume we're never asked has(dirname) and thus I use
152
the FTP size command and assume that if it doesn't raise,
157
s = f.size(self._abspath(relpath))
158
mutter("FTP has: %s" % self._abspath(relpath))
160
except ftplib.error_perm:
161
mutter("FTP has not: %s" % self._abspath(relpath))
164
def get(self, relpath, decode=False):
165
"""Get the file at the given relative path.
167
:param relpath: The relative path to the file
169
We're meant to return a file-like object which bzr will
170
then read from. For now we do this via the magic of StringIO
173
mutter("FTP get: %s" % self._abspath(relpath))
176
f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
179
except ftplib.error_perm, e:
180
raise NoSuchFile(self.abspath(relpath), extra=extra)
182
def put(self, relpath, fp):
183
"""Copy the file-like or string object into the location.
185
:param relpath: Location to put the contents, relative to base.
186
:param f: File-like or string object.
188
if not hasattr(fp, 'read'):
191
mutter("FTP put: %s" % self._abspath(relpath))
193
f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
194
except ftplib.error_perm, e:
195
raise TransportError(orig_error=e)
197
def mkdir(self, relpath):
198
"""Create a directory at the given path."""
200
mutter("FTP mkd: %s" % self._abspath(relpath))
203
f.mkd(self._abspath(relpath))
204
except ftplib.error_perm, e:
206
if 'File exists' in s:
207
raise FileExists(self.abspath(relpath), extra=s)
210
except ftplib.error_perm, e:
211
raise TransportError(orig_error=e)
213
def append(self, relpath, f):
214
"""Append the text in the file-like object into the final
217
raise TransportNotPossible('ftp does not support append()')
219
def copy(self, rel_from, rel_to):
220
"""Copy the item at rel_from to the location at rel_to"""
221
raise TransportNotPossible('ftp does not (yet) support copy()')
223
def move(self, rel_from, rel_to):
224
"""Move the item at rel_from to the location at rel_to"""
226
mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
227
self._abspath(rel_to)))
229
f.rename(self._abspath(rel_from), self._abspath(rel_to))
230
except ftplib.error_perm, e:
231
raise TransportError(orig_error=e)
233
def delete(self, relpath):
234
"""Delete the item at relpath"""
236
mutter("FTP rm: %s" % self._abspath(relpath))
238
f.delete(self._abspath(relpath))
239
except ftplib.error_perm, e:
240
raise TransportError(orig_error=e)
243
"""See Transport.listable."""
246
def list_dir(self, relpath):
247
"""See Transport.list_dir."""
249
mutter("FTP nlst: %s" % self._abspath(relpath))
251
basepath = self._abspath(relpath)
252
# FTP.nlst returns paths prefixed by relpath, strip 'em
253
the_list = f.nlst(basepath)
254
stripped = [path[len(basepath)+1:] for path in the_list]
255
# Remove . and .. if present, and return
256
return [path for path in stripped if path not in (".", "..")]
257
except ftplib.error_perm, e:
258
raise TransportError(orig_error=e)
260
def iter_files_recursive(self):
261
"""See Transport.iter_files_recursive.
263
This is cargo-culted from the SFTP transport"""
264
mutter("FTP iter_files_recursive")
265
queue = list(self.list_dir("."))
267
relpath = urllib.quote(queue.pop(0))
268
st = self.stat(relpath)
269
if stat.S_ISDIR(st.st_mode):
270
for i, basename in enumerate(self.list_dir(relpath)):
271
queue.insert(i, relpath+"/"+basename)
275
def stat(self, relpath):
276
"""Return the stat information for a file.
279
mutter("FTP stat: %s" % self._abspath(relpath))
281
return FtpStatResult(f, self._abspath(relpath))
282
except ftplib.error_perm, e:
283
raise TransportError(orig_error=e)
285
def lock_read(self, relpath):
286
"""Lock the given file for shared (read) access.
287
:return: A lock object, which should be passed to Transport.unlock()
289
# The old RemoteBranch ignore lock for reading, so we will
290
# continue that tradition and return a bogus lock object.
291
class BogusLock(object):
292
def __init__(self, path):
296
return BogusLock(relpath)
298
def lock_write(self, relpath):
299
"""Lock the given file for exclusive (write) access.
300
WARNING: many transports do not support this, so trying avoid using it
302
:return: A lock object, which should be passed to Transport.unlock()
304
return self.lock_read(relpath)