~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/ftp_server.py

  • Committer: Robert Collins
  • Date: 2007-11-09 17:50:31 UTC
  • mto: This revision was merged to the branch mainline in revision 2988.
  • Revision ID: robertc@robertcollins.net-20071109175031-agaiy6530rvbprmb
Change (without backwards compatibility) the
iter_lines_added_or_present_in_versions VersionedFile API to yield the
text version that each line is being returned from. This is useful for
reconcile in determining what inventories reference what texts.
(Robert Collins)

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 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