~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Martin Pool
  • Date: 2005-10-12 10:44:57 UTC
  • mto: (1185.16.26)
  • mto: This revision was merged to the branch mainline in revision 1454.
  • Revision ID: mbp@sourcefrog.net-20051012104457-a186917c83a2afc7
[pick] clear hashcache in format upgrade to avoid worrisome warning

robertc@lifelesslap.robertcollins.net-20051012002452-64068f25c8656f66

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)