~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

Merge from mpool.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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)