Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

import raw zope.server code

  • Loading branch information...
commit ca89e19909700b92fbd8fb9d099141b13a4b8862 0 parents
@mcdonc mcdonc authored
Showing with 8,206 additions and 0 deletions.
  1. +155 −0 CHANGES.txt
  2. +1 −0  COPYRIGHT.txt
  3. +44 −0 LICENSE.txt
  4. +19 −0 README.txt
  5. +50 −0 bootstrap.py
  6. +13 −0 buildout.cfg
  7. +76 −0 setup.py
  8. +1 −0  src/zope/__init__.py
  9. +25 −0 src/zope/server/__init__.py
  10. +77 −0 src/zope/server/adjustments.py
  11. +234 −0 src/zope/server/buffers.py
  12. +211 −0 src/zope/server/dualmodechannel.py
  13. +50 −0 src/zope/server/fixedstreamreceiver.py
  14. +11 −0 src/zope/server/ftp/README.txt
  15. +2 −0  src/zope/server/ftp/__init__.py
  16. +32 −0 src/zope/server/ftp/logger.py
  17. +147 −0 src/zope/server/ftp/publisher.py
  18. +957 −0 src/zope/server/ftp/server.py
  19. +1 −0  src/zope/server/ftp/tests/__init__.py
  20. +308 −0 src/zope/server/ftp/tests/demofs.py
  21. +154 −0 src/zope/server/ftp/tests/fstests.py
  22. +38 −0 src/zope/server/ftp/tests/test_demofs.py
  23. +392 −0 src/zope/server/ftp/tests/test_ftpserver.py
  24. +120 −0 src/zope/server/ftp/tests/test_publisher.py
  25. +2 −0  src/zope/server/http/__init__.py
  26. +106 −0 src/zope/server/http/chunking.py
  27. +94 −0 src/zope/server/http/commonaccesslogger.py
  28. +145 −0 src/zope/server/http/http_date.py
  29. +204 −0 src/zope/server/http/httprequestparser.py
  30. +53 −0 src/zope/server/http/httpserver.py
  31. +25 −0 src/zope/server/http/httpserverchannel.py
  32. +253 −0 src/zope/server/http/httptask.py
  33. +60 −0 src/zope/server/http/publisherhttpserver.py
  34. +2 −0  src/zope/server/http/tests/__init__.py
  35. +47 −0 src/zope/server/http/tests/test_commonaccesslogger.py
  36. +33 −0 src/zope/server/http/tests/test_httpdate.py
  37. +136 −0 src/zope/server/http/tests/test_httprequestparser.py
  38. +395 −0 src/zope/server/http/tests/test_httpserver.py
  39. +484 −0 src/zope/server/http/tests/test_wsgiserver.py
  40. +11 −0 src/zope/server/http/tests/wsgi_app.py
  41. +153 −0 src/zope/server/http/wsgihttpserver.py
  42. +298 −0 src/zope/server/interfaces/__init__.py
  43. +413 −0 src/zope/server/interfaces/ftp.py
  44. +34 −0 src/zope/server/interfaces/logger.py
  45. +2 −0  src/zope/server/linereceiver/__init__.py
  46. +68 −0 src/zope/server/linereceiver/linecommandparser.py
  47. +140 −0 src/zope/server/linereceiver/lineserverchannel.py
  48. +68 −0 src/zope/server/linereceiver/linetask.py
  49. +2 −0  src/zope/server/logger/__init__.py
  50. +69 −0 src/zope/server/logger/filelogger.py
  51. +177 −0 src/zope/server/logger/m_syslog.py
  52. +38 −0 src/zope/server/logger/pythonlogger.py
  53. +50 −0 src/zope/server/logger/resolvinglogger.py
  54. +91 −0 src/zope/server/logger/rotatingfilelogger.py
  55. +46 −0 src/zope/server/logger/socketlogger.py
  56. +58 −0 src/zope/server/logger/sysloglogger.py
  57. +40 −0 src/zope/server/logger/taillogger.py
  58. +2 −0  src/zope/server/logger/tests/__init__.py
  59. +66 −0 src/zope/server/logger/tests/test_pythonlogger.py
  60. +29 −0 src/zope/server/logger/unresolvinglogger.py
  61. +90 −0 src/zope/server/maxsockets.py
  62. +148 −0 src/zope/server/serverbase.py
  63. +231 −0 src/zope/server/serverchannelbase.py
  64. +124 −0 src/zope/server/taskthreads.py
  65. +2 −0  src/zope/server/tests/__init__.py
  66. +49 −0 src/zope/server/tests/asyncerror.py
  67. +98 −0 src/zope/server/tests/test_serverbase.py
  68. +151 −0 src/zope/server/tests/test_zombies.py
  69. +228 −0 src/zope/server/trigger.py
  70. +32 −0 src/zope/server/utilities.py
  71. +34 −0 src/zope/server/zlogintegration.py
  72. +7 −0 test.ini
155 CHANGES.txt
@@ -0,0 +1,155 @@
+=======
+CHANGES
+=======
+
+3.8.6 (unreleased)
+------------------
+
+- Nothing changed yet.
+
+
+3.8.5 (2011-09-13)
+------------------
+
+- fixed bug: requests lasting over 15 minutes were sometimes closed
+ prematurely.
+
+3.8.4 (2011-06-07)
+------------------
+
+- Fix syntax error in tests on Python < 2.6.
+
+
+3.8.3 (2011-05-18)
+------------------
+
+- Made ``start_response`` method of WSGI server implementation more compliant
+ with spec:
+
+ http://www.python.org/dev/peps/pep-0333/#the-start-response-callable
+
+3.8.2 (2010-12-04)
+------------------
+
+- Corrected license version in ``zope/server/http/tests/test_wsgiserver.py``.
+
+3.8.1 (2010-08-24)
+------------------
+
+- When the result of a WSGI application was received, ``task.write()`` was
+ only called once to transmit the data. This prohibited the transmission of
+ partial results. Now the WSGI server iterates through the result itself
+ making multiple ``task.write()`` calls, which will cause partial data to be
+ transmitted.
+
+- Created a second test case instance for the post-mortem WSGI server, so it
+ is tested as well.
+
+- Using python's ``doctest`` module instead of deprecated
+ ``zope.testing.doctest``.
+
+3.8.0 (2010-08-05)
+------------------
+
+- Implemented correct server proxy behavior. The HTTP server would always add
+ a "Server" and "Date" response header to the list of response headers
+ regardless whether one had been set already. The HTTP 1.1 spec specifies
+ that a proxy server must not modify the "Server" and "Date" header but add a
+ "Via" header instead.
+
+3.7.0 (2010-08-01)
+------------------
+
+- Implemented proxy support. Proxy requests contain a full URIs and the
+ request parser used to throw that information away. Using
+ ``urlparse.urlsplit()``, all pieces of the URL are recorded.
+
+- The proxy acheme and netloc/hostname are exposed in the WSGI environment as
+ ``zserver.proxy.scheme`` and ``zserver.proxy.host``.
+
+- Made tests runnable via buildout again.
+
+3.6.2 (2010-06-11)
+------------------
+
+- The log message "Exception during task" is no longer logged to the root
+ logger but to zope.server.taskthreads.
+
+
+3.6.1 (2009-10-07)
+------------------
+
+- Made tests pass with current zope.publisher which restricts redirects to the
+ current host by default.
+
+
+3.6.0 (2009-05-27)
+------------------
+
+- Moved some imports from test modules to their setUp to prevent
+ failures when ZEO tests are run by the same testrunner
+
+- Removed unused dependency on zope.deprecation.
+
+- Remove old zpkg-related DEPENDENCIES.cfg file.
+
+
+3.5.0 (2008-03-01)
+------------------
+
+- Improve package meta-data.
+
+- Fix of 599 error on conflict error in request
+ see: http://mail.zope.org/pipermail/zope-dev/2008-January/030844.html
+
+- Removed dependency on ZODB.
+
+
+3.5.0a2 (2007-06-02)
+--------------------
+
+- Made WSGI server really WSGI-compliant by adding variables to the
+ environment that are required by the spec.
+
+
+3.5.0a1 (2007-06-02)
+--------------------
+
+- Added a factory and entry point for PasteDeploy.
+
+
+3.4.3 (2008-08-18)
+------------------
+
+- Moved some imports from test modules to their setUp to prevent
+ failures when ZEO tests are run by the same testrunner
+
+
+3.4.2 (2008-02-02)
+------------------
+
+- Fix of 599 error on conflict error in request
+ see: http://mail.zope.org/pipermail/zope-dev/2008-January/030844.html
+
+
+3.4.1 (2007-06-02)
+------------------
+
+- Made WSGI server really WSGI-compliant by adding variables to the
+ environment that are required by the spec.
+
+
+3.4.0 (2007-06-02)
+------------------
+
+- Removed an unused import. Unchanged otherwise.
+
+
+3.4.0a1 (2007-04-22)
+--------------------
+
+- Initial release as a separate project, corresponds to zope.server
+ from Zope 3.4.0a1
+
+- Made WSGI server really WSGI-compliant by adding variables to the
+ environment that are required by the spec.
1  COPYRIGHT.txt
@@ -0,0 +1 @@
+Zope Foundation and Contributors
44 LICENSE.txt
@@ -0,0 +1,44 @@
+Zope Public License (ZPL) Version 2.1
+
+A copyright notice accompanies this license document that identifies the
+copyright holders.
+
+This license has been certified as open source. It has also been designated as
+GPL compatible by the Free Software Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions in source code must retain the accompanying copyright
+notice, this list of conditions, and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the accompanying copyright
+notice, this list of conditions, and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+3. Names of the copyright holders must not be used to endorse or promote
+products derived from this software without prior written permission from the
+copyright holders.
+
+4. The right to distribute this software or to use it for any purpose does not
+give you the right to use Servicemarks (sm) or Trademarks (tm) of the
+copyright
+holders. Use of them is covered by separate agreement with the copyright
+holders.
+
+5. If any files are modified, you must cause the modified files to carry
+prominent notices stating that you changed the files and the date of any
+change.
+
+Disclaimer
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
19 README.txt
@@ -0,0 +1,19 @@
+This package contains generic base classes for channel-based servers, the
+servers themselves and helper objects, such as tasks and requests.
+
+============
+WSGI Support
+============
+
+`zope.server`'s HTTP server comes with WSGI_ support.
+``zope.server.http.wsgihttpserver.WSGIHTTPServer`` can act as a WSGI gateway.
+There's also an entry point for PasteDeploy_ that lets you use zope.server's
+WSGI gateway from a configuration file, e.g.::
+
+ [server:main]
+ use = egg:zope.server
+ host = 127.0.0.1
+ port = 8080
+
+.. _WSGI: http://www.python.org/dev/peps/pep-0333/
+.. _PasteDeploy: http://pythonpaste.org/deploy/
50 bootstrap.py
@@ -0,0 +1,50 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+ez = {}
+exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+ ).read() in ez
+ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+import pkg_resources
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+if sys.platform == 'win32':
+ cmd = '"%s"' % cmd # work around spawn lamosity on windows
+
+ws = pkg_resources.working_set
+assert os.spawnle(
+ os.P_WAIT, sys.executable, sys.executable,
+ '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout',
+ dict(os.environ,
+ PYTHONPATH=
+ ws.find(pkg_resources.Requirement.parse('setuptools')).location
+ ),
+ ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)
13 buildout.cfg
@@ -0,0 +1,13 @@
+[buildout]
+develop = .
+parts = test paste-test
+
+[paste-test]
+recipe = zc.recipe.egg
+eggs = zope.server
+ PasteDeploy
+ PasteScript
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = zope.server [test]
76 setup.py
@@ -0,0 +1,76 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+# This package is developed by the Zope Toolkit project, documented here:
+# http://docs.zope.org/zopetoolkit
+# When developing and releasing this package, please follow the documented
+# Zope Toolkit policies as described by this documentation.
+##############################################################################
+"""Setup for zope.server package
+"""
+import os
+from setuptools import setup, find_packages
+
+def read(*rnames):
+ return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+
+tests_require = [
+ 'zope.testing',
+ 'zope.i18n',
+ 'zope.component',
+ ]
+
+
+setup(
+ name='zope.server',
+ version='3.8.6dev',
+ author='Zope Foundation and Contributors',
+ author_email='zope-dev@zope.org',
+ description='Zope Server (Web and FTP)',
+ long_description=(
+ read('README.txt')
+ + '\n\n' +
+ read('CHANGES.txt')
+ ),
+ license='ZPL 2.1',
+ keywords=('zope3 server http ftp'),
+ classifiers = [
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Zope Public License',
+ 'Programming Language :: Python',
+ 'Natural Language :: English',
+ 'Operating System :: OS Independent',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Framework :: Zope3'],
+ url='http://pypi.python.org/pypi/zope.server',
+ packages=find_packages('src'),
+ package_dir = {'': 'src'},
+ namespace_packages=['zope',],
+ tests_require=tests_require,
+ install_requires=[
+ 'setuptools',
+ 'zope.interface',
+ 'zope.publisher',
+ 'zope.security',
+ ],
+ extras_require=dict(test=tests_require),
+ include_package_data=True,
+ zip_safe=False,
+ entry_points="""
+ [paste.server_runner]
+ main = zope.server.http.wsgihttpserver:run_paste
+ """
+ )
1  src/zope/__init__.py
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)
25 src/zope/server/__init__.py
@@ -0,0 +1,25 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Zope 3's Servers
+
+This package contains generic base classes for channel-based servers, the
+servers themselves and helper objects, such as tasks and requests.
+"""
+import asyncore
+
+from zope.server.interfaces import IDispatcher
+from zope.interface import classImplements
+
+# Tell the the async.dispatcher that it implements IDispatcher.
+classImplements(asyncore.dispatcher, IDispatcher)
77 src/zope/server/adjustments.py
@@ -0,0 +1,77 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Adjustments are tunable parameters.
+"""
+import socket
+
+from zope.server import maxsockets
+
+
+class Adjustments(object):
+ """This class contains tunable communication parameters.
+
+ You can either change default_adj to adjust parameters for
+ all sockets, or you can create a new instance of this class,
+ change its attributes, and pass it to the channel constructors.
+ """
+
+ # backlog is the argument to pass to socket.listen().
+ backlog = 1024
+
+ # recv_bytes is the argument to pass to socket.recv().
+ recv_bytes = 8192
+
+ # send_bytes is the number of bytes to send to socket.send().
+ # Multiples of 9000 should avoid partly-filled packets, but don't
+ # set this larger than the TCP write buffer size. In Linux,
+ # /proc/sys/net/ipv4/tcp_wmem controls the minimum, default, and
+ # maximum sizes of TCP write buffers.
+ send_bytes = 9000
+
+ # copy_bytes is the number of bytes to copy from one file to another.
+ copy_bytes = 65536
+
+ # Create a tempfile if the pending output data gets larger
+ # than outbuf_overflow. With RAM so cheap, this probably
+ # ought to be set to the 16-32 MB range (circa 2001) for
+ # good performance with big transfers. The default is
+ # conservative.
+ outbuf_overflow = 1050000
+
+ # Create a tempfile if the data received gets larger
+ # than inbuf_overflow.
+ inbuf_overflow = 525000
+
+ # Stop accepting new connections if too many are already active.
+ connection_limit = maxsockets.max_select_sockets() - 3 # Safe
+
+ # Minimum seconds between cleaning up inactive channels.
+ cleanup_interval = 300
+
+ # Maximum seconds to leave an inactive connection open.
+ channel_timeout = 900
+
+ # Boolean: turn off to not log premature client disconnects.
+ log_socket_errors = 1
+
+ # The socket options to set on receiving a connection.
+ # It is a list of (level, optname, value) tuples.
+ # TCP_NODELAY is probably good for Zope, since Zope buffers
+ # data itself.
+ socket_options = [
+ (socket.SOL_TCP, socket.TCP_NODELAY, 1),
+ ]
+
+
+default_adj = Adjustments()
234 src/zope/server/buffers.py
@@ -0,0 +1,234 @@
+##############################################################################
+#
+# Copyright (c) 2001-2004 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Buffers
+"""
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+
+# copy_bytes controls the size of temp. strings for shuffling data around.
+COPY_BYTES = 1 << 18 # 256K
+
+# The maximum number of bytes to buffer in a simple string.
+STRBUF_LIMIT = 8192
+
+
+class FileBasedBuffer(object):
+
+ remain = 0
+
+ def __init__(self, file, from_buffer=None):
+ self.file = file
+ if from_buffer is not None:
+ from_file = from_buffer.getfile()
+ read_pos = from_file.tell()
+ from_file.seek(0)
+ while 1:
+ data = from_file.read(COPY_BYTES)
+ if not data:
+ break
+ file.write(data)
+ self.remain = int(file.tell() - read_pos)
+ from_file.seek(read_pos)
+ file.seek(read_pos)
+
+ def __len__(self):
+ return self.remain
+
+ def append(self, s):
+ file = self.file
+ read_pos = file.tell()
+ file.seek(0, 2)
+ file.write(s)
+ file.seek(read_pos)
+ self.remain = self.remain + len(s)
+
+ def get(self, bytes=-1, skip=0):
+ file = self.file
+ if not skip:
+ read_pos = file.tell()
+ if bytes < 0:
+ # Read all
+ res = file.read()
+ else:
+ res = file.read(bytes)
+ if skip:
+ self.remain -= len(res)
+ else:
+ file.seek(read_pos)
+ return res
+
+ def skip(self, bytes, allow_prune=0):
+ if self.remain < bytes:
+ raise ValueError("Can't skip %d bytes in buffer of %d bytes" % (
+ bytes, self.remain))
+ self.file.seek(bytes, 1)
+ self.remain = self.remain - bytes
+
+ def newfile(self):
+ raise NotImplementedError()
+
+ def prune(self):
+ file = self.file
+ if self.remain == 0:
+ read_pos = file.tell()
+ file.seek(0, 2)
+ sz = file.tell()
+ file.seek(read_pos)
+ if sz == 0:
+ # Nothing to prune.
+ return
+ nf = self.newfile()
+ while 1:
+ data = file.read(COPY_BYTES)
+ if not data:
+ break
+ nf.write(data)
+ self.file = nf
+
+ def getfile(self):
+ return self.file
+
+
+
+class TempfileBasedBuffer(FileBasedBuffer):
+
+ def __init__(self, from_buffer=None):
+ FileBasedBuffer.__init__(self, self.newfile(), from_buffer)
+
+ def newfile(self):
+ from tempfile import TemporaryFile
+ return TemporaryFile('w+b')
+
+
+
+class StringIOBasedBuffer(FileBasedBuffer):
+
+ def __init__(self, from_buffer=None):
+ if from_buffer is not None:
+ FileBasedBuffer.__init__(self, StringIO(), from_buffer)
+ else:
+ # Shortcut. :-)
+ self.file = StringIO()
+
+ def newfile(self):
+ return StringIO()
+
+
+
+class OverflowableBuffer(object):
+ """
+ This buffer implementation has four stages:
+ - No data
+ - String-based buffer
+ - StringIO-based buffer
+ - Temporary file storage
+ The first two stages are fastest for simple transfers.
+ """
+
+ overflowed = 0
+ buf = None
+ strbuf = '' # String-based buffer.
+
+ def __init__(self, overflow):
+ # overflow is the maximum to be stored in a StringIO buffer.
+ self.overflow = overflow
+
+ def __len__(self):
+ buf = self.buf
+ if buf is not None:
+ return len(buf)
+ else:
+ return len(self.strbuf)
+
+ def _create_buffer(self):
+ # print 'creating buffer'
+ strbuf = self.strbuf
+ if len(strbuf) >= self.overflow:
+ self._set_large_buffer()
+ else:
+ self._set_small_buffer()
+ buf = self.buf
+ if strbuf:
+ buf.append(self.strbuf)
+ self.strbuf = ''
+ return buf
+
+ def _set_small_buffer(self):
+ self.buf = StringIOBasedBuffer(self.buf)
+ self.overflowed = 0
+
+ def _set_large_buffer(self):
+ self.buf = TempfileBasedBuffer(self.buf)
+ self.overflowed = 1
+
+ def append(self, s):
+ buf = self.buf
+ if buf is None:
+ strbuf = self.strbuf
+ if len(strbuf) + len(s) < STRBUF_LIMIT:
+ self.strbuf = strbuf + s
+ return
+ buf = self._create_buffer()
+ buf.append(s)
+ sz = len(buf)
+ if not self.overflowed:
+ if sz >= self.overflow:
+ self._set_large_buffer()
+
+ def get(self, bytes=-1, skip=0):
+ buf = self.buf
+ if buf is None:
+ strbuf = self.strbuf
+ if not skip:
+ return strbuf
+ buf = self._create_buffer()
+ return buf.get(bytes, skip)
+
+ def skip(self, bytes, allow_prune=0):
+ buf = self.buf
+ if buf is None:
+ strbuf = self.strbuf
+ if allow_prune and bytes == len(strbuf):
+ # We could slice instead of converting to
+ # a buffer, but that would eat up memory in
+ # large transfers.
+ self.strbuf = ''
+ return
+ buf = self._create_buffer()
+ buf.skip(bytes, allow_prune)
+
+ def prune(self):
+ """
+ A potentially expensive operation that removes all data
+ already retrieved from the buffer.
+ """
+ buf = self.buf
+ if buf is None:
+ self.strbuf = ''
+ return
+ buf.prune()
+ if self.overflowed:
+ sz = len(buf)
+ if sz < self.overflow:
+ # Revert to a faster buffer.
+ self._set_small_buffer()
+
+ def getfile(self):
+ buf = self.buf
+ if buf is None:
+ buf = self._create_buffer()
+ return buf.getfile()
211 src/zope/server/dualmodechannel.py
@@ -0,0 +1,211 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Dual-mode channel
+"""
+import asyncore
+import socket
+from time import time
+
+from zope.server import trigger
+from zope.server.adjustments import default_adj
+from zope.server.buffers import OverflowableBuffer
+
+
+# Create the main trigger if it doesn't exist yet.
+the_trigger = trigger.trigger()
+
+
+class DualModeChannel(asyncore.dispatcher):
+ """Channel that switches between asynchronous and synchronous mode.
+
+ Call set_sync() before using a channel in a thread other than
+ the thread handling the main loop.
+
+ Call set_async() to give the channel back to the thread handling
+ the main loop.
+ """
+
+ # will_close is set to True to close the socket.
+ will_close = False
+
+ # boolean: async or sync mode
+ async_mode = True
+
+ def __init__(self, conn, addr, adj=None):
+ self.addr = addr
+ if adj is None:
+ adj = default_adj
+ self.adj = adj
+ self.outbuf = OverflowableBuffer(adj.outbuf_overflow)
+ self.creation_time = time()
+ asyncore.dispatcher.__init__(self, conn)
+
+ #
+ # ASYNCHRONOUS METHODS
+ #
+
+ def handle_close(self):
+ self.close()
+
+ def writable(self):
+ if not self.async_mode:
+ return 0
+ return self.will_close or self.outbuf
+
+ def handle_write(self):
+ if not self.async_mode:
+ return
+ if self.outbuf:
+ try:
+ self._flush_some()
+ except socket.error:
+ self.handle_comm_error()
+ elif self.will_close:
+ self.close()
+ self.last_activity = time()
+
+ def readable(self):
+ if not self.async_mode:
+ return 0
+ return not self.will_close
+
+ def handle_read(self):
+ if not self.async_mode or self.will_close:
+ return
+ try:
+ data = self.recv(self.adj.recv_bytes)
+ except socket.error:
+ self.handle_comm_error()
+ return
+ self.last_activity = time()
+ self.received(data)
+
+ def received(self, data):
+ """
+ Override to receive data in async mode.
+ """
+ pass
+
+ def handle_comm_error(self):
+ """
+ Designed for handling communication errors that occur
+ during asynchronous operations *only*. Probably should log
+ this, but in a different place.
+ """
+ self.handle_error()
+
+ def set_sync(self):
+ """Switches to synchronous mode.
+
+ The main thread will stop calling received().
+ """
+ self.async_mode = False
+
+ #
+ # SYNCHRONOUS METHODS
+ #
+
+ def flush(self, block=True):
+ """Sends pending data.
+
+ If block is set, this pauses the application. If it is turned
+ off, only the amount of data that can be sent without blocking
+ is sent.
+ """
+ if not block:
+ while self._flush_some():
+ pass
+ return
+ blocked = False
+ try:
+ while self.outbuf:
+ # We propagate errors to the application on purpose.
+ if not blocked:
+ self.socket.setblocking(1)
+ blocked = True
+ self._flush_some()
+ finally:
+ if blocked:
+ self.socket.setblocking(0)
+
+ def set_async(self):
+ """Switches to asynchronous mode.
+
+ The main thread will begin calling received() again.
+ """
+ self.async_mode = True
+ self.pull_trigger()
+ self.last_activity = time()
+
+ #
+ # METHODS USED IN BOTH MODES
+ #
+
+ def write(self, data):
+ wrote = 0
+ if isinstance(data, str):
+ if data:
+ self.outbuf.append(data)
+ wrote = len(data)
+ else:
+ for v in data:
+ if v:
+ self.outbuf.append(v)
+ wrote += len(v)
+
+ while len(self.outbuf) >= self.adj.send_bytes:
+ # Send what we can without blocking.
+ # We propagate errors to the application on purpose
+ # (to stop the application if the connection closes).
+ if not self._flush_some():
+ break
+
+ return wrote
+
+ def pull_trigger(self):
+ """Wakes up the main loop.
+ """
+ the_trigger.pull_trigger()
+
+ def _flush_some(self):
+ """Flushes data.
+
+ Returns 1 if some data was sent."""
+ outbuf = self.outbuf
+ if outbuf and self.connected:
+ chunk = outbuf.get(self.adj.send_bytes)
+ num_sent = self.send(chunk)
+ if num_sent:
+ outbuf.skip(num_sent, 1)
+ return 1
+ return 0
+
+ def close_when_done(self):
+ # Flush all possible.
+ while self._flush_some():
+ pass
+ self.will_close = True
+ if not self.async_mode:
+ # For safety, don't close the socket until the
+ # main thread calls handle_write().
+ self.async_mode = True
+ self.pull_trigger()
+
+ def close(self):
+ # Always close in asynchronous mode. If the connection is
+ # closed in a thread, the main loop can end up with a bad file
+ # descriptor.
+ assert self.async_mode
+ self.connected = False
+ asyncore.dispatcher.close(self)
50 src/zope/server/fixedstreamreceiver.py
@@ -0,0 +1,50 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Fixed Stream Receiver
+"""
+
+from zope.server.interfaces import IStreamConsumer
+from zope.interface import implements
+
+
+class FixedStreamReceiver(object):
+
+ implements(IStreamConsumer)
+
+ # See IStreamConsumer
+ completed = 0
+
+ def __init__(self, cl, buf):
+ self.remain = cl
+ self.buf = buf
+
+ def received(self, data):
+ 'See IStreamConsumer'
+ rm = self.remain
+ if rm < 1:
+ self.completed = 1 # Avoid any chance of spinning
+ return 0
+ datalen = len(data)
+ if rm <= datalen:
+ self.buf.append(data[:rm])
+ self.remain = 0
+ self.completed = 1
+ return rm
+ else:
+ self.buf.append(data)
+ self.remain -= datalen
+ return datalen
+
+ def getfile(self):
+ return self.buf.getfile()
11 src/zope/server/ftp/README.txt
@@ -0,0 +1,11 @@
+FTP Framework
+
+ This file contains documentation on the FTP server
+ framework.
+
+ The core server is implemented in server.py. This relies on a
+ file-system abstraction, defined in zope.server.interfaces.py.
+
+ The publisher module provides the connection to the object
+ publsihing system by providing a file-system implementation that
+ delegates file-system operations to objects through the publisher.
2  src/zope/server/ftp/__init__.py
@@ -0,0 +1,2 @@
+#
+# This file is necessary to make this directory a package.
32 src/zope/server/ftp/logger.py
@@ -0,0 +1,32 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Common FTP Activity Logger
+"""
+import time
+
+from zope.server.http.commonaccesslogger import CommonAccessLogger
+
+class CommonFTPActivityLogger(CommonAccessLogger):
+ """Outputs hits in common HTTP log format."""
+
+ def log(self, task):
+ """Receives a completed task and logs it in the common log format."""
+ now = time.time()
+ message = ' - %s [%s] "%s %s"' % (task.channel.username,
+ self.log_date_string(now),
+ task.m_name[4:].upper(),
+ task.channel.cwd,
+ )
+
+ self.output.logRequest(task.channel.addr[0], message)
147 src/zope/server/ftp/publisher.py
@@ -0,0 +1,147 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Zope Publisher-based FTP Server
+
+This FTP server uses the Zope 3 Publisher to execute commands.
+"""
+import posixpath
+
+from cStringIO import StringIO
+
+from zope.server.interfaces.ftp import IFileSystem
+from zope.server.interfaces.ftp import IFileSystemAccess
+
+from zope.server.ftp.server import FTPServer
+from zope.publisher.publish import publish
+
+from zope.interface import implements
+
+class PublisherFileSystem(object):
+ """Generic Publisher FileSystem implementation."""
+
+ implements(IFileSystem)
+
+ def __init__ (self, credentials, request_factory):
+ self.credentials = credentials
+ self.request_factory = request_factory
+
+ def type(self, path):
+ if path == '/':
+ return 'd'
+
+ return self._execute(path, 'type')
+
+ def names(self, path, filter=None):
+ return self._execute(path, 'names', split=False, filter=filter)
+
+ def ls(self, path, filter=None):
+ return self._execute(path, 'ls', split=False, filter=filter)
+
+ def readfile(self, path, outstream, start=0, end=None):
+ return self._execute(path, 'readfile',
+ outstream=outstream, start=start, end=end)
+
+ def lsinfo(self, path):
+ return self._execute(path, 'lsinfo')
+
+ def mtime(self, path):
+ return self._execute(path, 'mtime')
+
+ def size(self, path):
+ return self._execute(path, 'size')
+
+ def mkdir(self, path):
+ return self._execute(path, 'mkdir')
+
+ def remove(self, path):
+ return self._execute(path, 'remove')
+
+ def rmdir(self, path):
+ return self._execute(path, 'rmdir')
+
+ def rename(self, old, new):
+ 'See IWriteFileSystem'
+ old = self._translate(old)
+ new = self._translate(new)
+ path0, old = posixpath.split(old)
+ path1, new = posixpath.split(new)
+ assert path0 == path1
+ return self._execute(path0, 'rename', split=False, old=old, new=new)
+
+ def writefile(self, path, instream, start=None, end=None, append=False):
+ 'See IWriteFileSystem'
+ return self._execute(
+ path, 'writefile',
+ instream=instream, start=start, end=end, append=append)
+
+ def writable(self, path):
+ 'See IWriteFileSystem'
+ return self._execute(path, 'writable')
+
+ def _execute(self, path, command, split=True, **kw):
+ env = {}
+ env.update(kw)
+ env['command'] = command
+
+ path = self._translate(path)
+
+ if split:
+ env['path'], env['name'] = posixpath.split(path)
+ else:
+ env['path'] = path
+
+ env['credentials'] = self.credentials
+ request = self.request_factory(StringIO(''), env)
+
+ # Note that publish() calls close() on request, which deletes the
+ # response from the request, so that we need to keep track of it.
+ # agroszer: 2008.feb.1.: currently the above seems not to be true
+ # request will KEEP the response on close()
+ # even more if a retry occurs in the publisher,
+ # the response will be LOST, so we must accept the returned request
+ request = publish(request)
+ return request.response.getResult()
+
+ def _translate (self, path):
+ # Normalize
+ path = posixpath.normpath(path)
+ if path.startswith('..'):
+ # Someone is trying to get lower than the permitted root.
+ # We just ignore it.
+ path = '/'
+ return path
+
+
+class PublisherFTPServer(FTPServer):
+ """Generic FTP Server"""
+
+ def __init__(self, request_factory, name, ip, port, *args, **kw):
+ fs_access = PublisherFileSystemAccess(request_factory)
+ super(PublisherFTPServer, self).__init__(ip, port, fs_access,
+ *args, **kw)
+
+class PublisherFileSystemAccess(object):
+
+ implements(IFileSystemAccess)
+
+ def __init__(self, request_factory):
+ self.request_factory = request_factory
+
+ def authenticate(self, credentials):
+ # We can't actually do any authentication initially, as the
+ # user may not be defined at the root.
+ pass
+
+ def open(self, credentials):
+ return PublisherFileSystem(credentials, self.request_factory)
957 src/zope/server/ftp/server.py
@@ -0,0 +1,957 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""FTP Server
+"""
+import asyncore
+import posixpath
+import socket
+from datetime import date, timedelta
+from getopt import getopt, GetoptError
+
+from zope.security.interfaces import Unauthorized
+from zope.interface import implements
+from zope.server.buffers import OverflowableBuffer
+from zope.server.interfaces import ITask
+from zope.server.interfaces.ftp import IFileSystemAccess
+from zope.server.interfaces.ftp import IFTPCommandHandler
+from zope.server.linereceiver.lineserverchannel import LineServerChannel
+from zope.server.serverbase import ServerBase
+from zope.server.dualmodechannel import DualModeChannel, the_trigger
+
+status_messages = {
+ 'OPEN_DATA_CONN' : '150 Opening %s mode data connection for file list',
+ 'OPEN_CONN' : '150 Opening %s connection for %s',
+ 'SUCCESS_200' : '200 %s command successful.',
+ 'TYPE_SET_OK' : '200 Type set to %s.',
+ 'STRU_OK' : '200 STRU F Ok.',
+ 'MODE_OK' : '200 MODE S Ok.',
+ 'FILE_DATE' : '213 %4d%02d%02d%02d%02d%02d',
+ 'FILE_SIZE' : '213 %d Bytes',
+ 'HELP_START' : '214-The following commands are recognized',
+ 'HELP_END' : '214 Help done.',
+ 'SERVER_TYPE' : '215 %s Type: %s',
+ 'SERVER_READY' : '220 %s FTP server (Zope Async/Thread V0.1) ready.',
+ 'GOODBYE' : '221 Goodbye.',
+ 'SUCCESS_226' : '226 %s command successful.',
+ 'TRANS_SUCCESS' : '226 Transfer successful.',
+ 'PASV_MODE_MSG' : '227 Entering Passive Mode (%s,%d,%d)',
+ 'LOGIN_SUCCESS' : '230 Login Successful.',
+ 'SUCCESS_250' : '250 %s command successful.',
+ 'SUCCESS_257' : '257 %s command successful.',
+ 'ALREADY_CURRENT' : '257 "%s" is the current directory.',
+ 'PASS_REQUIRED' : '331 Password required',
+ 'RESTART_TRANSFER' : '350 Restarting at %d. Send STORE or '
+ 'RETRIEVE to initiate transfer.',
+ 'READY_FOR_DEST' : '350 File exists, ready for destination.',
+ 'NO_DATA_CONN' : "425 Can't build data connection",
+ 'TRANSFER_ABORTED' : '426 Connection closed; transfer aborted.',
+ 'CMD_UNKNOWN' : "500 '%s': command not understood.",
+ 'INTERNAL_ERROR' : "500 Internal error: %s",
+ 'ERR_ARGS' : '500 Bad command arguments',
+ 'MODE_UNKOWN' : '502 Unimplemented MODE type',
+ 'WRONG_BYTE_SIZE' : '504 Byte size must be 8',
+ 'STRU_UNKNOWN' : '504 Unimplemented STRU type',
+ 'NOT_AUTH' : "530 You are not authorized to perform the "
+ "'%s' command",
+ 'LOGIN_REQUIRED' : '530 Please log in with USER and PASS',
+ 'LOGIN_MISMATCH' : '530 The username and password do not match.',
+ 'ERR_NO_LIST' : '550 Could not list directory or file: %s',
+ 'ERR_NO_DIR' : '550 "%s": No such directory.',
+ 'ERR_NO_FILE' : '550 "%s": No such file.',
+ 'ERR_NO_DIR_FILE' : '550 "%s": No such file or directory.',
+ 'ERR_IS_NOT_FILE' : '550 "%s": Is not a file',
+ 'ERR_CREATE_FILE' : '550 Error creating file.',
+ 'ERR_CREATE_DIR' : '550 Error creating directory: %s',
+ 'ERR_DELETE_FILE' : '550 Error deleting file: %s',
+ 'ERR_DELETE_DIR' : '550 Error removing directory: %s',
+ 'ERR_OPEN_READ' : '553 Could not open file for reading: %s',
+ 'ERR_OPEN_WRITE' : '553 Could not open file for writing: %s',
+ 'ERR_IO' : '553 I/O Error: %s',
+ 'ERR_RENAME' : '560 Could not rename "%s" to "%s": %s',
+ 'ERR_RNFR_SOURCE' : '560 No source filename specify. Call RNFR first.',
+ }
+
+class FTPServerChannel(LineServerChannel):
+ """The FTP Server Channel represents a connection to a particular
+ client. We can therefore store information here."""
+
+ implements(IFTPCommandHandler)
+
+
+ # List of commands that are always available
+ special_commands = (
+ 'cmd_quit', 'cmd_type', 'cmd_noop', 'cmd_user', 'cmd_pass')
+
+ # These are the commands that are accessing the filesystem.
+ # Since this could be also potentially a longer process, these commands
+ # are also the ones that are executed in a different thread.
+ thread_commands = (
+ 'cmd_appe', 'cmd_cdup', 'cmd_cwd', 'cmd_dele',
+ 'cmd_list', 'cmd_nlst', 'cmd_mdtm', 'cmd_mkd',
+ 'cmd_pass', 'cmd_retr', 'cmd_rmd', 'cmd_rnfr',
+ 'cmd_rnto', 'cmd_size', 'cmd_stor', 'cmd_stru')
+
+ # Define the status messages
+ status_messages = status_messages
+
+ # Define the type of directory listing this server is returning
+ system = ('UNIX', 'L8')
+
+ # comply with (possibly troublesome) RFC959 requirements
+ # This is necessary to correctly run an active data connection
+ # through a firewall that triggers on the source port (expected
+ # to be 'L-1', or 20 in the normal case).
+ bind_local_minus_one = 0
+
+ restart_position = 0
+
+ type_map = {'a':'ASCII', 'i':'Binary', 'e':'EBCDIC', 'l':'Binary'}
+
+ type_mode_map = {'a':'t', 'i':'b', 'e':'b', 'l':'b'}
+
+
+ def __init__(self, server, conn, addr, adj=None):
+ super(FTPServerChannel, self).__init__(server, conn, addr, adj)
+
+ self.port_addr = None # The client's PORT address
+ self.passive_listener = None # The PASV listener
+ self.client_dc = None # The data connection
+
+ self.transfer_mode = 'a' # Have to default to ASCII :-|
+ self.passive_mode = 0
+ self.cwd = '/'
+ self._rnfr = None
+
+ self.username = ''
+ self.credentials = None
+
+ self.reply('SERVER_READY', self.server.server_name)
+
+
+ def _getFileSystem(self):
+ """Open the filesystem using the current credentials."""
+ return self.server.fs_access.open(self.credentials)
+
+
+ def cmd_abor(self, args):
+ 'See IFTPCommandHandler'
+ assert self.async_mode
+ self.reply('TRANSFER_ABORTED')
+ self.abortPassive()
+ self.abortData()
+
+
+ def cmd_appe (self, args):
+ 'See IFTPCommandHandler'
+ return self.cmd_stor(args, 'a')
+
+
+ def cmd_cdup(self, args):
+ 'See IFTPCommandHandler'
+ path = self._generatePath('../')
+ if self._getFileSystem().type(path):
+ self.cwd = path
+ self.reply('SUCCESS_250', 'CDUP')
+ else:
+ self.reply('ERR_NO_FILE', path)
+
+
+ def cmd_cwd(self, args):
+ 'See IFTPCommandHandler'
+ path = self._generatePath(args)
+ if self._getFileSystem().type(path) == 'd':
+ self.cwd = path
+ self.reply('SUCCESS_250', 'CWD')
+ else:
+ self.reply('ERR_NO_DIR', path)
+
+
+ def cmd_dele(self, args):
+ 'See IFTPCommandHandler'
+ if not args:
+ self.reply('ERR_ARGS')
+ return
+ path = self._generatePath(args)
+
+ try:
+ self._getFileSystem().remove(path)
+ except OSError, err:
+ self.reply('ERR_DELETE_FILE', str(err))
+ else:
+ self.reply('SUCCESS_250', 'DELE')
+
+
+ def cmd_help(self, args):
+ 'See IFTPCommandHandler'
+ self.reply('HELP_START', flush=0)
+ self.write('Help goes here somewhen.\r\n')
+ self.reply('HELP_END')
+
+
+ def cmd_list(self, args, long=1):
+ 'See IFTPCommandHandler'
+ opts = ()
+ if args.strip().startswith('-'):
+ try:
+ opts, args = getopt(args.split(), 'Llad')
+ except GetoptError:
+ self.reply('ERR_ARGS')
+ return
+ if len(args) > 1:
+ self.reply('ERR_ARGS')
+ return
+ args = args and args[0] or ''
+
+ fs = self._getFileSystem()
+ path = self._generatePath(args)
+ if not fs.type(path):
+ self.reply('ERR_NO_DIR_FILE', path)
+ return
+ args = args.split()
+ try:
+ s = self.getList(
+ args, long,
+ directory=bool([opt for opt in opts if opt[0]=='-d'])
+ )
+ except OSError, err:
+ self.reply('ERR_NO_LIST', str(err))
+ return
+ ok_reply = ('OPEN_DATA_CONN', self.type_map[self.transfer_mode])
+ cdc = RETRChannel(self, ok_reply)
+ try:
+ cdc.write(s)
+ cdc.close_when_done()
+ except OSError, err:
+ self.reply('ERR_NO_LIST', str(err))
+ cdc.reported = True
+ cdc.close_when_done()
+
+ def getList(self, args, long=0, directory=0):
+ # we need to scan the command line for arguments to '/bin/ls'...
+ fs = self._getFileSystem()
+ path_args = []
+ for arg in args:
+ if arg[0] != '-':
+ path_args.append (arg)
+ else:
+ # ignore arguments
+ pass
+ if len(path_args) < 1:
+ path = '.'
+ else:
+ path = path_args[0]
+
+ path = self._generatePath(path)
+
+ if fs.type(path) == 'd' and not directory:
+ if long:
+ file_list = map(ls, fs.ls(path))
+ else:
+ file_list = fs.names(path)
+ else:
+ if long:
+ file_list = [ls(fs.lsinfo(path))]
+ else:
+ file_list = [posixpath.split(path)[1]]
+
+ return '\r\n'.join(file_list) + '\r\n'
+
+
+ def cmd_mdtm(self, args):
+ 'See IFTPCommandHandler'
+ fs = self._getFileSystem()
+ # We simply do not understand this non-standard extension to MDTM
+ if len(args.split()) > 1:
+ self.reply('ERR_ARGS')
+ return
+ path = self._generatePath(args)
+
+ if fs.type(path) != 'f':
+ self.reply('ERR_IS_NOT_FILE', path)
+ else:
+ mtime = fs.mtime(path)
+ if mtime is not None:
+ mtime = (mtime.year, mtime.month, mtime.day,
+ mtime.hour, mtime. minute, mtime.second)
+ else:
+ mtime = 0, 0, 0, 0, 0, 0
+
+ self.reply('FILE_DATE', mtime)
+
+
+ def cmd_mkd(self, args):
+ 'See IFTPCommandHandler'
+ if not args:
+ self.reply('ERR_ARGS')
+ return
+ path = self._generatePath(args)
+ try:
+ self._getFileSystem().mkdir(path)
+ except OSError, err:
+ self.reply('ERR_CREATE_DIR', str(err))
+ else:
+ self.reply('SUCCESS_257', 'MKD')
+
+
+ def cmd_mode(self, args):
+ 'See IFTPCommandHandler'
+ if len(args) == 1 and args in 'sS':
+ self.reply('MODE_OK')
+ else:
+ self.reply('MODE_UNKNOWN')
+
+
+ def cmd_nlst(self, args):
+ 'See IFTPCommandHandler'
+ self.cmd_list(args, 0)
+
+
+ def cmd_noop(self, args):
+ 'See IFTPCommandHandler'
+ self.reply('SUCCESS_200', 'NOOP')
+
+
+ def cmd_pass(self, args):
+ 'See IFTPCommandHandler'
+ self.authenticated = 0
+ password = args
+ credentials = (self.username, password)
+ try:
+ self.server.fs_access.authenticate(credentials)
+ except Unauthorized:
+ self.reply('LOGIN_MISMATCH')
+ self.close_when_done()
+ else:
+ self.credentials = credentials
+ self.authenticated = 1
+ self.reply('LOGIN_SUCCESS')
+
+
+ def cmd_pasv(self, args):
+ 'See IFTPCommandHandler'
+ assert self.async_mode
+ # Kill any existing passive listener first.
+ self.abortPassive()
+ local_addr = self.getsockname()[0]
+ self.passive_listener = PassiveListener(self, local_addr)
+ port = self.passive_listener.port
+ self.reply('PASV_MODE_MSG', (','.join(local_addr.split('.')),
+ port/256,
+ port%256 ) )
+
+
+ def cmd_port(self, args):
+ 'See IFTPCommandHandler'
+ info = args.split(',')
+ ip = '.'.join(info[:4])
+ port = int(info[4])*256 + int(info[5])
+ # how many data connections at a time?
+ # I'm assuming one for now...
+ # TODO: we should (optionally) verify that the
+ # ip number belongs to the client. [wu-ftpd does this?]
+ self.port_addr = (ip, port)
+ self.reply('SUCCESS_200', 'PORT')
+
+
+ def cmd_pwd(self, args):
+ 'See IFTPCommandHandler'
+ self.reply('ALREADY_CURRENT', self.cwd)
+
+
+ def cmd_quit(self, args):
+ 'See IFTPCommandHandler'
+ self.reply('GOODBYE')
+ self.close_when_done()
+
+
+ def cmd_retr(self, args):
+ 'See IFTPCommandHandler'
+ fs = self._getFileSystem()
+ if not args:
+ self.reply('CMD_UNKNOWN', 'RETR')
+ path = self._generatePath(args)
+
+ if not (fs.type(path) == 'f'):
+ self.reply('ERR_IS_NOT_FILE', path)
+ return
+
+ start = 0
+ if self.restart_position:
+ start = self.restart_position
+ self.restart_position = 0
+
+ ok_reply = 'OPEN_CONN', (self.type_map[self.transfer_mode], path)
+ cdc = RETRChannel(self, ok_reply)
+ outstream = ApplicationOutputStream(cdc)
+
+ try:
+ fs.readfile(path, outstream, start)
+ cdc.close_when_done()
+ except OSError, err:
+ self.reply('ERR_OPEN_READ', str(err))
+ cdc.reported = True
+ cdc.close_when_done()
+ except IOError, err:
+ self.reply('ERR_IO', str(err))
+ cdc.reported = True
+ cdc.close_when_done()
+
+
+ def cmd_rest(self, args):
+ 'See IFTPCommandHandler'
+ try:
+ pos = int(args)
+ except ValueError:
+ self.reply('ERR_ARGS')
+ return
+ self.restart_position = pos
+ self.reply('RESTART_TRANSFER', pos)
+
+
+ def cmd_rmd(self, args):
+ 'See IFTPCommandHandler'
+ if not args:
+ self.reply('ERR_ARGS')
+ return
+ path = self._generatePath(args)
+ try:
+ self._getFileSystem().rmdir(path)
+ except OSError, err:
+ self.reply('ERR_DELETE_DIR', str(err))
+ else:
+ self.reply('SUCCESS_250', 'RMD')
+
+
+ def cmd_rnfr(self, args):
+ 'See IFTPCommandHandler'
+ path = self._generatePath(args)
+ if self._getFileSystem().type(path):
+ self._rnfr = path
+ self.reply('READY_FOR_DEST')
+ else:
+ self.reply('ERR_NO_FILE', path)
+
+
+ def cmd_rnto(self, args):
+ 'See IFTPCommandHandler'
+ path = self._generatePath(args)
+ if self._rnfr is None:
+ self.reply('ERR_RENAME')
+ try:
+ self._getFileSystem().rename(self._rnfr, path)
+ except OSError, err:
+ self.reply('ERR_RENAME', (self._rnfr, path, str(err)))
+ else:
+ self.reply('SUCCESS_250', 'RNTO')
+ self._rnfr = None
+
+
+ def cmd_size(self, args):
+ 'See IFTPCommandHandler'
+ path = self._generatePath(args)
+ fs = self._getFileSystem()
+ if fs.type(path) != 'f':
+ self.reply('ERR_NO_FILE', path)
+ else:
+ self.reply('FILE_SIZE', fs.size(path))
+
+
+ def cmd_stor(self, args, write_mode='w'):
+ 'See IFTPCommandHandler'
+ if not args:
+ self.reply('ERR_ARGS')
+ return
+ path = self._generatePath(args)
+
+ start = 0
+ if self.restart_position:
+ self.start = self.restart_position
+ mode = write_mode + self.type_mode_map[self.transfer_mode]
+
+ if not self._getFileSystem().writable(path):
+ self.reply('ERR_OPEN_WRITE', "Can't write file")
+ return
+
+ cdc = STORChannel(self, (path, mode, start))
+ self.syncConnectData(cdc)
+ self.reply('OPEN_CONN', (self.type_map[self.transfer_mode], path))
+
+
+ def finishSTOR(self, buffer, (path, mode, start)):
+ """Called by STORChannel when the client has sent all data."""
+ assert not self.async_mode
+ try:
+ infile = buffer.getfile()
+ infile.seek(0)
+ self._getFileSystem().writefile(path, infile, start,
+ append=(mode[0]=='a'))
+ except OSError, err:
+ self.reply('ERR_OPEN_WRITE', str(err))
+ except IOError, err:
+ self.reply('ERR_IO', str(err))
+ except:
+ self.exception()
+ else:
+ self.reply('TRANS_SUCCESS')
+
+
+ def cmd_stru(self, args):
+ 'See IFTPCommandHandler'
+ if len(args) == 1 and args in 'fF':
+ self.reply('STRU_OK')
+ else:
+ self.reply('STRU_UNKNOWN')
+
+
+ def cmd_syst(self, args):
+ 'See IFTPCommandHandler'
+ self.reply('SERVER_TYPE', self.system)
+
+
+ def cmd_type(self, args):
+ 'See IFTPCommandHandler'
+ # ascii, ebcdic, image, local <byte size>
+ args = args.split()
+ t = args[0].lower()
+ # no support for EBCDIC
+ # if t not in ['a','e','i','l']:
+ if t not in ['a','i','l']:
+ self.reply('ERR_ARGS')
+ elif t == 'l' and (len(args) > 2 and args[2] != '8'):
+ self.reply('WRONG_BYTE_SIZE')
+ else:
+ self.transfer_mode = t
+ self.reply('TYPE_SET_OK', self.type_map[t])
+
+
+ def cmd_user(self, args):
+ 'See IFTPCommandHandler'
+ self.authenticated = 0
+ if len(args) > 1:
+ self.username = args
+ self.reply('PASS_REQUIRED')
+ else:
+ self.reply('ERR_ARGS')
+
+ ############################################################
+
+ def _generatePath(self, args):
+ """Convert relative paths to absolute paths."""
+ # We use posixpath even on non-Posix platforms because we don't want
+ # slashes converted to backslashes.
+ path = posixpath.join(self.cwd, args)
+ return posixpath.normpath(path)
+
+ def syncConnectData(self, cdc):
+ """Calls asyncConnectData in the asynchronous thread."""
+ the_trigger.pull_trigger(lambda: self.asyncConnectData(cdc))
+
+ def asyncConnectData(self, cdc):
+ """Starts connecting the data channel.
+
+ This is a little complicated because the data connection might
+ be established already (in passive mode) or might be
+ established in the near future (in port or passive mode.) If
+ the connection has already been established,
+ self.passive_listener already has a socket and is waiting for
+ a call to connectData(). If the connection has not been
+ established in passive mode, the passive listener will
+ remember the data channel and send it when it's ready. In port
+ mode, this method tells the data connection to connect.
+ """
+ self.abortData()
+ self.client_dc = cdc
+ if self.passive_listener is not None:
+ # Connect via PASV
+ self.passive_listener.connectData(cdc)
+ if self.port_addr:
+ # Connect via PORT
+ a = self.port_addr
+ self.port_addr = None
+ cdc.connectPort(a)
+
+ def connectedPassive(self):
+ """Accepted a passive connection."""
+ self.passive_listener = None
+
+ def abortPassive(self):
+ """Close the passive listener."""
+ if self.passive_listener is not None:
+ self.passive_listener.abort()
+ self.passive_listener = None
+
+ def abortData(self):
+ """Close the data connection."""
+ if self.client_dc is not None:
+ self.client_dc.abort()
+ self.client_dc = None
+
+ def closedData(self):
+ self.client_dc = None
+
+ def close(self):
+ # Make sure the passive listener and active client DC get closed.
+ self.abortPassive()
+ self.abortData()
+ LineServerChannel.close(self)
+
+
+
+def ls(ls_info):
+ """Formats a directory entry similarly to the 'ls' command.
+ """
+
+ info = {
+ 'owner_name': 'na',
+ 'owner_readable': True,
+ 'owner_writable': True,
+ 'group_name': "na",
+ 'group_readable': True,
+ 'group_writable': True,
+ 'other_readable': False,
+ 'other_writable': False,
+ 'nlinks': 1,
+ 'size': 0,
+ }
+
+ if ls_info['type'] == 'd':
+ info['owner_executable'] = True
+ info['group_executable'] = True
+ info['other_executable'] = True
+ else:
+ info['owner_executable'] = False
+ info['group_executable'] = False
+ info['other_executable'] = False
+
+ info.update(ls_info)
+
+ mtime = info.get('mtime')
+ if mtime is not None:
+ if date.today() - mtime.date() > timedelta(days=180):
+ mtime = mtime.strftime('%b %d %Y')
+ else:
+ mtime = mtime.strftime('%b %d %H:%M')
+ else:
+ mtime = "Jan 02 0000"
+
+ return "%s%s%s%s%s%s%s%s%s%s %3d %-8s %-8s %8d %s %s" % (
+ info['type'] == 'd' and 'd' or '-',
+ info['owner_readable'] and 'r' or '-',
+ info['owner_writable'] and 'w' or '-',
+ info['owner_executable'] and 'x' or '-',
+ info['group_readable'] and 'r' or '-',
+ info['group_writable'] and 'w' or '-',
+ info['group_executable'] and 'x' or '-',
+ info['other_readable'] and 'r' or '-',
+ info['other_writable'] and 'w' or '-',
+ info['other_executable'] and 'x' or '-',
+ info['nlinks'],
+ info['owner_name'],
+ info['group_name'],
+ info['size'],
+ mtime,
+ info['name'],
+ )
+
+
+class PassiveListener(asyncore.dispatcher):
+ """This socket accepts a data connection, used when the server has
+ been placed in passive mode. Although the RFC implies that we
+ ought to be able to use the same listener over and over again,
+ this presents a problem: how do we shut it off, so that we are
+ accepting connections only when we expect them? [we can't]
+
+ wuftpd, and probably all the other servers, solve this by
+ allowing only one connection to hit this listener. They then
+ close it. Any subsequent data-connection command will then try
+ for the default port on the client side [which is of course
+ never there]. So the 'always-send-PORT/PASV' behavior seems
+ required.
+
+ Another note: wuftpd will also be listening on the channel as
+ soon as the PASV command is sent. It does not wait for a data
+ command first.
+ """
+
+ def __init__ (self, control_channel, local_addr):
+ asyncore.dispatcher.__init__ (self)
+ self.control_channel = control_channel
+ self.accepted = None # The accepted socket address
+ self.client_dc = None # The data connection to accept the socket
+ self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.closed = False
+ # bind to an address on the interface where the
+ # control connection is connected.
+ self.bind((local_addr, 0))
+ self.port = self.getsockname()[1]
+ self.listen(1)
+
+ def log (self, *ignore):
+ pass
+
+ def abort(self):
+ """Abort the passive listener."""
+ if not self.closed:
+ self.closed = True
+ self.close()
+ if self.accepted is not None:
+ self.accepted.close()
+
+ def handle_accept (self):
+ """Accept a connection from the client.
+
+ For some reason, sometimes accept() returns None instead of a
+ socket. This code ignores that case.
+ """
+ v = self.accept()
+ if v is None:
+ return
+ self.accepted, addr = v
+ if self.accepted is None:
+ return
+ self.accepted.setblocking(0)
+ self.closed = True
+ self.close()
+ if self.client_dc is not None:
+ self.connectData(self.client_dc)
+
+ def connectData(self, cdc):
+ """Sends the connection to the data channel.
+
+ If the connection has not yet been made, sends the connection
+ when it becomes available.
+ """
+ if self.accepted is not None:
+ cdc.set_socket(self.accepted)
+ # Note that this method will be called twice, once by the
+ # control channel, and once by handle_accept, and the two
+ # calls may come in either order. If handle_accept calls
+ # first, we don't want to call set_socket() on the data
+ # connection twice, so set self.accepted = None to keep a
+ # record that the data connection already has the socket.
+ self.accepted = None
+ self.control_channel.connectedPassive()
+ else:
+ self.client_dc = cdc
+
+
+class FTPDataChannel(DualModeChannel):
+ """Base class for FTP data connections.
+
+ Note that data channels are always in async mode.
+ """
+
+ def __init__ (self, control_channel):
+ self.control_channel = control_channel
+ self.reported = False
+ self.closed = False
+ DualModeChannel.__init__(self, None, None, control_channel.adj)
+
+ def connectPort(self, client_addr):
+ """Connect to a port on the client"""
+ self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+ #if bind_local_minus_one:
+ # self.bind(('', self.control_channel.server.port - 1))
+ try:
+ self.connect(client_addr)
+ except socket.error:
+ self.report('NO_DATA_CONN')
+
+ def abort(self):
+ """Abort the data connection without reporting."""
+ self.reported = True
+ if not self.closed:
+ self.closed = True
+ self.close()
+
+ def report(self, *reply_args):
+ """Reports the result of the data transfer."""
+ self.reported = True
+ if self.control_channel is not None:
+ self.control_channel.reply(*reply_args)
+
+ def reportDefault(self):
+ """Provide a default report on close."""
+ pass
+
+ def close(self):
+ """Notifies the control channel when the data connection closes."""
+ c = self.control_channel
+ try:
+ if c is not None and c.connected and not self.reported:
+ self.reportDefault()
+ finally:
+ self.control_channel = None
+ DualModeChannel.close(self)
+ if c is not None:
+ c.closedData()
+
+
+class STORChannel(FTPDataChannel):
+ """Channel for uploading one file from client to server"""
+
+ complete_transfer = 0
+ _fileno = None # provide a default for asyncore.dispatcher._fileno
+
+ def __init__ (self, control_channel, finish_args):
+ self.finish_args = finish_args
+ self.inbuf = OverflowableBuffer(control_channel.adj.inbuf_overflow)
+ FTPDataChannel.__init__(self, control_channel)
+ # Note that this channel starts in async mode.
+
+ def writable (self):
+ return 0
+
+ def handle_connect (self):
+ pass
+
+ def received (self, data):
+ if data:
+ self.inbuf.append(data)
+
+ def handle_close (self):
+ """Client closed, indicating EOF."""
+ c = self.control_channel
+ task = FinishSTORTask(c, self.inbuf, self.finish_args)
+ self.complete_transfer = 1
+ self.close()
+ c.queue_task(task)
+
+ def reportDefault(self):
+ if not self.complete_transfer:
+ self.report('TRANSFER_ABORTED')
+ # else the transfer completed and FinishSTORTask will
+ # provide a complete reply through finishSTOR().
+
+
+class FinishSTORTask(object):
+ """Calls control_channel.finishSTOR() in an application thread.
+
+ This task executes after the client has finished uploading.
+ """
+
+ implements(ITask)
+
+ def __init__(self, control_channel, inbuf, finish_args):
+ self.control_channel = control_channel
+ self.inbuf = inbuf
+ self.finish_args = finish_args
+
+ def service(self):
+ """Called to execute the task.
+ """
+ close_on_finish = 0
+ c = self.control_channel
+ try:
+ try:
+ c.finishSTOR(self.inbuf, self.finish_args)
+ except socket.error:
+ close_on_finish = 1
+ if c.adj.log_socket_errors:
+ raise
+ finally:
+ if close_on_finish:
+ c.close_when_done()
+
+ def cancel(self):
+ 'See ITask'
+ self.control_channel.close_when_done()
+
+ def defer(self):
+ 'See ITask'
+ pass
+
+
+class RETRChannel(FTPDataChannel):
+ """Channel for downloading one file from server to client
+
+ Also used for directory listings.
+ """
+
+ opened = 0
+ _fileno = None # provide a default for asyncore.dispatcher._fileno
+
+ def __init__ (self, control_channel, ok_reply_args):
+ self.ok_reply_args = ok_reply_args
+ FTPDataChannel.__init__(self, control_channel)
+
+ def _open(self):
+ """Signal the client to open the connection."""
+ self.opened = 1
+ self.control_channel.reply(*self.ok_reply_args)
+ self.control_channel.asyncConnectData(self)
+
+ def write(self, data):
+ if self.control_channel is None:
+ raise IOError('Client FTP connection closed')
+ if not self.opened:
+ self._open()
+ return FTPDataChannel.write(self, data)
+
+ def readable(self):
+ return not self.connected
+
+ def handle_read(self):
+ # This may be called upon making the connection.
+ try:
+ self.recv(1)
+ except socket.error:
+ # The connection failed.
+ self.report('NO_DATA_CONN')
+ self.close()
+
+ def handle_connect(self):
+ pass
+
+ def handle_comm_error(self):
+ self.report('TRANSFER_ABORTED')
+ self.close()
+
+ def reportDefault(self):
+ if not len(self.outbuf):
+ # All data transferred
+ if not self.opened:
+ # Zero-length file
+ self._open()
+ self.report('TRANS_SUCCESS')
+ else:
+ # Not all data transferred
+ self.report('TRANSFER_ABORTED')
+
+
+class ApplicationOutputStream(object):
+ """Provide stream output to RETRChannel.
+
+ Maps close() to close_when_done().
+ """
+
+ def __init__(self, retr_channel):
+ self.write = retr_channel.write
+ self.flush = retr_channel.flush
+ self.close = retr_channel.close_when_done
+
+
+class FTPServer(ServerBase):
+ """Generic FTP Server"""
+
+ channel_class = FTPServerChannel
+ SERVER_IDENT = 'zope.server.ftp'
+
+
+ def __init__(self, ip, port, fs_access, *args, **kw):
+
+ assert IFileSystemAccess.providedBy(fs_access)
+ self.fs_access = fs_access
+
+ super(FTPServer, self).__init__(ip, port, *args, **kw)
1  src/zope/server/ftp/tests/__init__.py
@@ -0,0 +1 @@
+# Make this directory a package.
308 src/zope/server/ftp/tests/demofs.py
@@ -0,0 +1,308 @@
+##############################################################################
+# Copyright (c) 2003 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+##############################################################################
+"""Demo file-system implementation, for testing
+"""
+import posixpath
+from zope.security.interfaces import Unauthorized
+from zope.server.interfaces.ftp import IFileSystem
+from zope.server.interfaces.ftp import IFileSystemAccess
+from zope.interface import implements
+
+execute = 1
+read = 2
+write = 4
+
+class File(object):
+ type = 'f'
+ modified=None
+
+ def __init__(self):
+ self.access = {'anonymous': read}
+
+ def accessable(self, user, access=read):
+ return (user == 'root'
+ or (self.access.get(user, 0) & access)
+ or (self.access.get('anonymous', 0) & access)
+ )
+
+ def grant(self, user, access):
+ self.access[user] = self.access.get(user, 0) | access
+
+ def revoke(self, user, access):
+ self.access[user] = self.access.get(user, 0) ^ access
+
+class Directory(File):
+
+ type = 'd'
+
+ def __init__(self):
+ super(Directory, self).__init__()
+ self.files = {}
+
+ def get(self, name, default=None):
+ return self.files.get(name, default)
+
+ def __getitem__(self, name):
+ return self.files[name]
+
+ def __setitem__(self, name, v):
+ self.files[name] = v
+
+ def __delitem__(self, name):
+ del self.files[name]
+
+ def __contains__(self, name):
+ return name in self.files
+
+ def __iter__(self):
+ return iter(self.files)
+
+class DemoFileSystem(object):
+ __doc__ = IFileSystem.__doc__
+
+ implements(IFileSystem)
+
+ File = File
+ Directory = Directory
+
+ def __init__(self, files, user=''):
+ self.files = files
+ self.user = user
+
+ def get(self, path, default=None):
+
+ while path.startswith('/'):
+ path = path[1:]
+
+ d = self.files
+ if path:
+ for name in path.split('/'):
+ if d.type is not 'd':
+ return default
+ if not d.accessable(self.user):
+ raise Unauthorized
+ d = d.get(name)
+ if d is None:
+ break
+
+ return d
+
+ def getany(self, path):
+ d = self.get(path)
+ if d is None:
+ raise OSError("No such file or directory:", path)
+ return d
+
+ def getdir(self, path):
+ d = self.getany(path)
+ if d.type != 'd':
+ raise OSError("Not a directory:", path)
+ return d
+
+ def getfile(self, path):
+ d = self.getany(path)
+ if d.type != 'f':
+ raise OSError("Not a file:", path)
+ return d
+
+ def getwdir(self, path):
+ d = self.getdir(path)
+ if not d.accessable(self.user, write):
+ raise OSError("Permission denied")
+ return d
+
+ def type(self, path):
+ "See zope.server.interfaces.ftp.IFileSystem"
+ f = self.get(path)
+ return getattr(f, 'type', None)
+
+ def names(self, path, filter=None):
+ "See zope.server.interfaces.ftp.IFileSystem"
+ f = list(self.getdir(path))
+ if filter is not None:
+ f = [name for name in f if filter(name)]
+
+ return f
+
+ def _lsinfo(self, name, file):
+ info = {
+ 'type': file.type,
+ 'name': name,
+ 'group_read': file.accessable(self.user, read),
+ 'group_write': file.accessable(self.user, write),
+ }
+ if file.type == 'f':
+ info['size'] = len(file.data)
+ if file.modified is not None:
+ info['mtime'] = file.modified
+
+ return info
+
+ def ls(self, path, filter=None):
+ "See zope.server.interfaces.ftp.IFileSystem"
+ f = self.getdir(path)
+ if filter is None:
+ return [self._lsinfo(name, f.files[name])
+ for name in f
+ ]
+
+ return [self._lsinfo(name, f.files[name])
+ for name in f
+ if filter(name)]
+
+ def readfile(self, path, outstream, start=0, end=None):
+ "See zope.server.interfaces.ftp.IFileSystem"
+ f = self.getfile(path)
+
+ data = f.data
+ if end is not None:
+ data = data[: