~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-07-06 03:15:29 UTC
  • mfrom: (1711.2.78 jam-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20060706031529-e189d8c3f42076be
(jam) allow plugins to include benchmarks

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
 
#
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
#
 
12
 
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29
29
import errno
30
30
import ftplib
31
31
import os
32
 
import os.path
33
32
import urllib
34
33
import urlparse
35
 
import select
36
34
import stat
37
35
import threading
38
36
import time
39
37
import random
40
38
from warnings import warn
41
39
 
42
 
from bzrlib import (
43
 
    errors,
44
 
    osutils,
45
 
    urlutils,
46
 
    )
47
 
from bzrlib.trace import mutter, warning
48
40
from bzrlib.transport import (
 
41
    Transport,
49
42
    Server,
50
 
    ConnectedTransport,
 
43
    split_url,
51
44
    )
52
 
from bzrlib.transport.local import LocalURLServer
 
45
import bzrlib.errors as errors
 
46
from bzrlib.trace import mutter, warning
53
47
import bzrlib.ui
54
48
 
55
49
_have_medusa = False
59
53
    """FTP failed for path: %(path)s%(extra)s"""
60
54
 
61
55
 
 
56
_FTP_cache = {}
 
57
def _find_FTP(hostname, port, username, password, is_active):
 
58
    """Find an ftplib.FTP instance attached to this triplet."""
 
59
    key = (hostname, port, username, password, is_active)
 
60
    alt_key = (hostname, port, username, '********', is_active)
 
61
    if key not in _FTP_cache:
 
62
        mutter("Constructing FTP instance against %r" % (alt_key,))
 
63
        conn = ftplib.FTP()
 
64
 
 
65
        conn.connect(host=hostname, port=port)
 
66
        if username and username != 'anonymous' and not password:
 
67
            password = bzrlib.ui.ui_factory.get_password(
 
68
                prompt='FTP %(user)s@%(host)s password',
 
69
                user=username, host=hostname)
 
70
        conn.login(user=username, passwd=password)
 
71
        conn.set_pasv(not is_active)
 
72
 
 
73
        _FTP_cache[key] = conn
 
74
 
 
75
    return _FTP_cache[key]    
 
76
 
 
77
 
62
78
class FtpStatResult(object):
63
79
    def __init__(self, f, relpath):
64
80
        try:
76
92
_number_of_retries = 2
77
93
_sleep_between_retries = 5
78
94
 
79
 
# FIXME: there are inconsistencies in the way temporary errors are
80
 
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
81
 
# be taken to analyze the implications for write operations (read operations
82
 
# are safe to retry). Overall even some read operations are never retried.
83
 
class FtpTransport(ConnectedTransport):
 
95
class FtpTransport(Transport):
84
96
    """This is the transport agent for ftp:// access."""
85
97
 
86
 
    def __init__(self, base, from_transport=None):
 
98
    def __init__(self, base, _provided_instance=None):
87
99
        """Set the base path where files will be stored."""
88
100
        assert base.startswith('ftp://') or base.startswith('aftp://')
89
 
        super(FtpTransport, self).__init__(base, from_transport)
90
 
        self._unqualified_scheme = 'ftp'
91
 
        if self._scheme == 'aftp':
92
 
            self.is_active = True
93
 
        else:
94
 
            self.is_active = False
 
101
 
 
102
        self.is_active = base.startswith('aftp://')
 
103
        if self.is_active:
 
104
            # urlparse won't handle aftp://
 
105
            base = base[1:]
 
106
        if not base.endswith('/'):
 
107
            base += '/'
 
108
        (self._proto, self._username,
 
109
            self._password, self._host,
 
110
            self._port, self._path) = split_url(base)
 
111
        base = self._unparse_url()
 
112
 
 
113
        super(FtpTransport, self).__init__(base)
 
114
        self._FTP_instance = _provided_instance
 
115
 
 
116
    def _unparse_url(self, path=None):
 
117
        if path is None:
 
118
            path = self._path
 
119
        path = urllib.quote(path)
 
120
        netloc = urllib.quote(self._host)
 
121
        if self._username is not None:
 
122
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
123
        if self._port is not None:
 
124
            netloc = '%s:%d' % (netloc, self._port)
 
125
        return urlparse.urlunparse(('ftp', netloc, path, '', '', ''))
95
126
 
96
127
    def _get_FTP(self):
97
128
        """Return the ftplib.FTP instance for this object."""
98
 
        # Ensures that a connection is established
99
 
        connection = self._get_connection()
100
 
        if connection is None:
101
 
            # First connection ever
102
 
            connection, credentials = self._create_connection()
103
 
            self._set_connection(connection, credentials)
104
 
        return connection
105
 
 
106
 
    def _create_connection(self, credentials=None):
107
 
        """Create a new connection with the provided credentials.
108
 
 
109
 
        :param credentials: The credentials needed to establish the connection.
110
 
 
111
 
        :return: The created connection and its associated credentials.
112
 
 
113
 
        The credentials are only the password as it may have been entered
114
 
        interactively by the user and may be different from the one provided
115
 
        in base url at transport creation time.
116
 
        """
117
 
        if credentials is None:
118
 
            password = self._password
119
 
        else:
120
 
            password = credentials
121
 
 
122
 
        mutter("Constructing FTP instance against %r" %
123
 
               ((self._host, self._port, self._user, '********',
124
 
                self.is_active),))
 
129
        if self._FTP_instance is not None:
 
130
            return self._FTP_instance
 
131
        
125
132
        try:
126
 
            connection = ftplib.FTP()
127
 
            connection.connect(host=self._host, port=self._port)
128
 
            if self._user and self._user != 'anonymous' and \
129
 
                    password is not None: # '' is a valid password
130
 
                get_password = bzrlib.ui.ui_factory.get_password
131
 
                password = get_password(prompt='FTP %(user)s@%(host)s password',
132
 
                                        user=self._user, host=self._host)
133
 
            connection.login(user=self._user, passwd=password)
134
 
            connection.set_pasv(not self.is_active)
 
133
            self._FTP_instance = _find_FTP(self._host, self._port,
 
134
                                           self._username, self._password,
 
135
                                           self.is_active)
 
136
            return self._FTP_instance
135
137
        except ftplib.error_perm, e:
136
 
            raise errors.TransportError(msg="Error setting up connection:"
137
 
                                        " %s" % str(e), orig_error=e)
138
 
        return connection, password
139
 
 
140
 
    def _reconnect(self):
141
 
        """Create a new connection with the previously used credentials"""
142
 
        credentials = self.get_credentials()
143
 
        connection, credentials = self._create_connection(credentials)
144
 
        self._set_connection(connection, credentials)
145
 
 
146
 
    def _translate_perm_error(self, err, path, extra=None,
147
 
                              unknown_exc=FtpPathError):
 
138
            raise errors.TransportError(msg="Error setting up connection: %s"
 
139
                                    % str(e), orig_error=e)
 
140
 
 
141
    def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
148
142
        """Try to translate an ftplib.error_perm exception.
149
143
 
150
144
        :param err: The error to translate into a bzr error
161
155
        if ('no such file' in s
162
156
            or 'could not open' in s
163
157
            or 'no such dir' in s
164
 
            or 'could not create file' in s # vsftpd
165
 
            or 'file doesn\'t exist' in s
166
158
            ):
167
159
            raise errors.NoSuchFile(path, extra=extra)
168
160
        if ('file exists' in s):
186
178
        """
187
179
        return True
188
180
 
189
 
    def _remote_path(self, relpath):
190
 
        # XXX: It seems that ftplib does not handle Unicode paths
191
 
        # at the same time, medusa won't handle utf8 paths So if
192
 
        # we .encode(utf8) here (see ConnectedTransport
193
 
        # implementation), then we get a Server failure.  while
194
 
        # if we use str(), we get a UnicodeError, and the test
195
 
        # suite just skips testing UnicodePaths.
196
 
        relative = str(urlutils.unescape(relpath))
197
 
        remote_path = self._combine_paths(self._path, relative)
198
 
        return remote_path
 
181
    def clone(self, offset=None):
 
182
        """Return a new FtpTransport with root at self.base + offset.
 
183
        """
 
184
        mutter("FTP clone")
 
185
        if offset is None:
 
186
            return FtpTransport(self.base, self._FTP_instance)
 
187
        else:
 
188
            return FtpTransport(self.abspath(offset), self._FTP_instance)
 
189
 
 
190
    def _abspath(self, relpath):
 
191
        assert isinstance(relpath, basestring)
 
192
        relpath = urllib.unquote(relpath)
 
193
        relpath_parts = relpath.split('/')
 
194
        if len(relpath_parts) > 1:
 
195
            if relpath_parts[0] == '':
 
196
                raise ValueError("path %r within branch %r seems to be absolute"
 
197
                                 % (relpath, self._path))
 
198
        basepath = self._path.split('/')
 
199
        if len(basepath) > 0 and basepath[-1] == '':
 
200
            basepath = basepath[:-1]
 
201
        for p in relpath_parts:
 
202
            if p == '..':
 
203
                if len(basepath) == 0:
 
204
                    # In most filesystems, a request for the parent
 
205
                    # of root, just returns root.
 
206
                    continue
 
207
                basepath.pop()
 
208
            elif p == '.' or p == '':
 
209
                continue # No-op
 
210
            else:
 
211
                basepath.append(p)
 
212
        # Possibly, we could use urlparse.urljoin() here, but
 
213
        # I'm concerned about when it chooses to strip the last
 
214
        # portion of the path, and when it doesn't.
 
215
        return '/'.join(basepath) or '/'
 
216
    
 
217
    def abspath(self, relpath):
 
218
        """Return the full url to the given relative path.
 
219
        This can be supplied with a string or a list
 
220
        """
 
221
        path = self._abspath(relpath)
 
222
        return self._unparse_url(path)
199
223
 
200
224
    def has(self, relpath):
201
225
        """Does the target location exist?"""
204
228
        # XXX: I assume we're never asked has(dirname) and thus I use
205
229
        # the FTP size command and assume that if it doesn't raise,
206
230
        # all is good.
207
 
        abspath = self._remote_path(relpath)
 
231
        abspath = self._abspath(relpath)
208
232
        try:
209
233
            f = self._get_FTP()
210
234
            mutter('FTP has check: %s => %s', relpath, abspath)
230
254
        """
231
255
        # TODO: decode should be deprecated
232
256
        try:
233
 
            mutter("FTP get: %s", self._remote_path(relpath))
 
257
            mutter("FTP get: %s", self._abspath(relpath))
234
258
            f = self._get_FTP()
235
259
            ret = StringIO()
236
 
            f.retrbinary('RETR '+self._remote_path(relpath), ret.write, 8192)
 
260
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
237
261
            ret.seek(0)
238
262
            return ret
239
263
        except ftplib.error_perm, e:
245
269
                                     orig_error=e)
246
270
            else:
247
271
                warning("FTP temporary error: %s. Retrying.", str(e))
248
 
                self._reconnect()
 
272
                self._FTP_instance = None
249
273
                return self.get(relpath, decode, retries+1)
250
274
        except EOFError, e:
251
275
            if retries > _number_of_retries:
255
279
            else:
256
280
                warning("FTP control connection closed. Trying to reopen.")
257
281
                time.sleep(_sleep_between_retries)
258
 
                self._reconnect()
 
282
                self._FTP_instance = None
259
283
                return self.get(relpath, decode, retries+1)
260
284
 
261
 
    def put_file(self, relpath, fp, mode=None, retries=0):
 
285
    def put(self, relpath, fp, mode=None, retries=0):
262
286
        """Copy the file-like or string object into the location.
263
287
 
264
288
        :param relpath: Location to put the contents, relative to base.
266
290
        :param retries: Number of retries after temporary failures so far
267
291
                        for this operation.
268
292
 
269
 
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
270
 
        ftplib does not
 
293
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
271
294
        """
272
 
        abspath = self._remote_path(relpath)
 
295
        abspath = self._abspath(relpath)
273
296
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
274
297
                        os.getpid(), random.randint(0,0x7FFFFFFF))
275
 
        if getattr(fp, 'read', None) is None:
 
298
        if not hasattr(fp, 'read'):
276
299
            fp = StringIO(fp)
277
300
        try:
278
301
            mutter("FTP put: %s", abspath)
279
302
            f = self._get_FTP()
280
303
            try:
281
304
                f.storbinary('STOR '+tmp_abspath, fp)
282
 
                self._rename_and_overwrite(tmp_abspath, abspath, f)
 
305
                f.rename(tmp_abspath, abspath)
283
306
            except (ftplib.error_temp,EOFError), e:
284
307
                warning("Failure during ftp PUT. Deleting temporary file.")
285
308
                try:
290
313
                    raise e
291
314
                raise
292
315
        except ftplib.error_perm, e:
293
 
            self._translate_perm_error(e, abspath, extra='could not store',
294
 
                                       unknown_exc=errors.NoSuchFile)
 
316
            self._translate_perm_error(e, abspath, extra='could not store')
295
317
        except ftplib.error_temp, e:
296
318
            if retries > _number_of_retries:
297
319
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
298
320
                                     % self.abspath(relpath), orig_error=e)
299
321
            else:
300
322
                warning("FTP temporary error: %s. Retrying.", str(e))
301
 
                self._reconnect()
302
 
                self.put_file(relpath, fp, mode, retries+1)
 
323
                self._FTP_instance = None
 
324
                self.put(relpath, fp, mode, retries+1)
303
325
        except EOFError:
304
326
            if retries > _number_of_retries:
305
327
                raise errors.TransportError("FTP control connection closed during PUT %s."
307
329
            else:
308
330
                warning("FTP control connection closed. Trying to reopen.")
309
331
                time.sleep(_sleep_between_retries)
310
 
                self._reconnect()
311
 
                self.put_file(relpath, fp, mode, retries+1)
 
332
                self._FTP_instance = None
 
333
                self.put(relpath, fp, mode, retries+1)
312
334
 
313
335
    def mkdir(self, relpath, mode=None):
314
336
        """Create a directory at the given path."""
315
 
        abspath = self._remote_path(relpath)
 
337
        abspath = self._abspath(relpath)
316
338
        try:
317
339
            mutter("FTP mkd: %s", abspath)
318
340
            f = self._get_FTP()
323
345
 
324
346
    def rmdir(self, rel_path):
325
347
        """Delete the directory at rel_path"""
326
 
        abspath = self._remote_path(rel_path)
 
348
        abspath = self._abspath(rel_path)
327
349
        try:
328
350
            mutter("FTP rmd: %s", abspath)
329
351
            f = self._get_FTP()
331
353
        except ftplib.error_perm, e:
332
354
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
333
355
 
334
 
    def append_file(self, relpath, f, mode=None):
 
356
    def append(self, relpath, f, mode=None):
335
357
        """Append the text in the file-like object into the final
336
358
        location.
337
359
        """
338
 
        abspath = self._remote_path(relpath)
 
360
        abspath = self._abspath(relpath)
339
361
        if self.has(relpath):
340
362
            ftp = self._get_FTP()
341
363
            result = ftp.size(abspath)
354
376
        number of retries is exceeded.
355
377
        """
356
378
        try:
357
 
            abspath = self._remote_path(relpath)
 
379
            abspath = self._abspath(relpath)
358
380
            mutter("FTP appe (try %d) to %s", retries, abspath)
359
381
            ftp = self._get_FTP()
360
382
            ftp.voidcmd("TYPE I")
374
396
                        "Aborting." % abspath, orig_error=e)
375
397
            else:
376
398
                warning("FTP temporary error: %s. Retrying.", str(e))
377
 
                self._reconnect()
 
399
                self._FTP_instance = None
378
400
                self._try_append(relpath, text, mode, retries+1)
379
401
 
380
402
    def _setmode(self, relpath, mode):
385
407
        """
386
408
        try:
387
409
            mutter("FTP site chmod: setting permissions to %s on %s",
388
 
                str(mode), self._remote_path(relpath))
 
410
                str(mode), self._abspath(relpath))
389
411
            ftp = self._get_FTP()
390
 
            cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
 
412
            cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
391
413
            ftp.sendcmd(cmd)
392
414
        except ftplib.error_perm, e:
393
415
            # Command probably not available on this server
394
416
            warning("FTP Could not set permissions to %s on %s. %s",
395
 
                    str(mode), self._remote_path(relpath), str(e))
 
417
                    str(mode), self._abspath(relpath), str(e))
396
418
 
397
419
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
398
420
    #       to copy something to another machine. And you may be able
399
421
    #       to give it its own address as the 'to' location.
400
422
    #       So implement a fancier 'copy()'
401
423
 
402
 
    def rename(self, rel_from, rel_to):
403
 
        abs_from = self._remote_path(rel_from)
404
 
        abs_to = self._remote_path(rel_to)
405
 
        mutter("FTP rename: %s => %s", abs_from, abs_to)
406
 
        f = self._get_FTP()
407
 
        return self._rename(abs_from, abs_to, f)
408
 
 
409
 
    def _rename(self, abs_from, abs_to, f):
410
 
        try:
411
 
            f.rename(abs_from, abs_to)
412
 
        except ftplib.error_perm, e:
413
 
            self._translate_perm_error(e, abs_from,
414
 
                ': unable to rename to %r' % (abs_to))
415
 
 
416
424
    def move(self, rel_from, rel_to):
417
425
        """Move the item at rel_from to the location at rel_to"""
418
 
        abs_from = self._remote_path(rel_from)
419
 
        abs_to = self._remote_path(rel_to)
 
426
        abs_from = self._abspath(rel_from)
 
427
        abs_to = self._abspath(rel_to)
420
428
        try:
421
429
            mutter("FTP mv: %s => %s", abs_from, abs_to)
422
430
            f = self._get_FTP()
423
 
            self._rename_and_overwrite(abs_from, abs_to, f)
 
431
            f.rename(abs_from, abs_to)
424
432
        except ftplib.error_perm, e:
425
433
            self._translate_perm_error(e, abs_from,
426
434
                extra='unable to rename to %r' % (rel_to,), 
427
435
                unknown_exc=errors.PathError)
428
436
 
429
 
    def _rename_and_overwrite(self, abs_from, abs_to, f):
430
 
        """Do a fancy rename on the remote server.
431
 
 
432
 
        Using the implementation provided by osutils.
433
 
        """
434
 
        osutils.fancy_rename(abs_from, abs_to,
435
 
            rename_func=lambda p1, p2: self._rename(p1, p2, f),
436
 
            unlink_func=lambda p: self._delete(p, f))
 
437
    rename = move
437
438
 
438
439
    def delete(self, relpath):
439
440
        """Delete the item at relpath"""
440
 
        abspath = self._remote_path(relpath)
441
 
        f = self._get_FTP()
442
 
        self._delete(abspath, f)
443
 
 
444
 
    def _delete(self, abspath, f):
 
441
        abspath = self._abspath(relpath)
445
442
        try:
446
443
            mutter("FTP rm: %s", abspath)
 
444
            f = self._get_FTP()
447
445
            f.delete(abspath)
448
446
        except ftplib.error_perm, e:
449
447
            self._translate_perm_error(e, abspath, 'error deleting',
455
453
 
456
454
    def list_dir(self, relpath):
457
455
        """See Transport.list_dir."""
458
 
        basepath = self._remote_path(relpath)
459
 
        mutter("FTP nlst: %s", basepath)
460
 
        f = self._get_FTP()
461
456
        try:
 
457
            mutter("FTP nlst: %s", self._abspath(relpath))
 
458
            f = self._get_FTP()
 
459
            basepath = self._abspath(relpath)
462
460
            paths = f.nlst(basepath)
 
461
            # If FTP.nlst returns paths prefixed by relpath, strip 'em
 
462
            if paths and paths[0].startswith(basepath):
 
463
                paths = [path[len(basepath)+1:] for path in paths]
 
464
            # Remove . and .. if present, and return
 
465
            return [path for path in paths if path not in (".", "..")]
463
466
        except ftplib.error_perm, e:
464
467
            self._translate_perm_error(e, relpath, extra='error with list_dir')
465
 
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
466
 
        if paths and paths[0].startswith(basepath):
467
 
            entries = [path[len(basepath)+1:] for path in paths]
468
 
        else:
469
 
            entries = paths
470
 
        # Remove . and .. if present
471
 
        return [urlutils.escape(entry) for entry in entries
472
 
                if entry not in ('.', '..')]
473
468
 
474
469
    def iter_files_recursive(self):
475
470
        """See Transport.iter_files_recursive.
478
473
        mutter("FTP iter_files_recursive")
479
474
        queue = list(self.list_dir("."))
480
475
        while queue:
481
 
            relpath = queue.pop(0)
 
476
            relpath = urllib.quote(queue.pop(0))
482
477
            st = self.stat(relpath)
483
478
            if stat.S_ISDIR(st.st_mode):
484
479
                for i, basename in enumerate(self.list_dir(relpath)):
488
483
 
489
484
    def stat(self, relpath):
490
485
        """Return the stat information for a file."""
491
 
        abspath = self._remote_path(relpath)
 
486
        abspath = self._abspath(relpath)
492
487
        try:
493
488
            mutter("FTP stat: %s", abspath)
494
489
            f = self._get_FTP()
519
514
 
520
515
 
521
516
class FtpServer(Server):
522
 
    """Common code for FTP server facilities."""
 
517
    """Common code for SFTP server facilities."""
523
518
 
524
519
    def __init__(self):
525
520
        self._root = None
541
536
        """This is used by medusa.ftp_server to log connections, etc."""
542
537
        self.logs.append(message)
543
538
 
544
 
    def setUp(self, vfs_server=None):
 
539
    def setUp(self):
 
540
 
545
541
        if not _have_medusa:
546
542
            raise RuntimeError('Must have medusa to run the FtpServer')
547
543
 
548
 
        assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
549
 
            "FtpServer currently assumes local transport, got %s" % vfs_server
550
 
 
551
544
        self._root = os.getcwdu()
552
545
        self._ftp_server = _ftp_server(
553
546
            authorizer=_test_authorizer(root=self._root),
558
551
            )
559
552
        self._port = self._ftp_server.getsockname()[1]
560
553
        # Don't let it loop forever, or handle an infinite number of requests.
561
 
        # In this case it will run for 1000s, or 10000 requests
562
 
        self._async_thread = threading.Thread(
563
 
                target=FtpServer._asyncore_loop_ignore_EBADF,
564
 
                kwargs={'timeout':0.1, 'count':10000})
 
554
        # In this case it will run for 100s, or 1000 requests
 
555
        self._async_thread = threading.Thread(target=asyncore.loop,
 
556
                kwargs={'timeout':0.1, 'count':1000})
565
557
        self._async_thread.setDaemon(True)
566
558
        self._async_thread.start()
567
559
 
572
564
        asyncore.close_all()
573
565
        self._async_thread.join()
574
566
 
575
 
    @staticmethod
576
 
    def _asyncore_loop_ignore_EBADF(*args, **kwargs):
577
 
        """Ignore EBADF during server shutdown.
578
 
 
579
 
        We close the socket to get the server to shutdown, but this causes
580
 
        select.select() to raise EBADF.
581
 
        """
582
 
        try:
583
 
            asyncore.loop(*args, **kwargs)
584
 
            # FIXME: If we reach that point, we should raise an exception
585
 
            # explaining that the 'count' parameter in setUp is too low or
586
 
            # testers may wonder why their test just sits there waiting for a
587
 
            # server that is already dead. Note that if the tester waits too
588
 
            # long under pdb the server will also die.
589
 
        except select.error, e:
590
 
            if e.args[0] != errno.EBADF:
591
 
                raise
592
 
 
593
567
 
594
568
_ftp_channel = None
595
569
_ftp_server = None
637
611
        def log(self, message):
638
612
            """Redirect logging requests."""
639
613
            mutter('_ftp_channel: %s', message)
640
 
 
 
614
            
641
615
        def log_info(self, message, type='info'):
642
616
            """Redirect logging requests."""
643
617
            mutter('_ftp_channel %s: %s', type, message)
644
 
 
 
618
            
645
619
        def cmd_rnfr(self, line):
646
620
            """Prepare for renaming a file."""
647
621
            self._renaming = line[1]
660
634
            pfrom = self.filesystem.translate(self._renaming)
661
635
            self._renaming = None
662
636
            pto = self.filesystem.translate(line[1])
663
 
            if os.path.exists(pto):
664
 
                self.respond('550 RNTO failed: file exists')
665
 
                return
666
637
            try:
667
638
                os.rename(pfrom, pto)
668
639
            except (IOError, OSError), e:
669
640
                # TODO: jam 20060516 return custom responses based on
670
641
                #       why the command failed
671
 
                # (bialix 20070418) str(e) on Python 2.5 @ Windows
672
 
                # sometimes don't provide expected error message;
673
 
                # so we obtain such message via os.strerror()
674
 
                self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
 
642
                self.respond('550 RNTO failed: %s' % (e,))
675
643
            except:
676
644
                self.respond('550 RNTO failed')
677
645
                # For a test server, we will go ahead and just die
709
677
                    self.filesystem.mkdir (path)
710
678
                    self.respond ('257 MKD command successful.')
711
679
                except (IOError, OSError), e:
712
 
                    # (bialix 20070418) str(e) on Python 2.5 @ Windows
713
 
                    # sometimes don't provide expected error message;
714
 
                    # so we obtain such message via os.strerror()
715
 
                    self.respond ('550 error creating directory: %s' %
716
 
                                  os.strerror(e.errno))
 
680
                    self.respond ('550 error creating directory: %s' % (e,))
717
681
                except:
718
682
                    self.respond ('550 error creating directory.')
719
683