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
34
from warnings import warn
37
from bzrlib.transport import Transport
38
from bzrlib.errors import (TransportNotPossible, TransportError,
39
NoSuchFile, FileExists)
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, mode=None):
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.
187
TODO: jam 20051215 This should be an atomic put, not overwritting files in place
188
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
190
if not hasattr(fp, 'read'):
193
mutter("FTP put: %s" % self._abspath(relpath))
195
f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
196
except ftplib.error_perm, e:
197
raise TransportError(orig_error=e)
199
def mkdir(self, relpath, mode=None):
200
"""Create a directory at the given path."""
202
mutter("FTP mkd: %s" % self._abspath(relpath))
205
f.mkd(self._abspath(relpath))
206
except ftplib.error_perm, e:
208
if 'File exists' in s:
209
raise FileExists(self.abspath(relpath), extra=s)
212
except ftplib.error_perm, e:
213
raise TransportError(orig_error=e)
215
def append(self, relpath, f):
216
"""Append the text in the file-like object into the final
219
raise TransportNotPossible('ftp does not support append()')
221
def copy(self, rel_from, rel_to):
222
"""Copy the item at rel_from to the location at rel_to"""
223
raise TransportNotPossible('ftp does not (yet) support copy()')
225
def move(self, rel_from, rel_to):
226
"""Move the item at rel_from to the location at rel_to"""
228
mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
229
self._abspath(rel_to)))
231
f.rename(self._abspath(rel_from), self._abspath(rel_to))
232
except ftplib.error_perm, e:
233
raise TransportError(orig_error=e)
235
def delete(self, relpath):
236
"""Delete the item at relpath"""
238
mutter("FTP rm: %s" % self._abspath(relpath))
240
f.delete(self._abspath(relpath))
241
except ftplib.error_perm, e:
242
raise TransportError(orig_error=e)
245
"""See Transport.listable."""
248
def list_dir(self, relpath):
249
"""See Transport.list_dir."""
251
mutter("FTP nlst: %s" % self._abspath(relpath))
253
basepath = self._abspath(relpath)
254
# FTP.nlst returns paths prefixed by relpath, strip 'em
255
the_list = f.nlst(basepath)
256
stripped = [path[len(basepath)+1:] for path in the_list]
257
# Remove . and .. if present, and return
258
return [path for path in stripped if path not in (".", "..")]
259
except ftplib.error_perm, e:
260
raise TransportError(orig_error=e)
262
def iter_files_recursive(self):
263
"""See Transport.iter_files_recursive.
265
This is cargo-culted from the SFTP transport"""
266
mutter("FTP iter_files_recursive")
267
queue = list(self.list_dir("."))
269
relpath = urllib.quote(queue.pop(0))
270
st = self.stat(relpath)
271
if stat.S_ISDIR(st.st_mode):
272
for i, basename in enumerate(self.list_dir(relpath)):
273
queue.insert(i, relpath+"/"+basename)
277
def stat(self, relpath):
278
"""Return the stat information for a file.
281
mutter("FTP stat: %s" % self._abspath(relpath))
283
return FtpStatResult(f, self._abspath(relpath))
284
except ftplib.error_perm, e:
285
raise TransportError(orig_error=e)
287
def lock_read(self, relpath):
288
"""Lock the given file for shared (read) access.
289
:return: A lock object, which should be passed to Transport.unlock()
291
# The old RemoteBranch ignore lock for reading, so we will
292
# continue that tradition and return a bogus lock object.
293
class BogusLock(object):
294
def __init__(self, path):
298
return BogusLock(relpath)
300
def lock_write(self, relpath):
301
"""Lock the given file for exclusive (write) access.
302
WARNING: many transports do not support this, so trying avoid using it
304
:return: A lock object, which should be passed to Transport.unlock()
306
return self.lock_read(relpath)
309
def get_test_permutations():
310
"""Return the permutations to be used in testing."""
311
warn("There are no FTP transport provider tests yet.")