~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Martin Pool
  • Date: 2006-01-13 06:31:42 UTC
  • Revision ID: mbp@sourcefrog.net-20060113063142-8e706dc1483c69e1
Bump version to 0.8pre

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 cStringIO import StringIO
 
28
import errno
 
29
import ftplib
 
30
import os
 
31
import urllib
 
32
import urlparse
 
33
import stat
 
34
from warnings import warn
 
35
 
 
36
 
 
37
from bzrlib.transport import Transport
 
38
from bzrlib.errors import (TransportNotPossible, TransportError,
 
39
                           NoSuchFile, FileExists)
 
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, mode=None):
 
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
        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
 
189
        """
 
190
        if not hasattr(fp, 'read'):
 
191
            fp = StringIO(fp)
 
192
        try:
 
193
            mutter("FTP put: %s" % self._abspath(relpath))
 
194
            f = self._get_FTP()
 
195
            f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
 
196
        except ftplib.error_perm, e:
 
197
            raise TransportError(orig_error=e)
 
198
 
 
199
    def mkdir(self, relpath, mode=None):
 
200
        """Create a directory at the given path."""
 
201
        try:
 
202
            mutter("FTP mkd: %s" % self._abspath(relpath))
 
203
            f = self._get_FTP()
 
204
            try:
 
205
                f.mkd(self._abspath(relpath))
 
206
            except ftplib.error_perm, e:
 
207
                s = str(e)
 
208
                if 'File exists' in s:
 
209
                    raise FileExists(self.abspath(relpath), extra=s)
 
210
                else:
 
211
                    raise
 
212
        except ftplib.error_perm, e:
 
213
            raise TransportError(orig_error=e)
 
214
 
 
215
    def append(self, relpath, f):
 
216
        """Append the text in the file-like object into the final
 
217
        location.
 
218
        """
 
219
        raise TransportNotPossible('ftp does not support append()')
 
220
 
 
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()')
 
224
 
 
225
    def move(self, rel_from, rel_to):
 
226
        """Move the item at rel_from to the location at rel_to"""
 
227
        try:
 
228
            mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
 
229
                                         self._abspath(rel_to)))
 
230
            f = self._get_FTP()
 
231
            f.rename(self._abspath(rel_from), self._abspath(rel_to))
 
232
        except ftplib.error_perm, e:
 
233
            raise TransportError(orig_error=e)
 
234
 
 
235
    def delete(self, relpath):
 
236
        """Delete the item at relpath"""
 
237
        try:
 
238
            mutter("FTP rm: %s" % self._abspath(relpath))
 
239
            f = self._get_FTP()
 
240
            f.delete(self._abspath(relpath))
 
241
        except ftplib.error_perm, e:
 
242
            raise TransportError(orig_error=e)
 
243
 
 
244
    def listable(self):
 
245
        """See Transport.listable."""
 
246
        return True
 
247
 
 
248
    def list_dir(self, relpath):
 
249
        """See Transport.list_dir."""
 
250
        try:
 
251
            mutter("FTP nlst: %s" % self._abspath(relpath))
 
252
            f = self._get_FTP()
 
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)
 
261
 
 
262
    def iter_files_recursive(self):
 
263
        """See Transport.iter_files_recursive.
 
264
 
 
265
        This is cargo-culted from the SFTP transport"""
 
266
        mutter("FTP iter_files_recursive")
 
267
        queue = list(self.list_dir("."))
 
268
        while queue:
 
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)
 
274
            else:
 
275
                yield relpath
 
276
 
 
277
    def stat(self, relpath):
 
278
        """Return the stat information for a file.
 
279
        """
 
280
        try:
 
281
            mutter("FTP stat: %s" % self._abspath(relpath))
 
282
            f = self._get_FTP()
 
283
            return FtpStatResult(f, self._abspath(relpath))
 
284
        except ftplib.error_perm, e:
 
285
            raise TransportError(orig_error=e)
 
286
 
 
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()
 
290
        """
 
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):
 
295
                self.path = path
 
296
            def unlock(self):
 
297
                pass
 
298
        return BogusLock(relpath)
 
299
 
 
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
 
303
 
 
304
        :return: A lock object, which should be passed to Transport.unlock()
 
305
        """
 
306
        return self.lock_read(relpath)
 
307
 
 
308
 
 
309
def get_test_permutations():
 
310
    """Return the permutations to be used in testing."""
 
311
    warn("There are no FTP transport provider tests yet.")
 
312
    return []