~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/ftp_server.py

  • Committer: Andrew Bennetts
  • Date: 2008-02-07 07:05:13 UTC
  • mto: This revision was merged to the branch mainline in revision 3398.
  • Revision ID: andrew.bennetts@canonical.com-20080207070513-u7tvul100g1yn6n7
Add a comment to the new CSS.

Show diffs side-by-side

added added

removed removed

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