~bzr-pqm/bzr/bzr.dev

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