~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 cStringIO import StringIO
1530.1.11 by Robert Collins
Push the transport permutations list into each transport module allowing for automatic testing of new modules that are registered as transports.
28
import errno
1185.36.4 by Daniel Silverstone
Add FTP transport
29
import ftplib
1530.1.11 by Robert Collins
Push the transport permutations list into each transport module allowing for automatic testing of new modules that are registered as transports.
30
import os
31
import urllib
1185.36.4 by Daniel Silverstone
Add FTP transport
32
import urlparse
33
import stat
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
34
import time
1185.72.13 by Matthieu Moy
Make ftp put atomic
35
import random
1530.1.11 by Robert Collins
Push the transport permutations list into each transport module allowing for automatic testing of new modules that are registered as transports.
36
from warnings import warn
37
38
39
from bzrlib.transport import Transport
40
from bzrlib.errors import (TransportNotPossible, TransportError,
41
                           NoSuchFile, FileExists)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
42
from bzrlib.trace import mutter, warning
1185.36.4 by Daniel Silverstone
Add FTP transport
43
44
1185.36.6 by Daniel Silverstone
Make sure put raises NoSuchFile on appropriate errors. Also ensure we only ever connect once. Seriously. Really.
45
_FTP_cache = {}
46
def _find_FTP(hostname, username, password, is_active):
47
    """Find an ftplib.FTP instance attached to this triplet."""
48
    key = "%s|%s|%s|%s" % (hostname, username, password, is_active)
49
    if key not in _FTP_cache:
50
        mutter("Constructing FTP instance against %r" % key)
51
        _FTP_cache[key] = ftplib.FTP(hostname, username, password)
52
        _FTP_cache[key].set_pasv(not is_active)
53
    return _FTP_cache[key]    
54
55
1185.36.4 by Daniel Silverstone
Add FTP transport
56
class FtpTransportError(TransportError):
57
    pass
58
59
60
class FtpStatResult(object):
61
    def __init__(self, f, relpath):
62
        try:
63
            self.st_size = f.size(relpath)
64
            self.st_mode = stat.S_IFREG
65
        except ftplib.error_perm:
66
            pwd = f.pwd()
67
            try:
68
                f.cwd(relpath)
69
                self.st_mode = stat.S_IFDIR
70
            finally:
71
                f.cwd(pwd)
72
73
1185.72.15 by Matthieu Moy
better error messages on ftp failures
74
_number_of_retries = 2
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
75
_sleep_between_retries = 5
1185.72.12 by Matthieu Moy
made __number_of_retries global
76
1185.36.4 by Daniel Silverstone
Add FTP transport
77
class FtpTransport(Transport):
78
    """This is the transport agent for ftp:// access."""
79
80
    def __init__(self, base, _provided_instance=None):
81
        """Set the base path where files will be stored."""
82
        assert base.startswith('ftp://') or base.startswith('aftp://')
83
        super(FtpTransport, self).__init__(base)
84
        self.is_active = base.startswith('aftp://')
85
        if self.is_active:
86
            base = base[1:]
87
        (self._proto, self._host,
88
            self._path, self._parameters,
89
            self._query, self._fragment) = urlparse.urlparse(self.base)
90
        self._FTP_instance = _provided_instance
91
92
    def _get_FTP(self):
93
        """Return the ftplib.FTP instance for this object."""
94
        if self._FTP_instance is not None:
95
            return self._FTP_instance
96
        
97
        try:
98
            username = ''
99
            password = ''
100
            hostname = self._host
101
            if '@' in hostname:
102
                username, hostname = hostname.split("@", 1)
103
            if ':' in username:
104
                username, password = username.split(":", 1)
105
1185.36.6 by Daniel Silverstone
Make sure put raises NoSuchFile on appropriate errors. Also ensure we only ever connect once. Seriously. Really.
106
            self._FTP_instance = _find_FTP(hostname, username, password,
107
                                           self.is_active)
1185.36.4 by Daniel Silverstone
Add FTP transport
108
            return self._FTP_instance
109
        except ftplib.error_perm, e:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
110
            raise TransportError(msg="Error setting up connection: %s"
1185.36.4 by Daniel Silverstone
Add FTP transport
111
                                    % str(e), orig_error=e)
112
113
    def should_cache(self):
114
        """Return True if the data pulled across should be cached locally.
115
        """
116
        return True
117
118
    def clone(self, offset=None):
119
        """Return a new FtpTransport with root at self.base + offset.
120
        """
121
        mutter("FTP clone")
122
        if offset is None:
123
            return FtpTransport(self.base, self._FTP_instance)
124
        else:
125
            return FtpTransport(self.abspath(offset), self._FTP_instance)
126
127
    def _abspath(self, relpath):
128
        assert isinstance(relpath, basestring)
129
        relpath = urllib.unquote(relpath)
130
        if isinstance(relpath, basestring):
131
            relpath_parts = relpath.split('/')
132
        else:
133
            # TODO: Don't call this with an array - no magic interfaces
134
            relpath_parts = relpath[:]
135
        if len(relpath_parts) > 1:
136
            if relpath_parts[0] == '':
137
                raise ValueError("path %r within branch %r seems to be absolute"
138
                                 % (relpath, self._path))
139
        basepath = self._path.split('/')
140
        if len(basepath) > 0 and basepath[-1] == '':
141
            basepath = basepath[:-1]
142
        for p in relpath_parts:
143
            if p == '..':
144
                if len(basepath) == 0:
145
                    # In most filesystems, a request for the parent
146
                    # of root, just returns root.
147
                    continue
148
                basepath.pop()
149
            elif p == '.' or p == '':
150
                continue # No-op
151
            else:
152
                basepath.append(p)
153
        # Possibly, we could use urlparse.urljoin() here, but
154
        # I'm concerned about when it chooses to strip the last
155
        # portion of the path, and when it doesn't.
156
        return '/'.join(basepath)
157
    
158
    def abspath(self, relpath):
159
        """Return the full url to the given relative path.
160
        This can be supplied with a string or a list
161
        """
162
        path = self._abspath(relpath)
163
        return urlparse.urlunparse((self._proto,
164
                self._host, path, '', '', ''))
165
166
    def has(self, relpath):
167
        """Does the target location exist?
168
169
        XXX: I assume we're never asked has(dirname) and thus I use
170
        the FTP size command and assume that if it doesn't raise,
171
        all is good.
172
        """
173
        try:
174
            f = self._get_FTP()
175
            s = f.size(self._abspath(relpath))
176
            mutter("FTP has: %s" % self._abspath(relpath))
177
            return True
178
        except ftplib.error_perm:
179
            mutter("FTP has not: %s" % self._abspath(relpath))
180
            return False
181
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
182
    def get(self, relpath, decode=False, retries=0):
1185.36.4 by Daniel Silverstone
Add FTP transport
183
        """Get the file at the given relative path.
184
185
        :param relpath: The relative path to the file
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
186
        :param retries: Number of retries after temporary failures so far
187
                        for this operation.
1185.36.4 by Daniel Silverstone
Add FTP transport
188
189
        We're meant to return a file-like object which bzr will
190
        then read from. For now we do this via the magic of StringIO
191
        """
192
        try:
193
            mutter("FTP get: %s" % self._abspath(relpath))
194
            f = self._get_FTP()
195
            ret = StringIO()
196
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
197
            ret.seek(0)
198
            return ret
199
        except ftplib.error_perm, e:
1185.50.39 by John Arbash Meinel
[patch] Wouter Bolsterlee: Fix ftp error in error handling code
200
            raise NoSuchFile(self.abspath(relpath), extra=str(e))
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
201
        except ftplib.error_temp, e:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
202
            if retries > _number_of_retries:
203
                raise TransportError(msg="FTP temporary error during GET %s. Aborting."
204
                                     % self.abspath(relpath),
205
                                     orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
206
            else:
207
                warning("FTP temporary error: %s. Retrying." % str(e))
208
                self._FTP_instance = None
209
                return self.get(relpath, decode, retries+1)
1185.72.15 by Matthieu Moy
better error messages on ftp failures
210
        except EOFError, e:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
211
            if retries > _number_of_retries:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
212
                raise TransportError("FTP control connection closed during GET %s."
213
                                     % self.abspath(relpath),
214
                                     orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
215
            else:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
216
                warning("FTP control connection closed. Trying to reopen.")
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
217
                time.sleep(_sleep_between_retries)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
218
                self._FTP_instance = None
219
                return self.get(relpath, decode, retries+1)
1185.36.4 by Daniel Silverstone
Add FTP transport
220
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
221
    def put(self, relpath, fp, mode=None, retries=0):
1185.36.4 by Daniel Silverstone
Add FTP transport
222
        """Copy the file-like or string object into the location.
223
224
        :param relpath: Location to put the contents, relative to base.
1185.72.13 by Matthieu Moy
Make ftp put atomic
225
        :param fp:       File-like or string object.
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
226
        :param retries: Number of retries after temporary failures so far
227
                        for this operation.
228
1185.58.2 by John Arbash Meinel
Added mode to the appropriate transport functions, and tests to make sure they work.
229
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
1185.36.4 by Daniel Silverstone
Add FTP transport
230
        """
1185.72.13 by Matthieu Moy
Make ftp put atomic
231
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (self._abspath(relpath), time.time(),
232
                        os.getpid(), random.randint(0,0x7FFFFFFF))
1185.36.4 by Daniel Silverstone
Add FTP transport
233
        if not hasattr(fp, 'read'):
234
            fp = StringIO(fp)
235
        try:
236
            mutter("FTP put: %s" % self._abspath(relpath))
237
            f = self._get_FTP()
1185.72.13 by Matthieu Moy
Make ftp put atomic
238
            try:
239
                f.storbinary('STOR '+tmp_abspath, fp)
240
                f.rename(tmp_abspath, self._abspath(relpath))
241
            except (ftplib.error_temp,EOFError), e:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
242
                warning("Failure during ftp PUT. Deleting temporary file.")
1185.72.13 by Matthieu Moy
Make ftp put atomic
243
                try:
244
                    f.delete(tmp_abspath)
245
                except:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
246
                    warning("Failed to delete temporary file on the server.\nFile: %s"
247
                            % tmp_abspath)
1185.72.13 by Matthieu Moy
Make ftp put atomic
248
                    raise e
249
                raise
1185.36.4 by Daniel Silverstone
Add FTP transport
250
        except ftplib.error_perm, e:
1185.36.6 by Daniel Silverstone
Make sure put raises NoSuchFile on appropriate errors. Also ensure we only ever connect once. Seriously. Really.
251
            if "no such file" in str(e).lower():
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
252
                raise NoSuchFile("Error storing %s: %s"
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
253
                                 % (self.abspath(relpath), str(e)), extra=e)
1185.36.6 by Daniel Silverstone
Make sure put raises NoSuchFile on appropriate errors. Also ensure we only ever connect once. Seriously. Really.
254
            else:
255
                raise FtpTransportError(orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
256
        except ftplib.error_temp, e:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
257
            if retries > _number_of_retries:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
258
                raise TransportError("FTP temporary error during PUT %s. Aborting."
259
                                     % self.abspath(relpath), orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
260
            else:
261
                warning("FTP temporary error: %s. Retrying." % str(e))
262
                self._FTP_instance = None
263
                self.put(relpath, fp, mode, retries+1)
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
264
        except EOFError:
1185.72.10 by Matthieu Moy
One 1 -> _number_of_retries was missing
265
            if retries > _number_of_retries:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
266
                raise TransportError("FTP control connection closed during PUT %s."
267
                                     % self.abspath(relpath), orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
268
            else:
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
269
                warning("FTP control connection closed. Trying to reopen.")
270
                time.sleep(_sleep_between_retries)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
271
                self._FTP_instance = None
272
                self.put(relpath, fp, mode, retries+1)
273
1185.36.4 by Daniel Silverstone
Add FTP transport
274
1185.58.2 by John Arbash Meinel
Added mode to the appropriate transport functions, and tests to make sure they work.
275
    def mkdir(self, relpath, mode=None):
1185.36.4 by Daniel Silverstone
Add FTP transport
276
        """Create a directory at the given path."""
277
        try:
278
            mutter("FTP mkd: %s" % self._abspath(relpath))
279
            f = self._get_FTP()
280
            try:
281
                f.mkd(self._abspath(relpath))
282
            except ftplib.error_perm, e:
283
                s = str(e)
284
                if 'File exists' in s:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
285
                    raise FileExists(self.abspath(relpath), extra=s)
1185.36.4 by Daniel Silverstone
Add FTP transport
286
                else:
287
                    raise
288
        except ftplib.error_perm, e:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
289
            raise TransportError(orig_error=e)
1185.36.4 by Daniel Silverstone
Add FTP transport
290
291
    def append(self, relpath, f):
292
        """Append the text in the file-like object into the final
293
        location.
294
        """
295
        raise TransportNotPossible('ftp does not support append()')
296
297
    def copy(self, rel_from, rel_to):
298
        """Copy the item at rel_from to the location at rel_to"""
299
        raise TransportNotPossible('ftp does not (yet) support copy()')
300
301
    def move(self, rel_from, rel_to):
302
        """Move the item at rel_from to the location at rel_to"""
303
        try:
304
            mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
305
                                         self._abspath(rel_to)))
306
            f = self._get_FTP()
307
            f.rename(self._abspath(rel_from), self._abspath(rel_to))
308
        except ftplib.error_perm, e:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
309
            raise TransportError(orig_error=e)
1185.36.4 by Daniel Silverstone
Add FTP transport
310
311
    def delete(self, relpath):
312
        """Delete the item at relpath"""
313
        try:
314
            mutter("FTP rm: %s" % self._abspath(relpath))
315
            f = self._get_FTP()
316
            f.delete(self._abspath(relpath))
317
        except ftplib.error_perm, e:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
318
            raise TransportError(orig_error=e)
1185.36.4 by Daniel Silverstone
Add FTP transport
319
320
    def listable(self):
321
        """See Transport.listable."""
322
        return True
323
324
    def list_dir(self, relpath):
325
        """See Transport.list_dir."""
326
        try:
327
            mutter("FTP nlst: %s" % self._abspath(relpath))
328
            f = self._get_FTP()
329
            basepath = self._abspath(relpath)
330
            # FTP.nlst returns paths prefixed by relpath, strip 'em
331
            the_list = f.nlst(basepath)
332
            stripped = [path[len(basepath)+1:] for path in the_list]
333
            # Remove . and .. if present, and return
334
            return [path for path in stripped if path not in (".", "..")]
335
        except ftplib.error_perm, e:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
336
            raise TransportError(orig_error=e)
1185.36.4 by Daniel Silverstone
Add FTP transport
337
338
    def iter_files_recursive(self):
339
        """See Transport.iter_files_recursive.
340
341
        This is cargo-culted from the SFTP transport"""
342
        mutter("FTP iter_files_recursive")
343
        queue = list(self.list_dir("."))
344
        while queue:
345
            relpath = urllib.quote(queue.pop(0))
346
            st = self.stat(relpath)
347
            if stat.S_ISDIR(st.st_mode):
348
                for i, basename in enumerate(self.list_dir(relpath)):
349
                    queue.insert(i, relpath+"/"+basename)
350
            else:
351
                yield relpath
352
353
    def stat(self, relpath):
354
        """Return the stat information for a file.
355
        """
356
        try:
357
            mutter("FTP stat: %s" % self._abspath(relpath))
358
            f = self._get_FTP()
359
            return FtpStatResult(f, self._abspath(relpath))
360
        except ftplib.error_perm, e:
1185.72.16 by Matthieu Moy
Raise NoSuchFile in stat when this is the case
361
            if "no such file" in str(e).lower():
362
                raise NoSuchFile("Error storing %s: %s"
363
                                 % (self.abspath(relpath), str(e)), extra=e)
364
            else:
365
                raise FtpTransportError(orig_error=e)
1185.36.4 by Daniel Silverstone
Add FTP transport
366
367
    def lock_read(self, relpath):
368
        """Lock the given file for shared (read) access.
369
        :return: A lock object, which should be passed to Transport.unlock()
370
        """
371
        # The old RemoteBranch ignore lock for reading, so we will
372
        # continue that tradition and return a bogus lock object.
373
        class BogusLock(object):
374
            def __init__(self, path):
375
                self.path = path
376
            def unlock(self):
377
                pass
378
        return BogusLock(relpath)
379
380
    def lock_write(self, relpath):
381
        """Lock the given file for exclusive (write) access.
382
        WARNING: many transports do not support this, so trying avoid using it
383
384
        :return: A lock object, which should be passed to Transport.unlock()
385
        """
386
        return self.lock_read(relpath)
1530.1.11 by Robert Collins
Push the transport permutations list into each transport module allowing for automatic testing of new modules that are registered as transports.
387
388
389
def get_test_permutations():
390
    """Return the permutations to be used in testing."""
391
    warn("There are no FTP transport provider tests yet.")
392
    return []