/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
1
# Copyright (C) 2007 Canonical Ltd
2
#
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.
7
#
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.
12
#
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
"""
17
FTP test server.
18
19
Based on medusa: http://www.amk.ca/python/code/medusa.html
20
"""
21
22
import asyncore
23
import os
24
import select
25
import stat
26
import threading
27
28
import medusa
29
import medusa.filesys
30
import medusa.ftp_server
31
32
from bzrlib import (
33
    tests,
34
    trace,
35
    transport,
36
    )
37
38
39
class test_authorizer(object):
40
    """A custom Authorizer object for running the test suite.
41
42
    The reason we cannot use dummy_authorizer, is because it sets the
43
    channel to readonly, which we don't always want to do.
44
    """
45
46
    def __init__(self, root):
47
        self.root = root
48
        # If secured_user is set secured_password will be checked
49
        self.secured_user = None
50
        self.secured_password = None
51
52
    def authorize(self, channel, username, password):
53
        """Return (success, reply_string, filesystem)"""
54
        channel.persona = -1, -1
55
        if username == 'anonymous':
56
            channel.read_only = 1
57
        else:
58
            channel.read_only = 0
59
60
        # Check secured_user if set
61
        if (self.secured_user is not None
62
            and username == self.secured_user
63
            and password != self.secured_password):
64
            return 0, 'Password invalid.', None
65
        else:
66
            return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
67
68
69
class ftp_channel(medusa.ftp_server.ftp_channel):
70
    """Customized ftp channel"""
71
72
    def log(self, message):
73
        """Redirect logging requests."""
74
        trace.mutter('ftp_channel: %s', message)
75
76
    def log_info(self, message, type='info'):
77
        """Redirect logging requests."""
78
        trace.mutter('ftp_channel %s: %s', type, message)
79
80
    def cmd_rnfr(self, line):
81
        """Prepare for renaming a file."""
82
        self._renaming = line[1]
83
        self.respond('350 Ready for RNTO')
84
        # TODO: jam 20060516 in testing, the ftp server seems to
85
        #       check that the file already exists, or it sends
86
        #       550 RNFR command failed
87
88
    def cmd_rnto(self, line):
89
        """Rename a file based on the target given.
90
91
        rnto must be called after calling rnfr.
92
        """
93
        if not self._renaming:
94
            self.respond('503 RNFR required first.')
95
        pfrom = self.filesystem.translate(self._renaming)
96
        self._renaming = None
97
        pto = self.filesystem.translate(line[1])
98
        if os.path.exists(pto):
99
            self.respond('550 RNTO failed: file exists')
100
            return
101
        try:
102
            os.rename(pfrom, pto)
103
        except (IOError, OSError), e:
104
            # TODO: jam 20060516 return custom responses based on
105
            #       why the command failed
106
            # (bialix 20070418) str(e) on Python 2.5 @ Windows
107
            # sometimes don't provide expected error message;
108
            # so we obtain such message via os.strerror()
109
            self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
110
        except:
111
            self.respond('550 RNTO failed')
112
            # For a test server, we will go ahead and just die
113
            raise
114
        else:
115
            self.respond('250 Rename successful.')
116
117
    def cmd_size(self, line):
118
        """Return the size of a file
119
120
        This is overloaded to help the test suite determine if the 
121
        target is a directory.
122
        """
123
        filename = line[1]
124
        if not self.filesystem.isfile(filename):
125
            if self.filesystem.isdir(filename):
126
                self.respond('550 "%s" is a directory' % (filename,))
127
            else:
128
                self.respond('550 "%s" is not a file' % (filename,))
129
        else:
130
            self.respond('213 %d' 
131
                % (self.filesystem.stat(filename)[stat.ST_SIZE]),)
132
133
    def cmd_mkd(self, line):
134
        """Create a directory.
135
136
        Overloaded because default implementation does not distinguish
137
        *why* it cannot make a directory.
138
        """
139
        if len (line) != 2:
140
            self.command_not_understood(''.join(line))
141
        else:
142
            path = line[1]
143
            try:
144
                self.filesystem.mkdir (path)
145
                self.respond ('257 MKD command successful.')
146
            except (IOError, OSError), e:
147
                # (bialix 20070418) str(e) on Python 2.5 @ Windows
148
                # sometimes don't provide expected error message;
149
                # so we obtain such message via os.strerror()
150
                self.respond ('550 error creating directory: %s' %
151
                              os.strerror(e.errno))
152
            except:
153
                self.respond ('550 error creating directory.')
154
155
156
class ftp_server(medusa.ftp_server.ftp_server):
157
    """Customize the behavior of the Medusa ftp_server.
158
159
    There are a few warts on the ftp_server, based on how it expects
160
    to be used.
161
    """
162
    _renaming = None
163
    ftp_channel_class = ftp_channel
164
165
    def __init__(self, *args, **kwargs):
166
        trace.mutter('Initializing ftp_server: %r, %r', args, kwargs)
167
        medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
168
169
    def log(self, message):
170
        """Redirect logging requests."""
171
        trace.mutter('ftp_server: %s', message)
172
173
    def log_info(self, message, type='info'):
174
        """Override the asyncore.log_info so we don't stipple the screen."""
175
        trace.mutter('ftp_server %s: %s', type, message)
176
177
178
class FTPServer(transport.Server):
179
    """Common code for FTP server facilities."""
180
181
    def __init__(self):
182
        self._root = None
183
        self._ftp_server = None
184
        self._port = None
185
        self._async_thread = None
186
        # ftp server logs
187
        self.logs = []
188
189
    def get_url(self):
190
        """Calculate an ftp url to this server."""
191
        return 'ftp://foo:bar@localhost:%d/' % (self._port)
192
193
#    def get_bogus_url(self):
194
#        """Return a URL which cannot be connected to."""
195
#        return 'ftp://127.0.0.1:1'
196
197
    def log(self, message):
198
        """This is used by medusa.ftp_server to log connections, etc."""
199
        self.logs.append(message)
200
201
    def setUp(self, vfs_server=None):
202
        from bzrlib.transport.local import LocalURLServer
203
        assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
204
            "FTPServer currently assumes local transport, got %s" % vfs_server
205
206
        self._root = os.getcwdu()
207
        self._ftp_server = ftp_server(
208
            authorizer=test_authorizer(root=self._root),
209
            ip='localhost',
210
            port=0, # bind to a random port
211
            resolver=None,
212
            logger_object=self # Use FTPServer.log() for messages
213
            )
214
        self._port = self._ftp_server.getsockname()[1]
215
        # Don't let it loop forever, or handle an infinite number of requests.
216
        # In this case it will run for 1000s, or 10000 requests
217
        self._async_thread = threading.Thread(
218
                target=FTPServer._asyncore_loop_ignore_EBADF,
219
                kwargs={'timeout':0.1, 'count':10000})
220
        self._async_thread.setDaemon(True)
221
        self._async_thread.start()
222
223
    def tearDown(self):
224
        """See bzrlib.transport.Server.tearDown."""
225
        self._ftp_server.close()
226
        asyncore.close_all()
227
        self._async_thread.join()
228
229
    @staticmethod
230
    def _asyncore_loop_ignore_EBADF(*args, **kwargs):
231
        """Ignore EBADF during server shutdown.
232
233
        We close the socket to get the server to shutdown, but this causes
234
        select.select() to raise EBADF.
235
        """
236
        try:
237
            asyncore.loop(*args, **kwargs)
238
            # FIXME: If we reach that point, we should raise an exception
239
            # explaining that the 'count' parameter in setUp is too low or
240
            # testers may wonder why their test just sits there waiting for a
241
            # server that is already dead. Note that if the tester waits too
242
            # long under pdb the server will also die.
243
        except select.error, e:
244
            if e.args[0] != errno.EBADF:
245
                raise
246
247
248
249