~bzr-pqm/bzr/bzr.dev

1185.36.4 by Daniel Silverstone
Add FTP transport
1
# Copyright (C) 2005 Canonical Ltd
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
2
#
1185.36.4 by Daniel Silverstone
Add FTP transport
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.
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
7
#
1185.36.4 by Daniel Silverstone
Add FTP transport
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.
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
12
#
1185.36.4 by Daniel Silverstone
Add FTP transport
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
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
28
import asyncore
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.
29
import errno
1185.36.4 by Daniel Silverstone
Add FTP transport
30
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.
31
import os
32
import urllib
1185.36.4 by Daniel Silverstone
Add FTP transport
33
import urlparse
34
import stat
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
35
import threading
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
36
import time
1185.72.13 by Matthieu Moy
Make ftp put atomic
37
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.
38
from warnings import warn
39
1948.1.7 by John Arbash Meinel
Fix FtpTransport so that it raises UnicodeError and skips the unicode path tests
40
from bzrlib import (
41
    errors,
42
    urlutils,
43
    )
44
from bzrlib.trace import mutter, warning
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
45
from bzrlib.transport import (
46
    Server,
47
    split_url,
1948.1.7 by John Arbash Meinel
Fix FtpTransport so that it raises UnicodeError and skips the unicode path tests
48
    Transport,
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
49
    )
1707.3.24 by John Arbash Meinel
Add prompt for password for ftp.
50
import bzrlib.ui
1551.3.11 by Aaron Bentley
Merge from Robert
51
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
52
_have_medusa = False
53
1551.3.11 by Aaron Bentley
Merge from Robert
54
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
55
class FtpPathError(errors.PathError):
56
    """FTP failed for path: %(path)s%(extra)s"""
57
58
1551.3.11 by Aaron Bentley
Merge from Robert
59
_FTP_cache = {}
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
60
def _find_FTP(hostname, port, username, password, is_active):
1551.3.11 by Aaron Bentley
Merge from Robert
61
    """Find an ftplib.FTP instance attached to this triplet."""
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
62
    key = (hostname, port, username, password, is_active)
63
    alt_key = (hostname, port, username, '********', is_active)
1551.3.11 by Aaron Bentley
Merge from Robert
64
    if key not in _FTP_cache:
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
65
        mutter("Constructing FTP instance against %r" % (alt_key,))
1707.3.5 by John Arbash Meinel
Adding port to ftp connections, and not logging the ftp password
66
        conn = ftplib.FTP()
67
68
        conn.connect(host=hostname, port=port)
1707.3.24 by John Arbash Meinel
Add prompt for password for ftp.
69
        if username and username != 'anonymous' and not password:
70
            password = bzrlib.ui.ui_factory.get_password(
71
                prompt='FTP %(user)s@%(host)s password',
72
                user=username, host=hostname)
1707.3.5 by John Arbash Meinel
Adding port to ftp connections, and not logging the ftp password
73
        conn.login(user=username, passwd=password)
74
        conn.set_pasv(not is_active)
75
76
        _FTP_cache[key] = conn
77
1551.3.11 by Aaron Bentley
Merge from Robert
78
    return _FTP_cache[key]    
79
80
1185.36.4 by Daniel Silverstone
Add FTP transport
81
class FtpStatResult(object):
82
    def __init__(self, f, relpath):
83
        try:
84
            self.st_size = f.size(relpath)
85
            self.st_mode = stat.S_IFREG
86
        except ftplib.error_perm:
87
            pwd = f.pwd()
88
            try:
89
                f.cwd(relpath)
90
                self.st_mode = stat.S_IFDIR
91
            finally:
92
                f.cwd(pwd)
93
94
1185.72.15 by Matthieu Moy
better error messages on ftp failures
95
_number_of_retries = 2
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
96
_sleep_between_retries = 5
1185.72.12 by Matthieu Moy
made __number_of_retries global
97
1185.36.4 by Daniel Silverstone
Add FTP transport
98
class FtpTransport(Transport):
99
    """This is the transport agent for ftp:// access."""
100
101
    def __init__(self, base, _provided_instance=None):
102
        """Set the base path where files will be stored."""
103
        assert base.startswith('ftp://') or base.startswith('aftp://')
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
104
1185.36.4 by Daniel Silverstone
Add FTP transport
105
        self.is_active = base.startswith('aftp://')
106
        if self.is_active:
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
107
            # urlparse won't handle aftp://
1185.36.4 by Daniel Silverstone
Add FTP transport
108
            base = base[1:]
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
109
        if not base.endswith('/'):
110
            base += '/'
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
111
        (self._proto, self._username,
112
            self._password, self._host,
113
            self._port, self._path) = split_url(base)
114
        base = self._unparse_url()
115
116
        super(FtpTransport, self).__init__(base)
1185.36.4 by Daniel Silverstone
Add FTP transport
117
        self._FTP_instance = _provided_instance
118
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
119
    def _unparse_url(self, path=None):
120
        if path is None:
121
            path = self._path
122
        path = urllib.quote(path)
123
        netloc = urllib.quote(self._host)
124
        if self._username is not None:
125
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
126
        if self._port is not None:
127
            netloc = '%s:%d' % (netloc, self._port)
1952.1.1 by John Arbash Meinel
Ghozzy: Fix Bzr's support of Active FTP (aftp://)
128
        proto = 'ftp'
129
        if self.is_active:
130
            proto = 'aftp'
131
        return urlparse.urlunparse((proto, netloc, path, '', '', ''))
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
132
1185.36.4 by Daniel Silverstone
Add FTP transport
133
    def _get_FTP(self):
134
        """Return the ftplib.FTP instance for this object."""
135
        if self._FTP_instance is not None:
136
            return self._FTP_instance
137
        
138
        try:
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
139
            self._FTP_instance = _find_FTP(self._host, self._port,
140
                                           self._username, self._password,
1551.3.11 by Aaron Bentley
Merge from Robert
141
                                           self.is_active)
1185.36.4 by Daniel Silverstone
Add FTP transport
142
            return self._FTP_instance
143
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
144
            raise errors.TransportError(msg="Error setting up connection: %s"
1185.36.4 by Daniel Silverstone
Add FTP transport
145
                                    % str(e), orig_error=e)
146
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
147
    def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
148
        """Try to translate an ftplib.error_perm exception.
149
150
        :param err: The error to translate into a bzr error
151
        :param path: The path which had problems
152
        :param extra: Extra information which can be included
153
        :param unknown_exc: If None, we will just raise the original exception
154
                    otherwise we raise unknown_exc(path, extra=extra)
155
        """
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
156
        s = str(err).lower()
157
        if not extra:
158
            extra = str(err)
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
159
        else:
160
            extra += ': ' + str(err)
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
161
        if ('no such file' in s
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
162
            or 'could not open' in s
163
            or 'no such dir' in s
164
            ):
165
            raise errors.NoSuchFile(path, extra=extra)
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
166
        if ('file exists' in s):
1707.3.15 by John Arbash Meinel
Handle trying to list a file instead of a dir
167
            raise errors.FileExists(path, extra=extra)
168
        if ('not a directory' in s):
169
            raise errors.PathError(path, extra=extra)
170
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
171
        mutter('unable to understand error for path: %s: %s', path, err)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
172
173
        if unknown_exc:
174
            raise unknown_exc(path, extra=extra)
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
175
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
176
        #       something like TransportError, but this loses the traceback
1707.3.15 by John Arbash Meinel
Handle trying to list a file instead of a dir
177
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
178
        #       to handle. Consider doing something like that here.
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
179
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
1707.3.15 by John Arbash Meinel
Handle trying to list a file instead of a dir
180
        raise
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
181
1185.36.4 by Daniel Silverstone
Add FTP transport
182
    def should_cache(self):
183
        """Return True if the data pulled across should be cached locally.
184
        """
185
        return True
186
187
    def clone(self, offset=None):
188
        """Return a new FtpTransport with root at self.base + offset.
189
        """
190
        mutter("FTP clone")
191
        if offset is None:
192
            return FtpTransport(self.base, self._FTP_instance)
193
        else:
194
            return FtpTransport(self.abspath(offset), self._FTP_instance)
195
196
    def _abspath(self, relpath):
197
        assert isinstance(relpath, basestring)
1948.1.7 by John Arbash Meinel
Fix FtpTransport so that it raises UnicodeError and skips the unicode path tests
198
        relpath = urlutils.unescape(relpath)
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
199
        relpath_parts = relpath.split('/')
1185.36.4 by Daniel Silverstone
Add FTP transport
200
        if len(relpath_parts) > 1:
201
            if relpath_parts[0] == '':
202
                raise ValueError("path %r within branch %r seems to be absolute"
203
                                 % (relpath, self._path))
204
        basepath = self._path.split('/')
205
        if len(basepath) > 0 and basepath[-1] == '':
206
            basepath = basepath[:-1]
207
        for p in relpath_parts:
208
            if p == '..':
209
                if len(basepath) == 0:
210
                    # In most filesystems, a request for the parent
211
                    # of root, just returns root.
212
                    continue
213
                basepath.pop()
214
            elif p == '.' or p == '':
215
                continue # No-op
216
            else:
217
                basepath.append(p)
218
        # Possibly, we could use urlparse.urljoin() here, but
219
        # I'm concerned about when it chooses to strip the last
220
        # portion of the path, and when it doesn't.
1948.1.7 by John Arbash Meinel
Fix FtpTransport so that it raises UnicodeError and skips the unicode path tests
221
222
        # XXX: It seems that ftplib does not handle Unicode paths
223
        # at the same time, medusa won't handle utf8 paths
224
        # So if we .encode(utf8) here, then we get a Server failure.
225
        # while if we use str(), we get a UnicodeError, and the test suite
226
        # just skips testing UnicodePaths.
227
        return str('/'.join(basepath) or '/')
1185.36.4 by Daniel Silverstone
Add FTP transport
228
    
229
    def abspath(self, relpath):
230
        """Return the full url to the given relative path.
231
        This can be supplied with a string or a list
232
        """
233
        path = self._abspath(relpath)
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
234
        return self._unparse_url(path)
1185.36.4 by Daniel Silverstone
Add FTP transport
235
236
    def has(self, relpath):
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
237
        """Does the target location exist?"""
238
        # FIXME jam 20060516 We *do* ask about directories in the test suite
239
        #       We don't seem to in the actual codebase
240
        # XXX: I assume we're never asked has(dirname) and thus I use
241
        # the FTP size command and assume that if it doesn't raise,
242
        # all is good.
243
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
244
        try:
245
            f = self._get_FTP()
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
246
            mutter('FTP has check: %s => %s', relpath, abspath)
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
247
            s = f.size(abspath)
248
            mutter("FTP has: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
249
            return True
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
250
        except ftplib.error_perm, e:
251
            if ('is a directory' in str(e).lower()):
252
                mutter("FTP has dir: %s: %s", abspath, e)
253
                return True
254
            mutter("FTP has not: %s: %s", abspath, e)
1185.36.4 by Daniel Silverstone
Add FTP transport
255
            return False
256
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
257
    def get(self, relpath, decode=False, retries=0):
1185.36.4 by Daniel Silverstone
Add FTP transport
258
        """Get the file at the given relative path.
259
260
        :param relpath: The relative path to the file
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
261
        :param retries: Number of retries after temporary failures so far
262
                        for this operation.
1185.36.4 by Daniel Silverstone
Add FTP transport
263
264
        We're meant to return a file-like object which bzr will
265
        then read from. For now we do this via the magic of StringIO
266
        """
1540.3.6 by Martin Pool
[merge] update from bzr.dev
267
        # TODO: decode should be deprecated
1185.36.4 by Daniel Silverstone
Add FTP transport
268
        try:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
269
            mutter("FTP get: %s", self._abspath(relpath))
1185.36.4 by Daniel Silverstone
Add FTP transport
270
            f = self._get_FTP()
271
            ret = StringIO()
272
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
273
            ret.seek(0)
274
            return ret
275
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
276
            raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
277
        except ftplib.error_temp, e:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
278
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
279
                raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
280
                                     % self.abspath(relpath),
281
                                     orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
282
            else:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
283
                warning("FTP temporary error: %s. Retrying.", str(e))
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
284
                self._FTP_instance = None
285
                return self.get(relpath, decode, retries+1)
1185.72.15 by Matthieu Moy
better error messages on ftp failures
286
        except EOFError, e:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
287
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
288
                raise errors.TransportError("FTP control connection closed during GET %s."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
289
                                     % self.abspath(relpath),
290
                                     orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
291
            else:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
292
                warning("FTP control connection closed. Trying to reopen.")
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
293
                time.sleep(_sleep_between_retries)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
294
                self._FTP_instance = None
295
                return self.get(relpath, decode, retries+1)
1185.36.4 by Daniel Silverstone
Add FTP transport
296
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
297
    def put(self, relpath, fp, mode=None, retries=0):
1185.36.4 by Daniel Silverstone
Add FTP transport
298
        """Copy the file-like or string object into the location.
299
300
        :param relpath: Location to put the contents, relative to base.
1185.72.13 by Matthieu Moy
Make ftp put atomic
301
        :param fp:       File-like or string object.
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
302
        :param retries: Number of retries after temporary failures so far
303
                        for this operation.
304
1185.58.2 by John Arbash Meinel
Added mode to the appropriate transport functions, and tests to make sure they work.
305
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
1185.36.4 by Daniel Silverstone
Add FTP transport
306
        """
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
307
        abspath = self._abspath(relpath)
308
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
1185.72.13 by Matthieu Moy
Make ftp put atomic
309
                        os.getpid(), random.randint(0,0x7FFFFFFF))
1185.36.4 by Daniel Silverstone
Add FTP transport
310
        if not hasattr(fp, 'read'):
311
            fp = StringIO(fp)
312
        try:
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
313
            mutter("FTP put: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
314
            f = self._get_FTP()
1185.72.13 by Matthieu Moy
Make ftp put atomic
315
            try:
316
                f.storbinary('STOR '+tmp_abspath, fp)
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
317
                f.rename(tmp_abspath, abspath)
1185.72.13 by Matthieu Moy
Make ftp put atomic
318
            except (ftplib.error_temp,EOFError), e:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
319
                warning("Failure during ftp PUT. Deleting temporary file.")
1185.72.13 by Matthieu Moy
Make ftp put atomic
320
                try:
321
                    f.delete(tmp_abspath)
322
                except:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
323
                    warning("Failed to delete temporary file on the"
324
                            " server.\nFile: %s", tmp_abspath)
1185.72.13 by Matthieu Moy
Make ftp put atomic
325
                    raise e
326
                raise
1185.36.4 by Daniel Silverstone
Add FTP transport
327
        except ftplib.error_perm, e:
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
328
            self._translate_perm_error(e, abspath, extra='could not store')
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
329
        except ftplib.error_temp, e:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
330
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
331
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
332
                                     % self.abspath(relpath), orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
333
            else:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
334
                warning("FTP temporary error: %s. Retrying.", str(e))
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
335
                self._FTP_instance = None
336
                self.put(relpath, fp, mode, retries+1)
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
337
        except EOFError:
1185.72.10 by Matthieu Moy
One 1 -> _number_of_retries was missing
338
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
339
                raise errors.TransportError("FTP control connection closed during PUT %s."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
340
                                     % self.abspath(relpath), orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
341
            else:
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
342
                warning("FTP control connection closed. Trying to reopen.")
343
                time.sleep(_sleep_between_retries)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
344
                self._FTP_instance = None
345
                self.put(relpath, fp, mode, retries+1)
346
1185.58.2 by John Arbash Meinel
Added mode to the appropriate transport functions, and tests to make sure they work.
347
    def mkdir(self, relpath, mode=None):
1185.36.4 by Daniel Silverstone
Add FTP transport
348
        """Create a directory at the given path."""
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
349
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
350
        try:
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
351
            mutter("FTP mkd: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
352
            f = self._get_FTP()
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
353
            f.mkd(abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
354
        except ftplib.error_perm, e:
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
355
            self._translate_perm_error(e, abspath,
356
                unknown_exc=errors.FileExists)
1185.36.4 by Daniel Silverstone
Add FTP transport
357
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
358
    def rmdir(self, rel_path):
359
        """Delete the directory at rel_path"""
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
360
        abspath = self._abspath(rel_path)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
361
        try:
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
362
            mutter("FTP rmd: %s", abspath)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
363
            f = self._get_FTP()
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
364
            f.rmd(abspath)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
365
        except ftplib.error_perm, e:
1707.3.21 by John Arbash Meinel
rmdir can raise PathError when it cannot delete a directory.
366
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
367
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
368
    def append(self, relpath, f, mode=None):
1185.36.4 by Daniel Silverstone
Add FTP transport
369
        """Append the text in the file-like object into the final
370
        location.
371
        """
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
372
        abspath = self._abspath(relpath)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
373
        if self.has(relpath):
374
            ftp = self._get_FTP()
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
375
            result = ftp.size(abspath)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
376
        else:
377
            result = 0
378
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
379
        mutter("FTP appe to %s", abspath)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
380
        self._try_append(relpath, f.read(), mode)
381
382
        return result
383
384
    def _try_append(self, relpath, text, mode=None, retries=0):
385
        """Try repeatedly to append the given text to the file at relpath.
386
        
387
        This is a recursive function. On errors, it will be called until the
388
        number of retries is exceeded.
389
        """
390
        try:
391
            abspath = self._abspath(relpath)
392
            mutter("FTP appe (try %d) to %s", retries, abspath)
393
            ftp = self._get_FTP()
394
            ftp.voidcmd("TYPE I")
395
            cmd = "APPE %s" % abspath
396
            conn = ftp.transfercmd(cmd)
397
            conn.sendall(text)
398
            conn.close()
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
399
            if mode:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
400
                self._setmode(relpath, mode)
401
            ftp.getresp()
402
        except ftplib.error_perm, e:
1707.3.26 by John Arbash Meinel
Added a default exception for append
403
            self._translate_perm_error(e, abspath, extra='error appending',
404
                unknown_exc=errors.NoSuchFile)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
405
        except ftplib.error_temp, e:
406
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
407
                raise errors.TransportError("FTP temporary error during APPEND %s." \
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
408
                        "Aborting." % abspath, orig_error=e)
409
            else:
410
                warning("FTP temporary error: %s. Retrying.", str(e))
411
                self._FTP_instance = None
412
                self._try_append(relpath, text, mode, retries+1)
413
414
    def _setmode(self, relpath, mode):
415
        """Set permissions on a path.
416
417
        Only set permissions if the FTP server supports the 'SITE CHMOD'
418
        extension.
419
        """
420
        try:
421
            mutter("FTP site chmod: setting permissions to %s on %s",
422
                str(mode), self._abspath(relpath))
423
            ftp = self._get_FTP()
424
            cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
425
            ftp.sendcmd(cmd)
426
        except ftplib.error_perm, e:
427
            # Command probably not available on this server
428
            warning("FTP Could not set permissions to %s on %s. %s",
429
                    str(mode), self._abspath(relpath), str(e))
1185.36.4 by Daniel Silverstone
Add FTP transport
430
1707.3.11 by John Arbash Meinel
fixing more tests.
431
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
432
    #       to copy something to another machine. And you may be able
433
    #       to give it its own address as the 'to' location.
434
    #       So implement a fancier 'copy()'
1185.36.4 by Daniel Silverstone
Add FTP transport
435
436
    def move(self, rel_from, rel_to):
437
        """Move the item at rel_from to the location at rel_to"""
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
438
        abs_from = self._abspath(rel_from)
439
        abs_to = self._abspath(rel_to)
1185.36.4 by Daniel Silverstone
Add FTP transport
440
        try:
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
441
            mutter("FTP mv: %s => %s", abs_from, abs_to)
1185.36.4 by Daniel Silverstone
Add FTP transport
442
            f = self._get_FTP()
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
443
            f.rename(abs_from, abs_to)
1185.36.4 by Daniel Silverstone
Add FTP transport
444
        except ftplib.error_perm, e:
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
445
            self._translate_perm_error(e, abs_from,
446
                extra='unable to rename to %r' % (rel_to,), 
447
                unknown_exc=errors.PathError)
1185.36.4 by Daniel Silverstone
Add FTP transport
448
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
449
    rename = move
450
1185.36.4 by Daniel Silverstone
Add FTP transport
451
    def delete(self, relpath):
452
        """Delete the item at relpath"""
1707.3.11 by John Arbash Meinel
fixing more tests.
453
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
454
        try:
1707.3.11 by John Arbash Meinel
fixing more tests.
455
            mutter("FTP rm: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
456
            f = self._get_FTP()
1707.3.11 by John Arbash Meinel
fixing more tests.
457
            f.delete(abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
458
        except ftplib.error_perm, e:
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
459
            self._translate_perm_error(e, abspath, 'error deleting',
460
                unknown_exc=errors.NoSuchFile)
1185.36.4 by Daniel Silverstone
Add FTP transport
461
462
    def listable(self):
463
        """See Transport.listable."""
464
        return True
465
466
    def list_dir(self, relpath):
467
        """See Transport.list_dir."""
1959.2.5 by John Arbash Meinel
Small fixes in ftp.list_dir()
468
        basepath = self._abspath(relpath)
469
        mutter("FTP nlst: %s", basepath)
1959.2.1 by John Arbash Meinel
David Allouche: Make transports return escaped paths
470
        f = self._get_FTP()
1185.36.4 by Daniel Silverstone
Add FTP transport
471
        try:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
472
            paths = f.nlst(basepath)
1185.36.4 by Daniel Silverstone
Add FTP transport
473
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
474
            self._translate_perm_error(e, relpath, extra='error with list_dir')
1959.2.1 by John Arbash Meinel
David Allouche: Make transports return escaped paths
475
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
476
        if paths and paths[0].startswith(basepath):
477
            entries = [path[len(basepath)+1:] for path in paths]
478
        else:
479
            entries = paths
480
        # Remove . and .. if present
1959.2.5 by John Arbash Meinel
Small fixes in ftp.list_dir()
481
        return [urlutils.escape(entry) for entry in entries
482
                if entry not in ('.', '..')]
1185.36.4 by Daniel Silverstone
Add FTP transport
483
484
    def iter_files_recursive(self):
485
        """See Transport.iter_files_recursive.
486
487
        This is cargo-culted from the SFTP transport"""
488
        mutter("FTP iter_files_recursive")
489
        queue = list(self.list_dir("."))
490
        while queue:
1959.2.1 by John Arbash Meinel
David Allouche: Make transports return escaped paths
491
            relpath = queue.pop(0)
1185.36.4 by Daniel Silverstone
Add FTP transport
492
            st = self.stat(relpath)
493
            if stat.S_ISDIR(st.st_mode):
494
                for i, basename in enumerate(self.list_dir(relpath)):
495
                    queue.insert(i, relpath+"/"+basename)
496
            else:
497
                yield relpath
498
499
    def stat(self, relpath):
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
500
        """Return the stat information for a file."""
501
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
502
        try:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
503
            mutter("FTP stat: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
504
            f = self._get_FTP()
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
505
            return FtpStatResult(f, abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
506
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
507
            self._translate_perm_error(e, abspath, extra='error w/ stat')
1185.36.4 by Daniel Silverstone
Add FTP transport
508
509
    def lock_read(self, relpath):
510
        """Lock the given file for shared (read) access.
511
        :return: A lock object, which should be passed to Transport.unlock()
512
        """
513
        # The old RemoteBranch ignore lock for reading, so we will
514
        # continue that tradition and return a bogus lock object.
515
        class BogusLock(object):
516
            def __init__(self, path):
517
                self.path = path
518
            def unlock(self):
519
                pass
520
        return BogusLock(relpath)
521
522
    def lock_write(self, relpath):
523
        """Lock the given file for exclusive (write) access.
524
        WARNING: many transports do not support this, so trying avoid using it
525
526
        :return: A lock object, which should be passed to Transport.unlock()
527
        """
528
        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.
529
530
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
531
class FtpServer(Server):
532
    """Common code for SFTP server facilities."""
533
534
    def __init__(self):
535
        self._root = None
536
        self._ftp_server = None
537
        self._port = None
538
        self._async_thread = None
539
        # ftp server logs
540
        self.logs = []
541
542
    def get_url(self):
543
        """Calculate an ftp url to this server."""
544
        return 'ftp://foo:bar@localhost:%d/' % (self._port)
545
546
#    def get_bogus_url(self):
547
#        """Return a URL which cannot be connected to."""
548
#        return 'ftp://127.0.0.1:1'
549
550
    def log(self, message):
551
        """This is used by medusa.ftp_server to log connections, etc."""
552
        self.logs.append(message)
553
554
    def setUp(self):
555
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
556
        if not _have_medusa:
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
557
            raise RuntimeError('Must have medusa to run the FtpServer')
558
559
        self._root = os.getcwdu()
560
        self._ftp_server = _ftp_server(
561
            authorizer=_test_authorizer(root=self._root),
562
            ip='localhost',
563
            port=0, # bind to a random port
564
            resolver=None,
565
            logger_object=self # Use FtpServer.log() for messages
566
            )
567
        self._port = self._ftp_server.getsockname()[1]
568
        # Don't let it loop forever, or handle an infinite number of requests.
569
        # In this case it will run for 100s, or 1000 requests
570
        self._async_thread = threading.Thread(target=asyncore.loop,
571
                kwargs={'timeout':0.1, 'count':1000})
572
        self._async_thread.setDaemon(True)
573
        self._async_thread.start()
574
575
    def tearDown(self):
576
        """See bzrlib.transport.Server.tearDown."""
577
        # have asyncore release the channel
578
        self._ftp_server.del_channel()
579
        asyncore.close_all()
580
        self._async_thread.join()
581
582
583
_ftp_channel = None
584
_ftp_server = None
585
_test_authorizer = None
586
587
588
def _setup_medusa():
589
    global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
590
    try:
591
        import medusa
592
        import medusa.filesys
593
        import medusa.ftp_server
594
    except ImportError:
595
        return False
596
597
    _have_medusa = True
598
599
    class test_authorizer(object):
600
        """A custom Authorizer object for running the test suite.
601
602
        The reason we cannot use dummy_authorizer, is because it sets the
603
        channel to readonly, which we don't always want to do.
604
        """
605
606
        def __init__(self, root):
607
            self.root = root
608
609
        def authorize(self, channel, username, password):
610
            """Return (success, reply_string, filesystem)"""
611
            if not _have_medusa:
612
                return 0, 'No Medusa.', None
613
614
            channel.persona = -1, -1
615
            if username == 'anonymous':
616
                channel.read_only = 1
617
            else:
618
                channel.read_only = 0
619
620
            return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
621
622
623
    class ftp_channel(medusa.ftp_server.ftp_channel):
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
624
        """Customized ftp channel"""
625
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
626
        def log(self, message):
627
            """Redirect logging requests."""
628
            mutter('_ftp_channel: %s', message)
629
            
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
630
        def log_info(self, message, type='info'):
631
            """Redirect logging requests."""
632
            mutter('_ftp_channel %s: %s', type, message)
633
            
634
        def cmd_rnfr(self, line):
635
            """Prepare for renaming a file."""
636
            self._renaming = line[1]
637
            self.respond('350 Ready for RNTO')
638
            # TODO: jam 20060516 in testing, the ftp server seems to
639
            #       check that the file already exists, or it sends
640
            #       550 RNFR command failed
641
642
        def cmd_rnto(self, line):
643
            """Rename a file based on the target given.
644
645
            rnto must be called after calling rnfr.
646
            """
647
            if not self._renaming:
648
                self.respond('503 RNFR required first.')
649
            pfrom = self.filesystem.translate(self._renaming)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
650
            self._renaming = None
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
651
            pto = self.filesystem.translate(line[1])
652
            try:
653
                os.rename(pfrom, pto)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
654
            except (IOError, OSError), e:
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
655
                # TODO: jam 20060516 return custom responses based on
656
                #       why the command failed
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
657
                self.respond('550 RNTO failed: %s' % (e,))
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
658
            except:
659
                self.respond('550 RNTO failed')
660
                # For a test server, we will go ahead and just die
661
                raise
1707.3.20 by John Arbash Meinel
Found the problem, was sending 2 responses
662
            else:
663
                self.respond('250 Rename successful.')
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
664
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
665
        def cmd_size(self, line):
666
            """Return the size of a file
667
668
            This is overloaded to help the test suite determine if the 
669
            target is a directory.
670
            """
671
            filename = line[1]
672
            if not self.filesystem.isfile(filename):
673
                if self.filesystem.isdir(filename):
674
                    self.respond('550 "%s" is a directory' % (filename,))
675
                else:
676
                    self.respond('550 "%s" is not a file' % (filename,))
677
            else:
678
                self.respond('213 %d' 
1707.3.17 by John Arbash Meinel
Fix errors in cmd_size
679
                    % (self.filesystem.stat(filename)[stat.ST_SIZE]),)
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
680
681
        def cmd_mkd(self, line):
682
            """Create a directory.
683
684
            Overloaded because default implementation does not distinguish
685
            *why* it cannot make a directory.
686
            """
687
            if len (line) != 2:
1773.4.1 by Martin Pool
Add pyflakes makefile target; fix many warnings
688
                self.command_not_understood(''.join(line))
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
689
            else:
690
                path = line[1]
691
                try:
692
                    self.filesystem.mkdir (path)
693
                    self.respond ('257 MKD command successful.')
694
                except (IOError, OSError), e:
695
                    self.respond ('550 error creating directory: %s' % (e,))
696
                except:
697
                    self.respond ('550 error creating directory.')
698
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
699
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
700
    class ftp_server(medusa.ftp_server.ftp_server):
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
701
        """Customize the behavior of the Medusa ftp_server.
702
703
        There are a few warts on the ftp_server, based on how it expects
704
        to be used.
705
        """
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
706
        _renaming = None
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
707
        ftp_channel_class = ftp_channel
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
708
709
        def __init__(self, *args, **kwargs):
710
            mutter('Initializing _ftp_server: %r, %r', args, kwargs)
711
            medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
712
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
713
        def log(self, message):
714
            """Redirect logging requests."""
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
715
            mutter('_ftp_server: %s', message)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
716
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
717
        def log_info(self, message, type='info'):
718
            """Override the asyncore.log_info so we don't stipple the screen."""
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
719
            mutter('_ftp_server %s: %s', type, message)
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
720
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
721
    _test_authorizer = test_authorizer
722
    _ftp_channel = ftp_channel
723
    _ftp_server = ftp_server
724
725
    return True
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
726
727
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.
728
def get_test_permutations():
729
    """Return the permutations to be used in testing."""
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
730
    if not _setup_medusa():
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
731
        warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
732
        return []
733
    else:
734
        return [(FtpTransport, FtpServer)]