1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
 
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.
 
 
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.
 
 
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.
 
 
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
 
 
19
cargo-culting from the sftp transport and the http transport.
 
 
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.
 
 
27
from cStringIO import StringIO
 
 
40
from warnings import warn
 
 
47
from bzrlib.trace import mutter, warning
 
 
48
from bzrlib.transport import (
 
 
53
from bzrlib.transport.local import LocalURLServer
 
 
59
class FtpPathError(errors.PathError):
 
 
60
    """FTP failed for path: %(path)s%(extra)s"""
 
 
64
def _find_FTP(hostname, port, username, password, is_active):
 
 
65
    """Find an ftplib.FTP instance attached to this triplet."""
 
 
66
    key = (hostname, port, username, password, is_active)
 
 
67
    alt_key = (hostname, port, username, '********', is_active)
 
 
68
    if key not in _FTP_cache:
 
 
69
        mutter("Constructing FTP instance against %r" % (alt_key,))
 
 
72
        conn.connect(host=hostname, port=port)
 
 
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)
 
 
77
        conn.login(user=username, passwd=password)
 
 
78
        conn.set_pasv(not is_active)
 
 
80
        _FTP_cache[key] = conn
 
 
82
    return _FTP_cache[key]    
 
 
85
class FtpStatResult(object):
 
 
86
    def __init__(self, f, relpath):
 
 
88
            self.st_size = f.size(relpath)
 
 
89
            self.st_mode = stat.S_IFREG
 
 
90
        except ftplib.error_perm:
 
 
94
                self.st_mode = stat.S_IFDIR
 
 
99
_number_of_retries = 2
 
 
100
_sleep_between_retries = 5
 
 
102
class FtpTransport(Transport):
 
 
103
    """This is the transport agent for ftp:// access."""
 
 
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://')
 
 
109
        self.is_active = base.startswith('aftp://')
 
 
111
            # urlparse won't handle aftp://
 
 
113
        if not base.endswith('/'):
 
 
115
        (self._proto, self._username,
 
 
116
            self._password, self._host,
 
 
117
            self._port, self._path) = split_url(base)
 
 
118
        base = self._unparse_url()
 
 
120
        super(FtpTransport, self).__init__(base)
 
 
121
        self._FTP_instance = _provided_instance
 
 
123
    def _unparse_url(self, path=None):
 
 
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)
 
 
135
        return urlparse.urlunparse((proto, netloc, path, '', '', ''))
 
 
138
        """Return the ftplib.FTP instance for this object."""
 
 
139
        if self._FTP_instance is not None:
 
 
140
            return self._FTP_instance
 
 
143
            self._FTP_instance = _find_FTP(self._host, self._port,
 
 
144
                                           self._username, self._password,
 
 
146
            return self._FTP_instance
 
 
147
        except ftplib.error_perm, e:
 
 
148
            raise errors.TransportError(msg="Error setting up connection: %s"
 
 
149
                                    % str(e), orig_error=e)
 
 
151
    def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
 
 
152
        """Try to translate an ftplib.error_perm exception.
 
 
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)
 
 
164
            extra += ': ' + str(err)
 
 
165
        if ('no such file' in s
 
 
166
            or 'could not open' in s
 
 
167
            or 'no such dir' in s
 
 
168
            or 'could not create file' in s # vsftpd
 
 
170
            raise errors.NoSuchFile(path, extra=extra)
 
 
171
        if ('file exists' in s):
 
 
172
            raise errors.FileExists(path, extra=extra)
 
 
173
        if ('not a directory' in s):
 
 
174
            raise errors.PathError(path, extra=extra)
 
 
176
        mutter('unable to understand error for path: %s: %s', path, err)
 
 
179
            raise unknown_exc(path, extra=extra)
 
 
180
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
 
 
181
        #       something like TransportError, but this loses the traceback
 
 
182
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
 
 
183
        #       to handle. Consider doing something like that here.
 
 
184
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
 
 
187
    def should_cache(self):
 
 
188
        """Return True if the data pulled across should be cached locally.
 
 
192
    def clone(self, offset=None):
 
 
193
        """Return a new FtpTransport with root at self.base + offset.
 
 
197
            return FtpTransport(self.base, self._FTP_instance)
 
 
199
            return FtpTransport(self.abspath(offset), self._FTP_instance)
 
 
201
    def _abspath(self, relpath):
 
 
202
        assert isinstance(relpath, basestring)
 
 
203
        relpath = urlutils.unescape(relpath)
 
 
204
        if relpath.startswith('/'):
 
 
207
            basepath = self._path.split('/')
 
 
208
        if len(basepath) > 0 and basepath[-1] == '':
 
 
209
            basepath = basepath[:-1]
 
 
210
        for p in relpath.split('/'):
 
 
212
                if len(basepath) == 0:
 
 
213
                    # In most filesystems, a request for the parent
 
 
214
                    # of root, just returns root.
 
 
217
            elif p == '.' or 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.
 
 
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 '/')
 
 
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
 
 
236
        path = self._abspath(relpath)
 
 
237
        return self._unparse_url(path)
 
 
239
    def has(self, relpath):
 
 
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,
 
 
246
        abspath = self._abspath(relpath)
 
 
249
            mutter('FTP has check: %s => %s', relpath, abspath)
 
 
251
            mutter("FTP has: %s", abspath)
 
 
253
        except ftplib.error_perm, e:
 
 
254
            if ('is a directory' in str(e).lower()):
 
 
255
                mutter("FTP has dir: %s: %s", abspath, e)
 
 
257
            mutter("FTP has not: %s: %s", abspath, e)
 
 
260
    def get(self, relpath, decode=False, retries=0):
 
 
261
        """Get the file at the given relative path.
 
 
263
        :param relpath: The relative path to the file
 
 
264
        :param retries: Number of retries after temporary failures so far
 
 
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
 
 
270
        # TODO: decode should be deprecated
 
 
272
            mutter("FTP get: %s", self._abspath(relpath))
 
 
275
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
 
 
278
        except ftplib.error_perm, e:
 
 
279
            raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
 
 
280
        except ftplib.error_temp, e:
 
 
281
            if retries > _number_of_retries:
 
 
282
                raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
 
 
283
                                     % self.abspath(relpath),
 
 
286
                warning("FTP temporary error: %s. Retrying.", str(e))
 
 
287
                self._FTP_instance = None
 
 
288
                return self.get(relpath, decode, retries+1)
 
 
290
            if retries > _number_of_retries:
 
 
291
                raise errors.TransportError("FTP control connection closed during GET %s."
 
 
292
                                     % self.abspath(relpath),
 
 
295
                warning("FTP control connection closed. Trying to reopen.")
 
 
296
                time.sleep(_sleep_between_retries)
 
 
297
                self._FTP_instance = None
 
 
298
                return self.get(relpath, decode, retries+1)
 
 
300
    def put_file(self, relpath, fp, mode=None, retries=0):
 
 
301
        """Copy the file-like or string object into the location.
 
 
303
        :param relpath: Location to put the contents, relative to base.
 
 
304
        :param fp:       File-like or string object.
 
 
305
        :param retries: Number of retries after temporary failures so far
 
 
308
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
 
 
311
        abspath = self._abspath(relpath)
 
 
312
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
 
 
313
                        os.getpid(), random.randint(0,0x7FFFFFFF))
 
 
314
        if getattr(fp, 'read', None) is None:
 
 
317
            mutter("FTP put: %s", abspath)
 
 
320
                f.storbinary('STOR '+tmp_abspath, fp)
 
 
321
                self._rename_and_overwrite(tmp_abspath, abspath, f)
 
 
322
            except (ftplib.error_temp,EOFError), e:
 
 
323
                warning("Failure during ftp PUT. Deleting temporary file.")
 
 
325
                    f.delete(tmp_abspath)
 
 
327
                    warning("Failed to delete temporary file on the"
 
 
328
                            " server.\nFile: %s", tmp_abspath)
 
 
331
        except ftplib.error_perm, e:
 
 
332
            self._translate_perm_error(e, abspath, extra='could not store')
 
 
333
        except ftplib.error_temp, e:
 
 
334
            if retries > _number_of_retries:
 
 
335
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
 
 
336
                                     % self.abspath(relpath), orig_error=e)
 
 
338
                warning("FTP temporary error: %s. Retrying.", str(e))
 
 
339
                self._FTP_instance = None
 
 
340
                self.put_file(relpath, fp, mode, retries+1)
 
 
342
            if retries > _number_of_retries:
 
 
343
                raise errors.TransportError("FTP control connection closed during PUT %s."
 
 
344
                                     % self.abspath(relpath), orig_error=e)
 
 
346
                warning("FTP control connection closed. Trying to reopen.")
 
 
347
                time.sleep(_sleep_between_retries)
 
 
348
                self._FTP_instance = None
 
 
349
                self.put_file(relpath, fp, mode, retries+1)
 
 
351
    def mkdir(self, relpath, mode=None):
 
 
352
        """Create a directory at the given path."""
 
 
353
        abspath = self._abspath(relpath)
 
 
355
            mutter("FTP mkd: %s", abspath)
 
 
358
        except ftplib.error_perm, e:
 
 
359
            self._translate_perm_error(e, abspath,
 
 
360
                unknown_exc=errors.FileExists)
 
 
362
    def rmdir(self, rel_path):
 
 
363
        """Delete the directory at rel_path"""
 
 
364
        abspath = self._abspath(rel_path)
 
 
366
            mutter("FTP rmd: %s", abspath)
 
 
369
        except ftplib.error_perm, e:
 
 
370
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
 
 
372
    def append_file(self, relpath, f, mode=None):
 
 
373
        """Append the text in the file-like object into the final
 
 
376
        abspath = self._abspath(relpath)
 
 
377
        if self.has(relpath):
 
 
378
            ftp = self._get_FTP()
 
 
379
            result = ftp.size(abspath)
 
 
383
        mutter("FTP appe to %s", abspath)
 
 
384
        self._try_append(relpath, f.read(), mode)
 
 
388
    def _try_append(self, relpath, text, mode=None, retries=0):
 
 
389
        """Try repeatedly to append the given text to the file at relpath.
 
 
391
        This is a recursive function. On errors, it will be called until the
 
 
392
        number of retries is exceeded.
 
 
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)
 
 
404
                self._setmode(relpath, mode)
 
 
406
        except ftplib.error_perm, e:
 
 
407
            self._translate_perm_error(e, abspath, extra='error appending',
 
 
408
                unknown_exc=errors.NoSuchFile)
 
 
409
        except ftplib.error_temp, e:
 
 
410
            if retries > _number_of_retries:
 
 
411
                raise errors.TransportError("FTP temporary error during APPEND %s." \
 
 
412
                        "Aborting." % abspath, orig_error=e)
 
 
414
                warning("FTP temporary error: %s. Retrying.", str(e))
 
 
415
                self._FTP_instance = None
 
 
416
                self._try_append(relpath, text, mode, retries+1)
 
 
418
    def _setmode(self, relpath, mode):
 
 
419
        """Set permissions on a path.
 
 
421
        Only set permissions if the FTP server supports the 'SITE CHMOD'
 
 
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))
 
 
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))
 
 
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()'
 
 
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)
 
 
445
        return self._rename(abs_from, abs_to, f)
 
 
447
    def _rename(self, abs_from, abs_to, f):
 
 
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))
 
 
454
    def move(self, rel_from, rel_to):
 
 
455
        """Move the item at rel_from to the location at rel_to"""
 
 
456
        abs_from = self._abspath(rel_from)
 
 
457
        abs_to = self._abspath(rel_to)
 
 
459
            mutter("FTP mv: %s => %s", abs_from, abs_to)
 
 
461
            self._rename_and_overwrite(abs_from, abs_to, f)
 
 
462
        except ftplib.error_perm, e:
 
 
463
            self._translate_perm_error(e, abs_from,
 
 
464
                extra='unable to rename to %r' % (rel_to,), 
 
 
465
                unknown_exc=errors.PathError)
 
 
467
    def _rename_and_overwrite(self, abs_from, abs_to, f):
 
 
468
        """Do a fancy rename on the remote server.
 
 
470
        Using the implementation provided by osutils.
 
 
472
        osutils.fancy_rename(abs_from, abs_to,
 
 
473
            rename_func=lambda p1, p2: self._rename(p1, p2, f),
 
 
474
            unlink_func=lambda p: self._delete(p, f))
 
 
476
    def delete(self, relpath):
 
 
477
        """Delete the item at relpath"""
 
 
478
        abspath = self._abspath(relpath)
 
 
480
        self._delete(abspath, f)
 
 
482
    def _delete(self, abspath, f):
 
 
484
            mutter("FTP rm: %s", abspath)
 
 
486
        except ftplib.error_perm, e:
 
 
487
            self._translate_perm_error(e, abspath, 'error deleting',
 
 
488
                unknown_exc=errors.NoSuchFile)
 
 
491
        """See Transport.listable."""
 
 
494
    def list_dir(self, relpath):
 
 
495
        """See Transport.list_dir."""
 
 
496
        basepath = self._abspath(relpath)
 
 
497
        mutter("FTP nlst: %s", basepath)
 
 
500
            paths = f.nlst(basepath)
 
 
501
        except ftplib.error_perm, e:
 
 
502
            self._translate_perm_error(e, relpath, extra='error with list_dir')
 
 
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]
 
 
508
        # Remove . and .. if present
 
 
509
        return [urlutils.escape(entry) for entry in entries
 
 
510
                if entry not in ('.', '..')]
 
 
512
    def iter_files_recursive(self):
 
 
513
        """See Transport.iter_files_recursive.
 
 
515
        This is cargo-culted from the SFTP transport"""
 
 
516
        mutter("FTP iter_files_recursive")
 
 
517
        queue = list(self.list_dir("."))
 
 
519
            relpath = queue.pop(0)
 
 
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)
 
 
527
    def stat(self, relpath):
 
 
528
        """Return the stat information for a file."""
 
 
529
        abspath = self._abspath(relpath)
 
 
531
            mutter("FTP stat: %s", abspath)
 
 
533
            return FtpStatResult(f, abspath)
 
 
534
        except ftplib.error_perm, e:
 
 
535
            self._translate_perm_error(e, abspath, extra='error w/ stat')
 
 
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()
 
 
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):
 
 
548
        return BogusLock(relpath)
 
 
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
 
 
554
        :return: A lock object, which should be passed to Transport.unlock()
 
 
556
        return self.lock_read(relpath)
 
 
559
class FtpServer(Server):
 
 
560
    """Common code for SFTP server facilities."""
 
 
564
        self._ftp_server = None
 
 
566
        self._async_thread = None
 
 
571
        """Calculate an ftp url to this server."""
 
 
572
        return 'ftp://foo:bar@localhost:%d/' % (self._port)
 
 
574
#    def get_bogus_url(self):
 
 
575
#        """Return a URL which cannot be connected to."""
 
 
576
#        return 'ftp://127.0.0.1:1'
 
 
578
    def log(self, message):
 
 
579
        """This is used by medusa.ftp_server to log connections, etc."""
 
 
580
        self.logs.append(message)
 
 
582
    def setUp(self, vfs_server=None):
 
 
584
            raise RuntimeError('Must have medusa to run the FtpServer')
 
 
586
        assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
 
 
587
            "FtpServer currently assumes local transport, got %s" % vfs_server
 
 
589
        self._root = os.getcwdu()
 
 
590
        self._ftp_server = _ftp_server(
 
 
591
            authorizer=_test_authorizer(root=self._root),
 
 
593
            port=0, # bind to a random port
 
 
595
            logger_object=self # Use FtpServer.log() for messages
 
 
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
 
 
600
        self._async_thread = threading.Thread(
 
 
601
                target=FtpServer._asyncore_loop_ignore_EBADF,
 
 
602
                kwargs={'timeout':0.1, 'count':1000})
 
 
603
        self._async_thread.setDaemon(True)
 
 
604
        self._async_thread.start()
 
 
607
        """See bzrlib.transport.Server.tearDown."""
 
 
608
        # have asyncore release the channel
 
 
609
        self._ftp_server.del_channel()
 
 
611
        self._async_thread.join()
 
 
614
    def _asyncore_loop_ignore_EBADF(*args, **kwargs):
 
 
615
        """Ignore EBADF during server shutdown.
 
 
617
        We close the socket to get the server to shutdown, but this causes
 
 
618
        select.select() to raise EBADF.
 
 
621
            asyncore.loop(*args, **kwargs)
 
 
622
        except select.error, e:
 
 
623
            if e.args[0] != errno.EBADF:
 
 
629
_test_authorizer = None
 
 
633
    global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
 
 
636
        import medusa.filesys
 
 
637
        import medusa.ftp_server
 
 
643
    class test_authorizer(object):
 
 
644
        """A custom Authorizer object for running the test suite.
 
 
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.
 
 
650
        def __init__(self, root):
 
 
653
        def authorize(self, channel, username, password):
 
 
654
            """Return (success, reply_string, filesystem)"""
 
 
656
                return 0, 'No Medusa.', None
 
 
658
            channel.persona = -1, -1
 
 
659
            if username == 'anonymous':
 
 
660
                channel.read_only = 1
 
 
662
                channel.read_only = 0
 
 
664
            return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
 
 
667
    class ftp_channel(medusa.ftp_server.ftp_channel):
 
 
668
        """Customized ftp channel"""
 
 
670
        def log(self, message):
 
 
671
            """Redirect logging requests."""
 
 
672
            mutter('_ftp_channel: %s', message)
 
 
674
        def log_info(self, message, type='info'):
 
 
675
            """Redirect logging requests."""
 
 
676
            mutter('_ftp_channel %s: %s', type, message)
 
 
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
 
 
686
        def cmd_rnto(self, line):
 
 
687
            """Rename a file based on the target given.
 
 
689
            rnto must be called after calling rnfr.
 
 
691
            if not self._renaming:
 
 
692
                self.respond('503 RNFR required first.')
 
 
693
            pfrom = self.filesystem.translate(self._renaming)
 
 
694
            self._renaming = None
 
 
695
            pto = self.filesystem.translate(line[1])
 
 
696
            if os.path.exists(pto):
 
 
697
                self.respond('550 RNTO failed: file exists')
 
 
700
                os.rename(pfrom, pto)
 
 
701
            except (IOError, OSError), e:
 
 
702
                # TODO: jam 20060516 return custom responses based on
 
 
703
                #       why the command failed
 
 
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))
 
 
709
                self.respond('550 RNTO failed')
 
 
710
                # For a test server, we will go ahead and just die
 
 
713
                self.respond('250 Rename successful.')
 
 
715
        def cmd_size(self, line):
 
 
716
            """Return the size of a file
 
 
718
            This is overloaded to help the test suite determine if the 
 
 
719
            target is a directory.
 
 
722
            if not self.filesystem.isfile(filename):
 
 
723
                if self.filesystem.isdir(filename):
 
 
724
                    self.respond('550 "%s" is a directory' % (filename,))
 
 
726
                    self.respond('550 "%s" is not a file' % (filename,))
 
 
728
                self.respond('213 %d' 
 
 
729
                    % (self.filesystem.stat(filename)[stat.ST_SIZE]),)
 
 
731
        def cmd_mkd(self, line):
 
 
732
            """Create a directory.
 
 
734
            Overloaded because default implementation does not distinguish
 
 
735
            *why* it cannot make a directory.
 
 
738
                self.command_not_understood(''.join(line))
 
 
742
                    self.filesystem.mkdir (path)
 
 
743
                    self.respond ('257 MKD command successful.')
 
 
744
                except (IOError, OSError), e:
 
 
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))
 
 
751
                    self.respond ('550 error creating directory.')
 
 
754
    class ftp_server(medusa.ftp_server.ftp_server):
 
 
755
        """Customize the behavior of the Medusa ftp_server.
 
 
757
        There are a few warts on the ftp_server, based on how it expects
 
 
761
        ftp_channel_class = ftp_channel
 
 
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)
 
 
767
        def log(self, message):
 
 
768
            """Redirect logging requests."""
 
 
769
            mutter('_ftp_server: %s', message)
 
 
771
        def log_info(self, message, type='info'):
 
 
772
            """Override the asyncore.log_info so we don't stipple the screen."""
 
 
773
            mutter('_ftp_server %s: %s', type, message)
 
 
775
    _test_authorizer = test_authorizer
 
 
776
    _ftp_channel = ftp_channel
 
 
777
    _ftp_server = ftp_server
 
 
782
def get_test_permutations():
 
 
783
    """Return the permutations to be used in testing."""
 
 
784
    if not _setup_medusa():
 
 
785
        warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
 
 
788
        return [(FtpTransport, FtpServer)]