thomasvs - in flumotion/trunk: . flumotion/component/misc/httpfile flumotion/test

flumotion-commit at lists.fluendo.com flumotion-commit at lists.fluendo.com
Mon May 7 23:33:07 CEST 2007


Author: thomasvs
Date: Mon May  7 23:32:53 2007
New Revision: 4877

Added:
   flumotion/trunk/flumotion/test/test_component_httpserver.py
Modified:
   flumotion/trunk/ChangeLog
   flumotion/trunk/flumotion/component/misc/httpfile/file.py
   flumotion/trunk/flumotion/component/misc/httpfile/httpfile.py
   flumotion/trunk/flumotion/test/Makefile.am
Log:
	* flumotion/component/misc/httpfile/httpfile.py (HTTPFileStreamer.init,
	  HTTPFileStreamer.getDescription, HTTPFileStreamer.do_setup,
	  HTTPFileStreamer.do_stop, HTTPFileStreamer.do_start,
	  HTTPFileStreamer.requestFinished, HTTPFileStreamer.getStreamData):
	  Rearrange imports.
	  Privatize some variables.
	  Store some variables for resources we need to clean up.
	  Set self.port from what we're actually listening to, so we can
	  listen to port 0 then figure out which port it chose.
	  Make sure we can work without any loggers.
	  Fix handling of empty and / mount points, as well as getting
	  empty or / resources - fixes #567
	* flumotion/test/Makefile.am:
	* flumotion/test/test_component_httpserver.py (MountTest,
	  MountTest.setUp, MountTest.tearDown, MountTest.start,
	  MountTest.getURL, MountTest.testDirMountEmpty,
	  MountTest.testDirMountRoot, MountTest.testDirMountOnDemand,
	  MountTest.testFileMountEmpty, MountTest.testFileMountOnDemand):
	  Add tests for http-server component with various mount points
	  and serving either a directory or a file.
	* flumotion/component/misc/httpfile/file.py (File.getChild,
	  File.renderAuthenticated):
	  Restat the file on every request, to make sure we handle changed
	  lengths of files.
	  Handle requests ending with /



Modified: flumotion/trunk/ChangeLog
==============================================================================
--- flumotion/trunk/ChangeLog	(original)
+++ flumotion/trunk/ChangeLog	Mon May  7 23:32:53 2007
@@ -1,3 +1,31 @@
+2007-05-07  Thomas Vander Stichele  <thomas at apestaart dot org>
+
+	* flumotion/component/misc/httpfile/httpfile.py (HTTPFileStreamer.init,
+	  HTTPFileStreamer.getDescription, HTTPFileStreamer.do_setup,
+	  HTTPFileStreamer.do_stop, HTTPFileStreamer.do_start,
+	  HTTPFileStreamer.requestFinished, HTTPFileStreamer.getStreamData):
+	  Rearrange imports.
+	  Privatize some variables.
+	  Store some variables for resources we need to clean up.
+	  Set self.port from what we're actually listening to, so we can
+	  listen to port 0 then figure out which port it chose.
+	  Make sure we can work without any loggers.
+	  Fix handling of empty and / mount points, as well as getting
+	  empty or / resources - fixes #567
+	* flumotion/test/Makefile.am:
+	* flumotion/test/test_component_httpserver.py (MountTest,
+	  MountTest.setUp, MountTest.tearDown, MountTest.start,
+	  MountTest.getURL, MountTest.testDirMountEmpty,
+	  MountTest.testDirMountRoot, MountTest.testDirMountOnDemand,
+	  MountTest.testFileMountEmpty, MountTest.testFileMountOnDemand):
+	  Add tests for http-server component with various mount points
+	  and serving either a directory or a file.
+	* flumotion/component/misc/httpfile/file.py (File.getChild,
+	  File.renderAuthenticated):
+	  Restat the file on every request, to make sure we handle changed
+	  lengths of files.
+	  Handle requests ending with /
+
 2007-05-07  Andy Wingo  <wingo at pobox.com>
 
 	* flumotion/worker/medium.py (WorkerMedium.remote_killJob): New

Modified: flumotion/trunk/flumotion/component/misc/httpfile/file.py
==============================================================================
--- flumotion/trunk/flumotion/component/misc/httpfile/file.py	(original)
+++ flumotion/trunk/flumotion/component/misc/httpfile/file.py	Mon May  7 23:32:53 2007
@@ -55,6 +55,10 @@
         self._component = component
 
     def getChild(self, path, request):
+        # we handle a request ending in '/' as well; this is how those come in
+        if path == '':
+            return self
+
         self.restat()
 
         if not self.isdir():
@@ -101,6 +105,9 @@
         # self.restat()
         self.debug('renderAuthenticated request %r' % request)
 
+        # make sure we notice changes in the file
+        self.restat()
+
         ext = os.path.splitext(self.basename())[1].lower()
         type = self.contentTypes.get(ext, self.defaultType)
 
@@ -109,6 +116,7 @@
             return self.childNotFound.render(request)
 
         if self.isdir():
+            self.debug("%s is a directory, can't be GET", self.path)
             return self.childNotFound.render(request)
 
         # Different headers not normally set in static.File...        

Modified: flumotion/trunk/flumotion/component/misc/httpfile/httpfile.py
==============================================================================
--- flumotion/trunk/flumotion/component/misc/httpfile/httpfile.py	(original)
+++ flumotion/trunk/flumotion/component/misc/httpfile/httpfile.py	Mon May  7 23:32:53 2007
@@ -1,4 +1,4 @@
-# -*- Mode: Python -*-
+# -*- Mode: Python; test-case-name: flumotion.test.test_component_httpserver -*-
 # vi:si:et:sw=4:sts=4:ts=4
 #
 # Flumotion - a streaming media server
@@ -22,17 +22,19 @@
 import time
 import string
 
+from twisted.web import resource, static, server, http
+from twisted.web import error as weberror
+from twisted.internet import defer, reactor, error
+from twisted.cred import credentials
+
 from flumotion.component import component
 from flumotion.common import log, messages, errors, netutils, interfaces
 from flumotion.component.component import moods
 from flumotion.component.misc.porter import porterclient
 from flumotion.component.base import http as httpbase
-from twisted.web import resource, static, server, http
-from twisted.web import error as weberror
-from twisted.internet import defer, reactor, error
+
 from flumotion.twisted import fdserver
 from flumotion.twisted.compat import implements
-from twisted.cred import credentials
 
 from flumotion.component.misc.httpfile import file
 
@@ -134,10 +136,10 @@
         self.type = None
         self.port = None
         self.hostname = None
-        self.loggers = []
-        self.logfilter = None
+        self._loggers = []
+        self._logfilter = None
 
-        self.description = 'On-Demand Flumotion Stream',
+        self._description = 'On-Demand Flumotion Stream',
 
         self._singleFile = False
         self._connected_clients = []
@@ -145,16 +147,20 @@
 
         self._pbclient = None
 
+        self._twistedPort = None
+        self._timeoutRequestsCallLater = None
+
         # store number of connected clients
         self.uiState.addKey("connected-clients", 0)
         self.uiState.addKey("bytes-transferred", 0)
 
     def getDescription(self):
-        return self.description
+        return self._description
 
     def do_setup(self):
         props = self.config['properties']
 
+        # always make sure the mount point starts with /
         mountPoint = props.get('mount-point', '')
         if not mountPoint.startswith('/'):
             mountPoint = '/' + mountPoint
@@ -171,8 +177,8 @@
             self._porterPath = props['porter-socket-path']
             self._porterUsername = props['porter-username']
             self._porterPassword = props['porter-password']
-        self.loggers = \
-            self.plugs['flumotion.component.plugs.loggers.Logger']
+        self._loggers = \
+            self.plugs.get('flumotion.component.plugs.loggers.Logger', [])
 
         if 'bouncer' in props:
             self.setBouncerName(props['bouncer'])
@@ -182,9 +188,15 @@
             filter = http.LogFilter()
             for f in props['ip-filter']:
                 filter.addIPFilter(f)
-            self.logfilter = filter
-        
+            self._logfilter = filter
+
     def do_stop(self):
+        if self._timeoutRequestsCallLater:
+            self._timeoutRequestsCallLater.cancel()
+            self._timeoutRequestsCallLater = None
+        if self._twistedPort:
+            self._twistedPort.stopListening()
+
         if self.type == 'slave' and self._pbclient:
             return self._pbclient.deregisterPath(self.mountPoint)
 
@@ -220,22 +232,24 @@
                 "Can't specify porter details in master mode")
 
     def do_start(self, *args, **kwargs):
-        #root = HTTPRoot()
+        self.debug('Starting with mount point "%s"' % self.mountPoint)
         root = resource.Resource()
-        # TwistedWeb wants the child path to not include the leading /
-        mount = self.mountPoint[1:]
         # split path on / and add iteratively twisted.web resources
-        children = string.split(mount, '/')
-        current_resource = root
+        # Asking for '' or '/' will retrieve the root Resource's '' child,
+        # so the split on / returning a first list value '' is correct
+        children = string.split(self.mountPoint, '/')
+        parent = root
         for child in children[:-1]:
             res = resource.Resource()
-            current_resource.putChild(child, res)
-            current_resource = res
+            self.debug("Putting Resource at %s", child)
+            parent.putChild(child, res)
+            parent = res
         fileResource = file.File(self.filePath, self)
         self.debug("Putting File resource at %r", children[-1:][0])
-        current_resource.putChild(children[-1:][0], fileResource)
+        parent.putChild(children[-1:][0], fileResource)
 
-        reactor.callLater(self.REQUEST_TIMEOUT, self._timeoutRequests)
+        self._timeoutRequestsCallLater = reactor.callLater(
+            self.REQUEST_TIMEOUT, self._timeoutRequests)
 
         d = defer.Deferred()
         if self.type == 'slave':
@@ -257,10 +271,14 @@
         else:
             # File Streamer is standalone.
             try:
-                self.debug('Listening on %s' % self.port)
+                self.debug('Going to listen on port %d' % self.port)
                 iface = ""
-                reactor.listenTCP(self.port, Site(root, self),
-                    interface=iface)
+                # we could be listening on port 0, in which case we need
+                # to figure out the actual port we listen on
+                self._twistedPort = reactor.listenTCP(self.port,
+                    Site(root, self), interface=iface)
+                self.port = self._twistedPort.getHost().port
+                self.debug('Listening on port %d' % self.port)
             except error.CannotListenError:
                 t = 'Port %d is not available.' % self.port
                 self.warning(t)
@@ -294,11 +312,6 @@
                     msg = 'slave mode, missing required property porter-%s' % k
                     return defer.fail(errors.ConfigError(msg))
 
-        if props.get('mount-point', None) is not None: 
-            if props['mount-point'] == '/':
-                return defer.fail(errors.ConfigError(
-                    "A mount-point of / is not supported in this release"))
-
             path = props.get('path', None) 
             if path is None: 
                 msg = "missing required property 'path'"
@@ -334,7 +347,7 @@
         headers = request.getAllHeaders()
 
         ip = request.getClientIP()
-        if not self.logfilter or not self.logfilter.isInRange(ip):
+        if not self._logfilter or not self._logfilter.isInRange(ip):
             args = {'ip': ip,
                     'time': time.gmtime(),
                     'method': request.method,
@@ -348,7 +361,7 @@
                     'user-agent': headers.get('user-agent', None),
                     'time-connected': timeConnected}
 
-            for logger in self.loggers:
+            for logger in self._loggers:
                 logger.event('http_session_completed', args)
 
         self._connected_clients.remove(request)
@@ -369,7 +382,7 @@
         else:
             return {
                 'protocol': 'HTTP',
-                'description': self.description,
+                'description': self._description,
                 'url' : self.getUrl()
                 }
 
@@ -403,7 +416,6 @@
         """
         Close the logfile, then reopen using the previous logfilename
         """
-        for logger in self.loggers:
+        for logger in self._loggers:
             self.debug('rotating logger %r' % logger)
             logger.rotate()
-

Modified: flumotion/trunk/flumotion/test/Makefile.am
==============================================================================
--- flumotion/trunk/flumotion/test/Makefile.am	(original)
+++ flumotion/trunk/flumotion/test/Makefile.am	Mon May  7 23:32:53 2007
@@ -29,6 +29,7 @@
 	test_component.py 		\
 	test_component_init.py 		\
 	test_component_feed.py		\
+	test_component_httpserver.py 	\
 	test_component_httpstreamer.py 	\
 	test_config.py 			\
 	test_credentials.py 		\

Added: flumotion/trunk/flumotion/test/test_component_httpserver.py
==============================================================================
--- (empty file)
+++ flumotion/trunk/flumotion/test/test_component_httpserver.py	Mon May  7 23:32:53 2007
@@ -0,0 +1,164 @@
+# -*- Mode: Python; test-case-name: flumotion.test.test_component_httpserver -*-
+# vi:si:et:sw=4:sts=4:ts=4
+#
+# Flumotion - a streaming media server
+# Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com).
+# All rights reserved.
+
+# This file may be distributed and/or modified under the terms of
+# the GNU General Public License version 2 as published by
+# the Free Software Foundation.
+# This file is distributed without any warranty; without even the implied
+# warranty of merchantability or fitness for a particular purpose.
+# See "LICENSE.GPL" in the source distribution for more information.
+
+# Licensees having purchased or holding a valid Flumotion Advanced
+# Streaming Server license may use this file in accordance with the
+# Flumotion Advanced Streaming Server Commercial License Agreement.
+# See "LICENSE.Flumotion" in the source distribution for more information.
+
+# Headers in this file shall remain intact.
+
+from twisted.trial import unittest
+
+import common
+
+import os
+import tempfile
+
+from twisted.python import failure
+from twisted.internet import defer
+from twisted.web import client, error
+
+from flumotion.common import log
+from flumotion.component.misc.httpfile import httpfile
+
+class MountTest(log.Loggable, unittest.TestCase):
+    def setUp(self):
+        self.path = tempfile.mkdtemp(suffix=".flumotion.test")
+        A = os.path.join(self.path, 'A')
+        open(A, "w").write('test file A')
+        B = os.path.join(self.path, 'B')
+        os.mkdir(B)
+        C = os.path.join(self.path, 'B', 'C')
+        open(C, "w").write('test file C')
+        
+        self.component = httpfile.HTTPFileStreamer()
+
+    def tearDown(self):
+        self.component.stop()
+        os.system('rm -r %s' % self.path)
+
+    def start(self, properties):
+        # start the component with the given properties
+        config = {
+            'feed': [],
+            'name': 'http-server',
+            'parent': 'default',
+            'avatarId': '/default/http-server',
+            'clock-master': None,
+            'type': 'http-server',
+            'plugs': {},
+            'properties': properties,
+        }
+
+        d = self.component.setup(config)
+        d.addCallback(lambda _: self.component.start())
+        return d
+
+    def getURL(self, path):
+        # path should start with /
+        return 'http://localhost:%d%s' % (self.component.port, path)
+
+    def testDirMountEmpty(self):
+        properties = {
+            u'mount-point': '',
+            u'path': self.path,
+            u'port': 0,
+        }
+        self.start(properties)
+
+        d = client.getPage(self.getURL('/A'))
+        d.addCallback(lambda r: self.assertEquals(r, 'test file A'))
+        d.addCallback(lambda r: client.getPage(self.getURL('/B/C')))
+        d.addCallback(lambda r: self.assertEquals(r, 'test file C'))
+        # getting a non-existing resource should give web.error.Error
+        d.addCallback(lambda r: client.getPage(self.getURL('/B/D')))
+        d.addErrback(lambda f: f.trap(error.Error))
+        return d
+
+    def testDirMountRoot(self):
+        properties = {
+            u'mount-point': '/',
+            u'path': self.path,
+            u'port': 0,
+        }
+        self.start(properties)
+
+        d = client.getPage(self.getURL('/A'))
+        d.addCallback(lambda r: self.assertEquals(r, 'test file A'))
+        d.addCallback(lambda r: client.getPage(self.getURL('/B/C')))
+        d.addCallback(lambda r: self.assertEquals(r, 'test file C'))
+        # getting a non-existing resource should give web.error.Error
+        d.addCallback(lambda r: client.getPage(self.getURL('/B/D')))
+        d.addErrback(lambda f: f.trap(error.Error))
+        return d
+
+    def testDirMountOnDemand(self):
+        properties = {
+            u'mount-point': '/ondemand',
+            u'path': self.path,
+            u'port': 0,
+        }
+        self.start(properties)
+
+        d = client.getPage(self.getURL('/ondemand/A'))
+        d.addCallback(lambda r: self.assertEquals(r, 'test file A'))
+        d.addCallback(lambda r: client.getPage(self.getURL('/ondemand/B/C')))
+        d.addCallback(lambda r: self.assertEquals(r, 'test file C'))
+        # getting a non-existing resource should give web.error.Error
+        d.addCallback(lambda r: client.getPage(self.getURL('/A')))
+        d.addErrback(lambda f: f.trap(error.Error))
+        d.addCallback(lambda r: client.getPage(self.getURL('/ondemand/B/D')))
+        d.addErrback(lambda f: f.trap(error.Error))
+        return d
+  
+    def testFileMountEmpty(self):
+        properties = {
+            u'mount-point': '',
+            u'path': os.path.join(self.path, 'A'),
+            u'port': 0,
+        }
+        self.start(properties)
+
+        d = defer.Deferred()
+        # FIXME: what if just the server URL is requested ?
+        #d.addCallback(lambda r: client.getPage(self.getURL('')))
+        #d.addCallback(lambda r: self.assertEquals(r, 'test file A'))
+        d.addCallback(lambda r: client.getPage(self.getURL('/')))
+        d.addCallback(lambda r: self.assertEquals(r, 'test file A'))
+        # getting a non-existing resource should give web.error.Error
+        d.addCallback(lambda r: client.getPage(self.getURL('/B/D')))
+        d.addErrback(lambda f: f.trap(error.Error))
+        d.callback(None)
+        return d
+
+    def testFileMountOnDemand(self):
+        properties = {
+            u'mount-point': '/ondemand',
+            u'path': os.path.join(self.path, 'A'),
+            u'port': 0,
+        }
+        self.start(properties)
+
+        d = client.getPage(self.getURL('/ondemand'))
+        d.addCallback(lambda r: self.assertEquals(r, 'test file A'))
+        # getting a non-existing resource should give web.error.Error
+        d.addCallback(lambda r: client.getPage(self.getURL('/A')))
+        d.addErrback(lambda f: f.trap(error.Error))
+        d.addCallback(lambda r: client.getPage(self.getURL('/ondemand/B/D')))
+        d.addErrback(lambda f: f.trap(error.Error))
+        return d
+  
+if __name__ == '__main__':
+    unittest.main()


More information about the flumotion-commit mailing list