~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Alexander Belchenko
  • Date: 2007-01-04 23:36:44 UTC
  • mfrom: (2224 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2225.
  • Revision ID: bialix@ukr.net-20070104233644-7znkxoj9b0y7ev28
merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005 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
33
32
import urllib
34
33
import urlparse
35
 
import select
36
34
import stat
37
35
import threading
38
36
import time
41
39
 
42
40
from bzrlib import (
43
41
    errors,
44
 
    osutils,
45
42
    urlutils,
46
43
    )
47
44
from bzrlib.trace import mutter, warning
48
45
from bzrlib.transport import (
49
46
    Server,
50
 
    ConnectedTransport,
 
47
    split_url,
 
48
    Transport,
51
49
    )
52
 
from bzrlib.transport.local import LocalURLServer
53
50
import bzrlib.ui
54
51
 
55
52
_have_medusa = False
59
56
    """FTP failed for path: %(path)s%(extra)s"""
60
57
 
61
58
 
 
59
_FTP_cache = {}
 
60
def _find_FTP(hostname, port, username, password, is_active):
 
61
    """Find an ftplib.FTP instance attached to this triplet."""
 
62
    key = (hostname, port, username, password, is_active)
 
63
    alt_key = (hostname, port, username, '********', is_active)
 
64
    if key not in _FTP_cache:
 
65
        mutter("Constructing FTP instance against %r" % (alt_key,))
 
66
        conn = ftplib.FTP()
 
67
 
 
68
        conn.connect(host=hostname, port=port)
 
69
        if username and username != 'anonymous' and not password:
 
70
            password = bzrlib.ui.ui_factory.get_password(
 
71
                prompt='FTP %(user)s@%(host)s password',
 
72
                user=username, host=hostname)
 
73
        conn.login(user=username, passwd=password)
 
74
        conn.set_pasv(not is_active)
 
75
 
 
76
        _FTP_cache[key] = conn
 
77
 
 
78
    return _FTP_cache[key]    
 
79
 
 
80
 
62
81
class FtpStatResult(object):
63
82
    def __init__(self, f, relpath):
64
83
        try:
76
95
_number_of_retries = 2
77
96
_sleep_between_retries = 5
78
97
 
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):
 
98
class FtpTransport(Transport):
85
99
    """This is the transport agent for ftp:// access."""
86
100
 
87
 
    def __init__(self, base, _from_transport=None):
 
101
    def __init__(self, base, _provided_instance=None):
88
102
        """Set the base path where files will be stored."""
89
103
        assert base.startswith('ftp://') or base.startswith('aftp://')
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
 
104
 
 
105
        self.is_active = base.startswith('aftp://')
 
106
        if self.is_active:
 
107
            # urlparse won't handle aftp://
 
108
            base = base[1:]
 
109
        if not base.endswith('/'):
 
110
            base += '/'
 
111
        (self._proto, self._username,
 
112
            self._password, self._host,
 
113
            self._port, self._path) = split_url(base)
 
114
        base = self._unparse_url()
 
115
 
 
116
        super(FtpTransport, self).__init__(base)
 
117
        self._FTP_instance = _provided_instance
 
118
 
 
119
    def _unparse_url(self, path=None):
 
120
        if path is None:
 
121
            path = self._path
 
122
        path = urllib.quote(path)
 
123
        netloc = urllib.quote(self._host)
 
124
        if self._username is not None:
 
125
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
126
        if self._port is not None:
 
127
            netloc = '%s:%d' % (netloc, self._port)
 
128
        proto = 'ftp'
 
129
        if self.is_active:
 
130
            proto = 'aftp'
 
131
        return urlparse.urlunparse((proto, netloc, path, '', '', ''))
97
132
 
98
133
    def _get_FTP(self):
99
134
        """Return the ftplib.FTP instance for this object."""
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),))
 
135
        if self._FTP_instance is not None:
 
136
            return self._FTP_instance
 
137
        
127
138
        try:
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)
 
139
            self._FTP_instance = _find_FTP(self._host, self._port,
 
140
                                           self._username, self._password,
 
141
                                           self.is_active)
 
142
            return self._FTP_instance
137
143
        except ftplib.error_perm, e:
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):
 
144
            raise errors.TransportError(msg="Error setting up connection: %s"
 
145
                                    % str(e), orig_error=e)
 
146
 
 
147
    def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
150
148
        """Try to translate an ftplib.error_perm exception.
151
149
 
152
150
        :param err: The error to translate into a bzr error
163
161
        if ('no such file' in s
164
162
            or 'could not open' in s
165
163
            or 'no such dir' in s
166
 
            or 'could not create file' in s # vsftpd
167
 
            or 'file doesn\'t exist' in s
168
164
            ):
169
165
            raise errors.NoSuchFile(path, extra=extra)
170
166
        if ('file exists' in s):
188
184
        """
189
185
        return True
190
186
 
191
 
    def _remote_path(self, relpath):
 
187
    def clone(self, offset=None):
 
188
        """Return a new FtpTransport with root at self.base + offset.
 
189
        """
 
190
        mutter("FTP clone")
 
191
        if offset is None:
 
192
            return FtpTransport(self.base, self._FTP_instance)
 
193
        else:
 
194
            return FtpTransport(self.abspath(offset), self._FTP_instance)
 
195
 
 
196
    def _abspath(self, relpath):
 
197
        assert isinstance(relpath, basestring)
 
198
        relpath = urlutils.unescape(relpath)
 
199
        if relpath.startswith('/'):
 
200
            basepath = []
 
201
        else:
 
202
            basepath = self._path.split('/')
 
203
        if len(basepath) > 0 and basepath[-1] == '':
 
204
            basepath = basepath[:-1]
 
205
        for p in relpath.split('/'):
 
206
            if p == '..':
 
207
                if len(basepath) == 0:
 
208
                    # In most filesystems, a request for the parent
 
209
                    # of root, just returns root.
 
210
                    continue
 
211
                basepath.pop()
 
212
            elif p == '.' or p == '':
 
213
                continue # No-op
 
214
            else:
 
215
                basepath.append(p)
 
216
        # Possibly, we could use urlparse.urljoin() here, but
 
217
        # I'm concerned about when it chooses to strip the last
 
218
        # portion of the path, and when it doesn't.
 
219
 
192
220
        # 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
 
221
        # at the same time, medusa won't handle utf8 paths
 
222
        # So if we .encode(utf8) here, then we get a Server failure.
 
223
        # while if we use str(), we get a UnicodeError, and the test suite
 
224
        # just skips testing UnicodePaths.
 
225
        return str('/'.join(basepath) or '/')
 
226
    
 
227
    def abspath(self, relpath):
 
228
        """Return the full url to the given relative path.
 
229
        This can be supplied with a string or a list
 
230
        """
 
231
        path = self._abspath(relpath)
 
232
        return self._unparse_url(path)
201
233
 
202
234
    def has(self, relpath):
203
235
        """Does the target location exist?"""
206
238
        # XXX: I assume we're never asked has(dirname) and thus I use
207
239
        # the FTP size command and assume that if it doesn't raise,
208
240
        # all is good.
209
 
        abspath = self._remote_path(relpath)
 
241
        abspath = self._abspath(relpath)
210
242
        try:
211
243
            f = self._get_FTP()
212
244
            mutter('FTP has check: %s => %s', relpath, abspath)
232
264
        """
233
265
        # TODO: decode should be deprecated
234
266
        try:
235
 
            mutter("FTP get: %s", self._remote_path(relpath))
 
267
            mutter("FTP get: %s", self._abspath(relpath))
236
268
            f = self._get_FTP()
237
269
            ret = StringIO()
238
 
            f.retrbinary('RETR '+self._remote_path(relpath), ret.write, 8192)
 
270
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
239
271
            ret.seek(0)
240
272
            return ret
241
273
        except ftplib.error_perm, e:
247
279
                                     orig_error=e)
248
280
            else:
249
281
                warning("FTP temporary error: %s. Retrying.", str(e))
250
 
                self._reconnect()
 
282
                self._FTP_instance = None
251
283
                return self.get(relpath, decode, retries+1)
252
284
        except EOFError, e:
253
285
            if retries > _number_of_retries:
257
289
            else:
258
290
                warning("FTP control connection closed. Trying to reopen.")
259
291
                time.sleep(_sleep_between_retries)
260
 
                self._reconnect()
 
292
                self._FTP_instance = None
261
293
                return self.get(relpath, decode, retries+1)
262
294
 
263
295
    def put_file(self, relpath, fp, mode=None, retries=0):
268
300
        :param retries: Number of retries after temporary failures so far
269
301
                        for this operation.
270
302
 
271
 
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
272
 
        ftplib does not
 
303
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
273
304
        """
274
 
        abspath = self._remote_path(relpath)
 
305
        abspath = self._abspath(relpath)
275
306
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
276
307
                        os.getpid(), random.randint(0,0x7FFFFFFF))
277
308
        if getattr(fp, 'read', None) is None:
281
312
            f = self._get_FTP()
282
313
            try:
283
314
                f.storbinary('STOR '+tmp_abspath, fp)
284
 
                self._rename_and_overwrite(tmp_abspath, abspath, f)
 
315
                f.rename(tmp_abspath, abspath)
285
316
            except (ftplib.error_temp,EOFError), e:
286
317
                warning("Failure during ftp PUT. Deleting temporary file.")
287
318
                try:
292
323
                    raise e
293
324
                raise
294
325
        except ftplib.error_perm, e:
295
 
            self._translate_perm_error(e, abspath, extra='could not store',
296
 
                                       unknown_exc=errors.NoSuchFile)
 
326
            self._translate_perm_error(e, abspath, extra='could not store')
297
327
        except ftplib.error_temp, e:
298
328
            if retries > _number_of_retries:
299
329
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
300
330
                                     % self.abspath(relpath), orig_error=e)
301
331
            else:
302
332
                warning("FTP temporary error: %s. Retrying.", str(e))
303
 
                self._reconnect()
 
333
                self._FTP_instance = None
304
334
                self.put_file(relpath, fp, mode, retries+1)
305
335
        except EOFError:
306
336
            if retries > _number_of_retries:
309
339
            else:
310
340
                warning("FTP control connection closed. Trying to reopen.")
311
341
                time.sleep(_sleep_between_retries)
312
 
                self._reconnect()
 
342
                self._FTP_instance = None
313
343
                self.put_file(relpath, fp, mode, retries+1)
314
344
 
315
345
    def mkdir(self, relpath, mode=None):
316
346
        """Create a directory at the given path."""
317
 
        abspath = self._remote_path(relpath)
 
347
        abspath = self._abspath(relpath)
318
348
        try:
319
349
            mutter("FTP mkd: %s", abspath)
320
350
            f = self._get_FTP()
325
355
 
326
356
    def rmdir(self, rel_path):
327
357
        """Delete the directory at rel_path"""
328
 
        abspath = self._remote_path(rel_path)
 
358
        abspath = self._abspath(rel_path)
329
359
        try:
330
360
            mutter("FTP rmd: %s", abspath)
331
361
            f = self._get_FTP()
337
367
        """Append the text in the file-like object into the final
338
368
        location.
339
369
        """
340
 
        abspath = self._remote_path(relpath)
 
370
        abspath = self._abspath(relpath)
341
371
        if self.has(relpath):
342
372
            ftp = self._get_FTP()
343
373
            result = ftp.size(abspath)
356
386
        number of retries is exceeded.
357
387
        """
358
388
        try:
359
 
            abspath = self._remote_path(relpath)
 
389
            abspath = self._abspath(relpath)
360
390
            mutter("FTP appe (try %d) to %s", retries, abspath)
361
391
            ftp = self._get_FTP()
362
392
            ftp.voidcmd("TYPE I")
376
406
                        "Aborting." % abspath, orig_error=e)
377
407
            else:
378
408
                warning("FTP temporary error: %s. Retrying.", str(e))
379
 
                self._reconnect()
 
409
                self._FTP_instance = None
380
410
                self._try_append(relpath, text, mode, retries+1)
381
411
 
382
412
    def _setmode(self, relpath, mode):
387
417
        """
388
418
        try:
389
419
            mutter("FTP site chmod: setting permissions to %s on %s",
390
 
                str(mode), self._remote_path(relpath))
 
420
                str(mode), self._abspath(relpath))
391
421
            ftp = self._get_FTP()
392
 
            cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
 
422
            cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
393
423
            ftp.sendcmd(cmd)
394
424
        except ftplib.error_perm, e:
395
425
            # Command probably not available on this server
396
426
            warning("FTP Could not set permissions to %s on %s. %s",
397
 
                    str(mode), self._remote_path(relpath), str(e))
 
427
                    str(mode), self._abspath(relpath), str(e))
398
428
 
399
429
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
400
430
    #       to copy something to another machine. And you may be able
401
431
    #       to give it its own address as the 'to' location.
402
432
    #       So implement a fancier 'copy()'
403
433
 
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
 
 
418
434
    def move(self, rel_from, rel_to):
419
435
        """Move the item at rel_from to the location at rel_to"""
420
 
        abs_from = self._remote_path(rel_from)
421
 
        abs_to = self._remote_path(rel_to)
 
436
        abs_from = self._abspath(rel_from)
 
437
        abs_to = self._abspath(rel_to)
422
438
        try:
423
439
            mutter("FTP mv: %s => %s", abs_from, abs_to)
424
440
            f = self._get_FTP()
425
 
            self._rename_and_overwrite(abs_from, abs_to, f)
 
441
            f.rename(abs_from, abs_to)
426
442
        except ftplib.error_perm, e:
427
443
            self._translate_perm_error(e, abs_from,
428
444
                extra='unable to rename to %r' % (rel_to,), 
429
445
                unknown_exc=errors.PathError)
430
446
 
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))
 
447
    rename = move
439
448
 
440
449
    def delete(self, relpath):
441
450
        """Delete the item at relpath"""
442
 
        abspath = self._remote_path(relpath)
443
 
        f = self._get_FTP()
444
 
        self._delete(abspath, f)
445
 
 
446
 
    def _delete(self, abspath, f):
 
451
        abspath = self._abspath(relpath)
447
452
        try:
448
453
            mutter("FTP rm: %s", abspath)
 
454
            f = self._get_FTP()
449
455
            f.delete(abspath)
450
456
        except ftplib.error_perm, e:
451
457
            self._translate_perm_error(e, abspath, 'error deleting',
452
458
                unknown_exc=errors.NoSuchFile)
453
459
 
454
 
    def external_url(self):
455
 
        """See bzrlib.transport.Transport.external_url."""
456
 
        # FTP URL's are externally usable.
457
 
        return self.base
458
 
 
459
460
    def listable(self):
460
461
        """See Transport.listable."""
461
462
        return True
462
463
 
463
464
    def list_dir(self, relpath):
464
465
        """See Transport.list_dir."""
465
 
        basepath = self._remote_path(relpath)
 
466
        basepath = self._abspath(relpath)
466
467
        mutter("FTP nlst: %s", basepath)
467
468
        f = self._get_FTP()
468
469
        try:
495
496
 
496
497
    def stat(self, relpath):
497
498
        """Return the stat information for a file."""
498
 
        abspath = self._remote_path(relpath)
 
499
        abspath = self._abspath(relpath)
499
500
        try:
500
501
            mutter("FTP stat: %s", abspath)
501
502
            f = self._get_FTP()
526
527
 
527
528
 
528
529
class FtpServer(Server):
529
 
    """Common code for FTP server facilities."""
 
530
    """Common code for SFTP server facilities."""
530
531
 
531
532
    def __init__(self):
532
533
        self._root = None
548
549
        """This is used by medusa.ftp_server to log connections, etc."""
549
550
        self.logs.append(message)
550
551
 
551
 
    def setUp(self, vfs_server=None):
 
552
    def setUp(self):
 
553
 
552
554
        if not _have_medusa:
553
555
            raise RuntimeError('Must have medusa to run the FtpServer')
554
556
 
555
 
        assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
556
 
            "FtpServer currently assumes local transport, got %s" % vfs_server
557
 
 
558
557
        self._root = os.getcwdu()
559
558
        self._ftp_server = _ftp_server(
560
559
            authorizer=_test_authorizer(root=self._root),
565
564
            )
566
565
        self._port = self._ftp_server.getsockname()[1]
567
566
        # Don't let it loop forever, or handle an infinite number of requests.
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})
 
567
        # In this case it will run for 100s, or 1000 requests
 
568
        self._async_thread = threading.Thread(target=asyncore.loop,
 
569
                kwargs={'timeout':0.1, 'count':1000})
572
570
        self._async_thread.setDaemon(True)
573
571
        self._async_thread.start()
574
572
 
579
577
        asyncore.close_all()
580
578
        self._async_thread.join()
581
579
 
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
 
 
600
580
 
601
581
_ftp_channel = None
602
582
_ftp_server = None
644
624
        def log(self, message):
645
625
            """Redirect logging requests."""
646
626
            mutter('_ftp_channel: %s', message)
647
 
 
 
627
            
648
628
        def log_info(self, message, type='info'):
649
629
            """Redirect logging requests."""
650
630
            mutter('_ftp_channel %s: %s', type, message)
651
 
 
 
631
            
652
632
        def cmd_rnfr(self, line):
653
633
            """Prepare for renaming a file."""
654
634
            self._renaming = line[1]
667
647
            pfrom = self.filesystem.translate(self._renaming)
668
648
            self._renaming = None
669
649
            pto = self.filesystem.translate(line[1])
670
 
            if os.path.exists(pto):
671
 
                self.respond('550 RNTO failed: file exists')
672
 
                return
673
650
            try:
674
651
                os.rename(pfrom, pto)
675
652
            except (IOError, OSError), e:
676
653
                # TODO: jam 20060516 return custom responses based on
677
654
                #       why the command failed
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))
 
655
                self.respond('550 RNTO failed: %s' % (e,))
682
656
            except:
683
657
                self.respond('550 RNTO failed')
684
658
                # For a test server, we will go ahead and just die
716
690
                    self.filesystem.mkdir (path)
717
691
                    self.respond ('257 MKD command successful.')
718
692
                except (IOError, OSError), 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))
 
693
                    self.respond ('550 error creating directory: %s' % (e,))
724
694
                except:
725
695
                    self.respond ('550 error creating directory.')
726
696