~bzr-pqm/bzr/bzr.dev

1185.36.4 by Daniel Silverstone
Add FTP transport
1
# Copyright (C) 2005 Canonical Ltd
2
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.
7
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.
12
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.
17
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
19
cargo-culting from the sftp transport and the http transport.
20
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.
25
"""
26
27
from bzrlib.transport import Transport
28
29
from bzrlib.errors import (TransportNotPossible, NoSuchFile, 
30
                           NonRelativePath, TransportError, ConnectionError)
31
32
import os, errno
33
from cStringIO import StringIO
34
import ftplib
35
import urlparse
36
import urllib
37
import stat
38
39
from bzrlib.errors import BzrError, BzrCheckError
40
from bzrlib.branch import Branch
41
from bzrlib.trace import mutter
42
43
44
class FtpTransportError(TransportError):
45
    pass
46
47
48
class FtpStatResult(object):
49
    def __init__(self, f, relpath):
50
        try:
51
            self.st_size = f.size(relpath)
52
            self.st_mode = stat.S_IFREG
53
        except ftplib.error_perm:
54
            pwd = f.pwd()
55
            try:
56
                f.cwd(relpath)
57
                self.st_mode = stat.S_IFDIR
58
            finally:
59
                f.cwd(pwd)
60
61
62
class FtpTransport(Transport):
63
    """This is the transport agent for ftp:// access."""
64
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://')
70
        if self.is_active:
71
            base = base[1:]
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
76
77
78
    def _get_FTP(self):
79
        """Return the ftplib.FTP instance for this object."""
80
        if self._FTP_instance is not None:
81
            return self._FTP_instance
82
        
83
        try:
84
            username = ''
85
            password = ''
86
            hostname = self._host
87
            if '@' in hostname:
88
                username, hostname = hostname.split("@", 1)
89
            if ':' in username:
90
                username, password = username.split(":", 1)
91
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)
99
100
    def should_cache(self):
101
        """Return True if the data pulled across should be cached locally.
102
        """
103
        return True
104
105
    def clone(self, offset=None):
106
        """Return a new FtpTransport with root at self.base + offset.
107
        """
108
        mutter("FTP clone")
109
        if offset is None:
110
            return FtpTransport(self.base, self._FTP_instance)
111
        else:
112
            return FtpTransport(self.abspath(offset), self._FTP_instance)
113
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('/')
119
        else:
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:
130
            if p == '..':
131
                if len(basepath) == 0:
132
                    # In most filesystems, a request for the parent
133
                    # of root, just returns root.
134
                    continue
135
                basepath.pop()
136
            elif p == '.' or p == '':
137
                continue # No-op
138
            else:
139
                basepath.append(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)
144
    
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
148
        """
149
        path = self._abspath(relpath)
150
        return urlparse.urlunparse((self._proto,
151
                self._host, path, '', '', ''))
152
153
    def has(self, relpath):
154
        """Does the target location exist?
155
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,
158
        all is good.
159
        """
160
        try:
161
            f = self._get_FTP()
162
            s = f.size(self._abspath(relpath))
163
            mutter("FTP has: %s" % self._abspath(relpath))
164
            return True
165
        except ftplib.error_perm:
166
            mutter("FTP has not: %s" % self._abspath(relpath))
167
            return False
168
169
    def get(self, relpath, decode=False):
170
        """Get the file at the given relative path.
171
172
        :param relpath: The relative path to the file
173
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
176
        """
177
        try:
178
            mutter("FTP get: %s" % self._abspath(relpath))
179
            f = self._get_FTP()
180
            ret = StringIO()
181
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
182
            ret.seek(0)
183
            return ret
184
        except ftplib.error_perm, e:
185
            raise NoSuchFile(msg="Error retrieving %s: %s"
186
                             % (self.abspath(relpath), str(e)),
187
                             orig_error=e)
188
189
    def put(self, relpath, fp):
190
        """Copy the file-like or string object into the location.
191
192
        :param relpath: Location to put the contents, relative to base.
193
        :param f:       File-like or string object.
194
        """
195
        if not hasattr(fp, 'read'):
196
            fp = StringIO(fp)
197
        try:
198
            mutter("FTP put: %s" % self._abspath(relpath))
199
            f = self._get_FTP()
200
            f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
201
        except ftplib.error_perm, e:
202
            raise FtpTransportError(orig_error=e)
203
204
    def mkdir(self, relpath):
205
        """Create a directory at the given path."""
206
        try:
207
            mutter("FTP mkd: %s" % self._abspath(relpath))
208
            f = self._get_FTP()
209
            try:
210
                f.mkd(self._abspath(relpath))
211
            except ftplib.error_perm, e:
212
                s = str(e)
213
                if 'File exists' in s:
214
                    # Swallow attempts to mkdir something which is already
215
                    # present. Hopefully this will shush some errors.
216
                    return
217
                else:
218
                    raise
219
        except ftplib.error_perm, e:
220
            raise FtpTransportError(orig_error=e)
221
222
    def append(self, relpath, f):
223
        """Append the text in the file-like object into the final
224
        location.
225
        """
226
        raise TransportNotPossible('ftp does not support append()')
227
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()')
231
232
    def move(self, rel_from, rel_to):
233
        """Move the item at rel_from to the location at rel_to"""
234
        try:
235
            mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
236
                                         self._abspath(rel_to)))
237
            f = self._get_FTP()
238
            f.rename(self._abspath(rel_from), self._abspath(rel_to))
239
        except ftplib.error_perm, e:
240
            raise FtpTransportError(orig_error=e)
241
242
    def delete(self, relpath):
243
        """Delete the item at relpath"""
244
        try:
245
            mutter("FTP rm: %s" % self._abspath(relpath))
246
            f = self._get_FTP()
247
            f.delete(self._abspath(relpath))
248
        except ftplib.error_perm, e:
249
            raise FtpTransportError(orig_error=e)
250
251
    def listable(self):
252
        """See Transport.listable."""
253
        return True
254
255
    def list_dir(self, relpath):
256
        """See Transport.list_dir."""
257
        try:
258
            mutter("FTP nlst: %s" % self._abspath(relpath))
259
            f = self._get_FTP()
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)
268
269
    def iter_files_recursive(self):
270
        """See Transport.iter_files_recursive.
271
272
        This is cargo-culted from the SFTP transport"""
273
        mutter("FTP iter_files_recursive")
274
        queue = list(self.list_dir("."))
275
        while queue:
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)
281
            else:
282
                yield relpath
283
284
    def stat(self, relpath):
285
        """Return the stat information for a file.
286
        """
287
        try:
288
            mutter("FTP stat: %s" % self._abspath(relpath))
289
            f = self._get_FTP()
290
            return FtpStatResult(f, self._abspath(relpath))
291
        except ftplib.error_perm, e:
292
            raise FtpTransportError(orig_error=e)
293
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()
297
        """
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):
302
                self.path = path
303
            def unlock(self):
304
                pass
305
        return BogusLock(relpath)
306
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
310
311
        :return: A lock object, which should be passed to Transport.unlock()
312
        """
313
        return self.lock_read(relpath)