~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

Update news and readme

- better explanation of dependencies

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, TransportError,
30
 
                           NoSuchFile, FileExists)
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.branch import Branch
40
 
from bzrlib.trace import mutter
41
 
 
42
 
 
43
 
class FtpStatResult(object):
44
 
    def __init__(self, f, relpath):
45
 
        try:
46
 
            self.st_size = f.size(relpath)
47
 
            self.st_mode = stat.S_IFREG
48
 
        except ftplib.error_perm:
49
 
            pwd = f.pwd()
50
 
            try:
51
 
                f.cwd(relpath)
52
 
                self.st_mode = stat.S_IFDIR
53
 
            finally:
54
 
                f.cwd(pwd)
55
 
 
56
 
 
57
 
class FtpTransport(Transport):
58
 
    """This is the transport agent for ftp:// access."""
59
 
 
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://')
65
 
        if self.is_active:
66
 
            base = base[1:]
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
71
 
 
72
 
 
73
 
    def _get_FTP(self):
74
 
        """Return the ftplib.FTP instance for this object."""
75
 
        if self._FTP_instance is not None:
76
 
            return self._FTP_instance
77
 
        
78
 
        try:
79
 
            username = ''
80
 
            password = ''
81
 
            hostname = self._host
82
 
            if '@' in hostname:
83
 
                username, hostname = hostname.split("@", 1)
84
 
            if ':' in username:
85
 
                username, password = username.split(":", 1)
86
 
 
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)
94
 
 
95
 
    def should_cache(self):
96
 
        """Return True if the data pulled across should be cached locally.
97
 
        """
98
 
        return True
99
 
 
100
 
    def clone(self, offset=None):
101
 
        """Return a new FtpTransport with root at self.base + offset.
102
 
        """
103
 
        mutter("FTP clone")
104
 
        if offset is None:
105
 
            return FtpTransport(self.base, self._FTP_instance)
106
 
        else:
107
 
            return FtpTransport(self.abspath(offset), self._FTP_instance)
108
 
 
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('/')
114
 
        else:
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:
125
 
            if p == '..':
126
 
                if len(basepath) == 0:
127
 
                    # In most filesystems, a request for the parent
128
 
                    # of root, just returns root.
129
 
                    continue
130
 
                basepath.pop()
131
 
            elif p == '.' or p == '':
132
 
                continue # No-op
133
 
            else:
134
 
                basepath.append(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)
139
 
    
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
143
 
        """
144
 
        path = self._abspath(relpath)
145
 
        return urlparse.urlunparse((self._proto,
146
 
                self._host, path, '', '', ''))
147
 
 
148
 
    def has(self, relpath):
149
 
        """Does the target location exist?
150
 
 
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,
153
 
        all is good.
154
 
        """
155
 
        try:
156
 
            f = self._get_FTP()
157
 
            s = f.size(self._abspath(relpath))
158
 
            mutter("FTP has: %s" % self._abspath(relpath))
159
 
            return True
160
 
        except ftplib.error_perm:
161
 
            mutter("FTP has not: %s" % self._abspath(relpath))
162
 
            return False
163
 
 
164
 
    def get(self, relpath, decode=False):
165
 
        """Get the file at the given relative path.
166
 
 
167
 
        :param relpath: The relative path to the file
168
 
 
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
171
 
        """
172
 
        try:
173
 
            mutter("FTP get: %s" % self._abspath(relpath))
174
 
            f = self._get_FTP()
175
 
            ret = StringIO()
176
 
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
177
 
            ret.seek(0)
178
 
            return ret
179
 
        except ftplib.error_perm, e:
180
 
            raise NoSuchFile(self.abspath(relpath), extra=extra)
181
 
 
182
 
    def put(self, relpath, fp):
183
 
        """Copy the file-like or string object into the location.
184
 
 
185
 
        :param relpath: Location to put the contents, relative to base.
186
 
        :param f:       File-like or string object.
187
 
        """
188
 
        if not hasattr(fp, 'read'):
189
 
            fp = StringIO(fp)
190
 
        try:
191
 
            mutter("FTP put: %s" % self._abspath(relpath))
192
 
            f = self._get_FTP()
193
 
            f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
194
 
        except ftplib.error_perm, e:
195
 
            raise TransportError(orig_error=e)
196
 
 
197
 
    def mkdir(self, relpath):
198
 
        """Create a directory at the given path."""
199
 
        try:
200
 
            mutter("FTP mkd: %s" % self._abspath(relpath))
201
 
            f = self._get_FTP()
202
 
            try:
203
 
                f.mkd(self._abspath(relpath))
204
 
            except ftplib.error_perm, e:
205
 
                s = str(e)
206
 
                if 'File exists' in s:
207
 
                    raise FileExists(self.abspath(relpath), extra=s)
208
 
                else:
209
 
                    raise
210
 
        except ftplib.error_perm, e:
211
 
            raise TransportError(orig_error=e)
212
 
 
213
 
    def append(self, relpath, f):
214
 
        """Append the text in the file-like object into the final
215
 
        location.
216
 
        """
217
 
        raise TransportNotPossible('ftp does not support append()')
218
 
 
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()')
222
 
 
223
 
    def move(self, rel_from, rel_to):
224
 
        """Move the item at rel_from to the location at rel_to"""
225
 
        try:
226
 
            mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
227
 
                                         self._abspath(rel_to)))
228
 
            f = self._get_FTP()
229
 
            f.rename(self._abspath(rel_from), self._abspath(rel_to))
230
 
        except ftplib.error_perm, e:
231
 
            raise TransportError(orig_error=e)
232
 
 
233
 
    def delete(self, relpath):
234
 
        """Delete the item at relpath"""
235
 
        try:
236
 
            mutter("FTP rm: %s" % self._abspath(relpath))
237
 
            f = self._get_FTP()
238
 
            f.delete(self._abspath(relpath))
239
 
        except ftplib.error_perm, e:
240
 
            raise TransportError(orig_error=e)
241
 
 
242
 
    def listable(self):
243
 
        """See Transport.listable."""
244
 
        return True
245
 
 
246
 
    def list_dir(self, relpath):
247
 
        """See Transport.list_dir."""
248
 
        try:
249
 
            mutter("FTP nlst: %s" % self._abspath(relpath))
250
 
            f = self._get_FTP()
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)
259
 
 
260
 
    def iter_files_recursive(self):
261
 
        """See Transport.iter_files_recursive.
262
 
 
263
 
        This is cargo-culted from the SFTP transport"""
264
 
        mutter("FTP iter_files_recursive")
265
 
        queue = list(self.list_dir("."))
266
 
        while queue:
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)
272
 
            else:
273
 
                yield relpath
274
 
 
275
 
    def stat(self, relpath):
276
 
        """Return the stat information for a file.
277
 
        """
278
 
        try:
279
 
            mutter("FTP stat: %s" % self._abspath(relpath))
280
 
            f = self._get_FTP()
281
 
            return FtpStatResult(f, self._abspath(relpath))
282
 
        except ftplib.error_perm, e:
283
 
            raise TransportError(orig_error=e)
284
 
 
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()
288
 
        """
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):
293
 
                self.path = path
294
 
            def unlock(self):
295
 
                pass
296
 
        return BogusLock(relpath)
297
 
 
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
301
 
 
302
 
        :return: A lock object, which should be passed to Transport.unlock()
303
 
        """
304
 
        return self.lock_read(relpath)