~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Vincent Ladeuil
  • Date: 2007-10-19 13:19:48 UTC
  • mto: (2949.3.1 157752)
  • mto: This revision was merged to the branch mainline in revision 2953.
  • Revision ID: v.ladeuil+lp@free.fr-20071019131948-g5793d6onl5ik5sm
Separate transport from test server.

* bzrlib/transport/ftp.py:
Move ftp test server to its own file.
(get_test_permutations): Reworked around FTPServerFeature.

* bzrlib/tests/test_ftp_transport.py:
(TestCaseWithFTPServer.setUp): Use FTPServerFeature instead of
MedusaFeature.

* bzrlib/tests/__init__.py:
(_FTPServerFeature): New feature allowing a cleaner separation
between ftp.py and FTPServer.py.

* bzrlib/tests/FTPServer.py: 
New file. Extracted from bzrlib/transport/ftp.py (use case for
tracking moving lines).

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
"""
26
26
 
27
27
from cStringIO import StringIO
28
 
import asyncore
29
28
import errno
30
29
import ftplib
31
30
import os
32
31
import os.path
33
 
import urllib
34
32
import urlparse
35
 
import select
36
33
import stat
37
 
import threading
38
34
import time
39
35
import random
40
36
from warnings import warn
557
553
        return self.lock_read(relpath)
558
554
 
559
555
 
560
 
class FtpServer(Server):
561
 
    """Common code for FTP server facilities."""
562
 
 
563
 
    def __init__(self):
564
 
        self._root = None
565
 
        self._ftp_server = None
566
 
        self._port = None
567
 
        self._async_thread = None
568
 
        # ftp server logs
569
 
        self.logs = []
570
 
 
571
 
    def get_url(self):
572
 
        """Calculate an ftp url to this server."""
573
 
        return 'ftp://foo:bar@localhost:%d/' % (self._port)
574
 
 
575
 
#    def get_bogus_url(self):
576
 
#        """Return a URL which cannot be connected to."""
577
 
#        return 'ftp://127.0.0.1:1'
578
 
 
579
 
    def log(self, message):
580
 
        """This is used by medusa.ftp_server to log connections, etc."""
581
 
        self.logs.append(message)
582
 
 
583
 
    def setUp(self, vfs_server=None):
584
 
        if not _have_medusa:
585
 
            raise RuntimeError('Must have medusa to run the FtpServer')
586
 
 
587
 
        assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
588
 
            "FtpServer currently assumes local transport, got %s" % vfs_server
589
 
 
590
 
        self._root = os.getcwdu()
591
 
        self._ftp_server = _ftp_server(
592
 
            authorizer=_test_authorizer(root=self._root),
593
 
            ip='localhost',
594
 
            port=0, # bind to a random port
595
 
            resolver=None,
596
 
            logger_object=self # Use FtpServer.log() for messages
597
 
            )
598
 
        self._port = self._ftp_server.getsockname()[1]
599
 
        # Don't let it loop forever, or handle an infinite number of requests.
600
 
        # In this case it will run for 1000s, or 10000 requests
601
 
        self._async_thread = threading.Thread(
602
 
                target=FtpServer._asyncore_loop_ignore_EBADF,
603
 
                kwargs={'timeout':0.1, 'count':10000})
604
 
        self._async_thread.setDaemon(True)
605
 
        self._async_thread.start()
606
 
 
607
 
    def tearDown(self):
608
 
        """See bzrlib.transport.Server.tearDown."""
609
 
        self._ftp_server.close()
610
 
        asyncore.close_all()
611
 
        self._async_thread.join()
612
 
 
613
 
    @staticmethod
614
 
    def _asyncore_loop_ignore_EBADF(*args, **kwargs):
615
 
        """Ignore EBADF during server shutdown.
616
 
 
617
 
        We close the socket to get the server to shutdown, but this causes
618
 
        select.select() to raise EBADF.
619
 
        """
620
 
        try:
621
 
            asyncore.loop(*args, **kwargs)
622
 
            # FIXME: If we reach that point, we should raise an exception
623
 
            # explaining that the 'count' parameter in setUp is too low or
624
 
            # testers may wonder why their test just sits there waiting for a
625
 
            # server that is already dead. Note that if the tester waits too
626
 
            # long under pdb the server will also die.
627
 
        except select.error, e:
628
 
            if e.args[0] != errno.EBADF:
629
 
                raise
630
 
 
631
 
 
632
 
_ftp_channel = None
633
 
_ftp_server = None
634
 
_test_authorizer = None
635
 
 
636
 
 
637
 
def _setup_medusa():
638
 
    global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
639
 
    try:
640
 
        import medusa
641
 
        import medusa.filesys
642
 
        import medusa.ftp_server
643
 
    except ImportError:
644
 
        return False
645
 
 
646
 
    _have_medusa = True
647
 
 
648
 
    class test_authorizer(object):
649
 
        """A custom Authorizer object for running the test suite.
650
 
 
651
 
        The reason we cannot use dummy_authorizer, is because it sets the
652
 
        channel to readonly, which we don't always want to do.
653
 
        """
654
 
 
655
 
        def __init__(self, root):
656
 
            self.root = root
657
 
            # If secured_user is set secured_password will be checked
658
 
            self.secured_user = None
659
 
            self.secured_password = None
660
 
 
661
 
        def authorize(self, channel, username, password):
662
 
            """Return (success, reply_string, filesystem)"""
663
 
            if not _have_medusa:
664
 
                return 0, 'No Medusa.', None
665
 
 
666
 
            channel.persona = -1, -1
667
 
            if username == 'anonymous':
668
 
                channel.read_only = 1
669
 
            else:
670
 
                channel.read_only = 0
671
 
 
672
 
            # Check secured_user if set
673
 
            if (self.secured_user is not None
674
 
                and username == self.secured_user
675
 
                and password != self.secured_password):
676
 
                return 0, 'Password invalid.', None
677
 
            else:
678
 
                return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
679
 
 
680
 
 
681
 
    class ftp_channel(medusa.ftp_server.ftp_channel):
682
 
        """Customized ftp channel"""
683
 
 
684
 
        def log(self, message):
685
 
            """Redirect logging requests."""
686
 
            mutter('_ftp_channel: %s', message)
687
 
 
688
 
        def log_info(self, message, type='info'):
689
 
            """Redirect logging requests."""
690
 
            mutter('_ftp_channel %s: %s', type, message)
691
 
 
692
 
        def cmd_rnfr(self, line):
693
 
            """Prepare for renaming a file."""
694
 
            self._renaming = line[1]
695
 
            self.respond('350 Ready for RNTO')
696
 
            # TODO: jam 20060516 in testing, the ftp server seems to
697
 
            #       check that the file already exists, or it sends
698
 
            #       550 RNFR command failed
699
 
 
700
 
        def cmd_rnto(self, line):
701
 
            """Rename a file based on the target given.
702
 
 
703
 
            rnto must be called after calling rnfr.
704
 
            """
705
 
            if not self._renaming:
706
 
                self.respond('503 RNFR required first.')
707
 
            pfrom = self.filesystem.translate(self._renaming)
708
 
            self._renaming = None
709
 
            pto = self.filesystem.translate(line[1])
710
 
            if os.path.exists(pto):
711
 
                self.respond('550 RNTO failed: file exists')
712
 
                return
713
 
            try:
714
 
                os.rename(pfrom, pto)
715
 
            except (IOError, OSError), e:
716
 
                # TODO: jam 20060516 return custom responses based on
717
 
                #       why the command failed
718
 
                # (bialix 20070418) str(e) on Python 2.5 @ Windows
719
 
                # sometimes don't provide expected error message;
720
 
                # so we obtain such message via os.strerror()
721
 
                self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
722
 
            except:
723
 
                self.respond('550 RNTO failed')
724
 
                # For a test server, we will go ahead and just die
725
 
                raise
726
 
            else:
727
 
                self.respond('250 Rename successful.')
728
 
 
729
 
        def cmd_size(self, line):
730
 
            """Return the size of a file
731
 
 
732
 
            This is overloaded to help the test suite determine if the 
733
 
            target is a directory.
734
 
            """
735
 
            filename = line[1]
736
 
            if not self.filesystem.isfile(filename):
737
 
                if self.filesystem.isdir(filename):
738
 
                    self.respond('550 "%s" is a directory' % (filename,))
739
 
                else:
740
 
                    self.respond('550 "%s" is not a file' % (filename,))
741
 
            else:
742
 
                self.respond('213 %d' 
743
 
                    % (self.filesystem.stat(filename)[stat.ST_SIZE]),)
744
 
 
745
 
        def cmd_mkd(self, line):
746
 
            """Create a directory.
747
 
 
748
 
            Overloaded because default implementation does not distinguish
749
 
            *why* it cannot make a directory.
750
 
            """
751
 
            if len (line) != 2:
752
 
                self.command_not_understood(''.join(line))
753
 
            else:
754
 
                path = line[1]
755
 
                try:
756
 
                    self.filesystem.mkdir (path)
757
 
                    self.respond ('257 MKD command successful.')
758
 
                except (IOError, OSError), e:
759
 
                    # (bialix 20070418) str(e) on Python 2.5 @ Windows
760
 
                    # sometimes don't provide expected error message;
761
 
                    # so we obtain such message via os.strerror()
762
 
                    self.respond ('550 error creating directory: %s' %
763
 
                                  os.strerror(e.errno))
764
 
                except:
765
 
                    self.respond ('550 error creating directory.')
766
 
 
767
 
 
768
 
    class ftp_server(medusa.ftp_server.ftp_server):
769
 
        """Customize the behavior of the Medusa ftp_server.
770
 
 
771
 
        There are a few warts on the ftp_server, based on how it expects
772
 
        to be used.
773
 
        """
774
 
        _renaming = None
775
 
        ftp_channel_class = ftp_channel
776
 
 
777
 
        def __init__(self, *args, **kwargs):
778
 
            mutter('Initializing _ftp_server: %r, %r', args, kwargs)
779
 
            medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
780
 
 
781
 
        def log(self, message):
782
 
            """Redirect logging requests."""
783
 
            mutter('_ftp_server: %s', message)
784
 
 
785
 
        def log_info(self, message, type='info'):
786
 
            """Override the asyncore.log_info so we don't stipple the screen."""
787
 
            mutter('_ftp_server %s: %s', type, message)
788
 
 
789
 
    _test_authorizer = test_authorizer
790
 
    _ftp_channel = ftp_channel
791
 
    _ftp_server = ftp_server
792
 
 
793
 
    return True
794
 
 
795
 
 
796
556
def get_test_permutations():
797
557
    """Return the permutations to be used in testing."""
798
 
    if not _setup_medusa():
799
 
        warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
 
558
    from bzrlib import tests
 
559
    if tests.FTPServerFeature.available():
 
560
        from bzrlib.tests import FTPServer
 
561
        return [(FtpTransport, FTPServer.FTPServer)]
 
562
    else:
800
563
        return []
801
 
    else:
802
 
        return [(FtpTransport, FtpServer)]