~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

Merged bzr.dev and updated NEWS with a better description of changes

Show diffs side-by-side

added added

removed removed

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