thomasvs - in flumotion/branches/platform-3: . flumotion/component/misc/httpfile flumotion/test

flumotion-commit at lists.fluendo.com flumotion-commit at lists.fluendo.com
Wed May 16 11:58:01 CEST 2007


Author: thomasvs
Date: Wed May 16 11:57:57 2007
New Revision: 4955

Added:
   flumotion/branches/platform-3/flumotion/test/test_component_httpserver.py
      - copied unchanged from r4879, flumotion/trunk/flumotion/test/test_component_httpserver.py
   flumotion/branches/platform-3/flumotion/test/test_misc_httpfile.py
      - copied, changed from r4829, flumotion/trunk/flumotion/test/test_misc_httpfile.py
Modified:
   flumotion/branches/platform-3/   (props changed)
   flumotion/branches/platform-3/ChangeLog
   flumotion/branches/platform-3/flumotion/component/misc/httpfile/file.py
   flumotion/branches/platform-3/flumotion/component/misc/httpfile/httpfile.py
   flumotion/branches/platform-3/flumotion/test/Makefile.am
Log:
Merged revisions 4828-4829,4836-4838,4840-4841,4843-4844,4846-4848,4852,4858,4863-4865,4870-4873,4877-4892,4894,4898,4903-4904,4911-4913 via svnmerge from 
https://svn.fluendo.com/svn/flumotion/trunk

........
  r4828 | thomasvs | 2007-04-25 00:06:21 +0200 (Wed, 25 Apr 2007) | 6 lines
  
  	* flumotion/test/Makefile.am:
  	* flumotion/test/test_misc_httpfile.py:
  	  Add a test before refactoring.
  	  Increases coverage for file.py from 19% to 79%.
........
  r4829 | thomasvs | 2007-04-25 00:20:44 +0200 (Wed, 25 Apr 2007) | 18 lines
  
  	* flumotion/component/misc/httpfile/file.py (File, File.__init__,
  	  File.render, File.terminateSimpleRequest, File.renderAuthenticated,
  	  File.createSimilarFile):
  	  Comment and document.
  	  Remove bogus pychecker inhibition.
  	  Privatize File._component.
  	  Instead of commenting badly named variables, use self-documenting
  	  names.
  	  Be a bit more strict about range headers we accept.
  	  Make the start/end logic shorter and easier to grok by adding
  	  first/last variables and simplifying.
  	* flumotion/test/test_misc_httpfile.py
  	  (TestTextFile.testWrongEmptyBytesRange,
  	  TestTextFile.testWrongNoRange, TestTextFile.testWrongTypeRange,
  	  TestTextFile.testRangeSet):
  	  Add two tests, fix one.
........
  r4877 | thomasvs | 2007-05-07 23:32:53 +0200 (Mon, 07 May 2007) | 27 lines
  
  	* 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 /
........
  r4878 | thomasvs | 2007-05-07 23:55:11 +0200 (Mon, 07 May 2007) | 10 lines
  
  	* flumotion/component/misc/httpfile/file.py (File.getChild):
  	* flumotion/component/misc/httpfile/httpfile.py
  	  (HTTPFileStreamer.do_setup, HTTPFileStreamer.do_start):
  	  We still need to distinguish between the root resource being the
  	  File resource directly, and a tree.  Apparently client.getPage()
  	  in the tests does not mimic correctly what something like wget would
  	  do, making it hard to test for the Root resource tree being set up
  	  correctly.
........
  r4879 | thomasvs | 2007-05-08 00:31:06 +0200 (Tue, 08 May 2007) | 20 lines
  
  	* flumotion/component/misc/httpfile/file.py (File, File.__init__,
  	  File.getChild, File.renderAuthenticated, MimedFileFactory,
  	  MimedFileFactory.__init__, MimedFileFactory.create, FLVFile,
  	  FLVFile.renderAuthenticated):
  	  Add a subclass for handling FLV files.
  	  Implement handling of the start= GET parameter, just like in
  	  Apache and lighthttpd.  Fixes #618.
  	  Add a MimedFileFactory that allows us to create even the root
  	  resource as a mime-type-dependent subclass.
  	* flumotion/component/misc/httpfile/httpfile.py (HTTPFileStreamer.init,
  	  HTTPFileStreamer.do_start):
  	* flumotion/test/test_misc_httpfile.py (TestDirectory,
  	  TestDirectory.setUp, TestDirectory.tearDown,
  	  TestDirectory.testGetChild, TestDirectory.testFLV,
  	  TestDirectory.finish, TestDirectory.testFLVStart,
  	  TestDirectory.finish, TestDirectory.testFLVStartZero,
  	  TestDirectory.finish):
  	  Add tests for this.
........
  r4898 | thomasvs | 2007-05-09 11:54:33 +0200 (Wed, 09 May 2007) | 4 lines
  
  	* flumotion/component/misc/httpfile/file.py (loadMimeTypes):
  	  Add .flv mime type in the code.
........
  r4913 | thomasvs | 2007-05-09 16:57:22 +0200 (Wed, 09 May 2007) | 14 lines
  
  	* flumotion/component/misc/httpfile/file.py (File.renderAuthenticated,
  	  File.do_prepareBody, MimedFileFactory, FLVFile,
  	  FLVFile.do_prepareBody, FileTransfer):
  	  Make sure start= requests on FLV files give me the right mime type
  	  by not writing before all headers are set.
  	  factor out an overridable do_prepareBody in which FLV and the like
  	  can do their manipulative trickery.
  	  Re-fixes #618.
  	* flumotion/test/test_misc_httpfile.py (TestTextFile.finishCallback,
  	  TestDirectory.finish, TestDirectory.finish, TestDirectory.finish,
  	  TestDirectory.testFLVRangeStart, TestDirectory.finish):
  	  Add tests.  Be more assertive.
........


Modified: flumotion/branches/platform-3/ChangeLog
==============================================================================
--- flumotion/branches/platform-3/ChangeLog	(original)
+++ flumotion/branches/platform-3/ChangeLog	Wed May 16 11:57:57 2007
@@ -1,3 +1,78 @@
+2007-05-16  Thomas Vander Stichele  <thomas at apestaart dot org>
+
+	merged from: TRUNK, 4828,4829,4877-4879,4898,4913
+
+	* flumotion/component/misc/httpfile/file.py (File.renderAuthenticated,
+	  File.do_prepareBody, MimedFileFactory, FLVFile,
+	  FLVFile.do_prepareBody, FileTransfer):
+	  Make sure start= requests on FLV files give me the right mime type
+	  by not writing before all headers are set.
+	  factor out an overridable do_prepareBody in which FLV and the like
+	  can do their manipulative trickery.
+	  Re-fixes #618.
+
+	* flumotion/component/misc/httpfile/file.py (loadMimeTypes):
+	  Add .flv mime type in the code.
+
+	* flumotion/component/misc/httpfile/file.py (File, File.__init__,
+	  File.getChild, File.renderAuthenticated, MimedFileFactory,
+	  MimedFileFactory.__init__, MimedFileFactory.create, FLVFile,
+	  FLVFile.renderAuthenticated):
+	  Add a subclass for handling FLV files.
+	  Implement handling of the start= GET parameter, just like in
+	  Apache and lighthttpd.  Fixes #618.
+	  Add a MimedFileFactory that allows us to create even the root
+	  resource as a mime-type-dependent subclass.
+
+	* 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/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 /
+	* flumotion/component/misc/httpfile/file.py (File, File.__init__,
+	  File.render, File.terminateSimpleRequest, File.renderAuthenticated,
+	  File.createSimilarFile):
+	  Comment and document.
+	  Remove bogus pychecker inhibition.
+	  Privatize File._component.
+	  Instead of commenting badly named variables, use self-documenting
+	  names.
+	  Be a bit more strict about range headers we accept.
+	  Make the start/end logic shorter and easier to grok by adding
+	  first/last variables and simplifying.
+	* flumotion/component/misc/httpfile/file.py (File.getChild):
+	* flumotion/component/misc/httpfile/httpfile.py
+	  (HTTPFileStreamer.do_setup, HTTPFileStreamer.do_start):
+	  We still need to distinguish between the root resource being the
+	  File resource directly, and a tree.  Apparently client.getPage()
+	  in the tests does not mimic correctly what something like wget would
+	  do, making it hard to test for the Root resource tree being set up
+	  correctly.
+
+	* flumotion/test/Makefile.am:
+	* flumotion/test/test_misc_httpfile.py:
+	  Add a test before refactoring.
+	  Increases coverage for file.py from 19% to 79%.
+	* 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.
+
 2007-04-04  Thomas Vander Stichele  <thomas at apestaart dot org>
 
 	merged from: TRUNK, 4743

Modified: flumotion/branches/platform-3/flumotion/component/misc/httpfile/file.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/misc/httpfile/file.py	(original)
+++ flumotion/branches/platform-3/flumotion/component/misc/httpfile/file.py	Wed May 16 11:57:57 2007
@@ -1,4 +1,4 @@
-# -*- Mode: Python -*-
+# -*- Mode: Python; test-case-name: flumotion.test.test_misc_httpfile -*-
 # vi:si:et:sw=4:sts=4:ts=4
 #
 # Flumotion - a streaming media server
@@ -18,39 +18,56 @@
 # See "LICENSE.Flumotion" in the source distribution for more information.
 
 # Headers in this file shall remain intact.
+
 import string
 import os
 
+from twisted.web import resource, server, http
+from twisted.web import error as weberror, static
+from twisted.internet import defer, reactor, error, abstract
+from twisted.python import filepath
+from twisted.cred import credentials
+
+from flumotion.configure import configure
 from flumotion.component import component
 from flumotion.common import log, messages, errors, netutils
 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, server, http
-from twisted.web import error as weberror
-from twisted.internet import defer, reactor, error, abstract
-from twisted.python import filepath
 from flumotion.twisted import fdserver
-from twisted.cred import credentials
 
-from twisted.web.static import loadMimeTypes, getTypeAndEncoding
+# add our own mime types to the ones parsed from /etc/mime.types
+def loadMimeTypes():
+    d = static.loadMimeTypes()
+    d['.flv'] = 'video/x-flv'
+    return d
 
-class File(resource.Resource, filepath.FilePath, log.Loggable):
-    __pychecker__ = 'no-objattrs'
+# this file is inspired by/adapted from twisted.web.static
 
+class File(resource.Resource, filepath.FilePath, log.Loggable):
     contentTypes = loadMimeTypes()
-
     defaultType = "application/octet-stream"
 
     childNotFound = weberror.NoResource("File not found.")
 
-    def __init__(self, path, component):
+    def __init__(self, path, component, mimeToResource=None):
+        """
+        @param component: L{flumotion.component.component.BaseComponent}
+        """
         resource.Resource.__init__(self)
         filepath.FilePath.__init__(self, path)
 
-        self.component = component
+        self._component = component
+        # mapping of mime type -> File subclass
+        self._mimeToResource = mimeToResource or {}
+        self._factory = MimedFileFactory(component, self._mimeToResource)
 
     def getChild(self, path, request):
+        self.log('getChild: self %r, path %r', self, path)
+        # we handle a request ending in '/' as well; this is how those come in
+        if path == '':
+            return self
+
         self.restat()
 
         if not self.isdir():
@@ -64,7 +81,7 @@
         if not fpath.exists():
             return self.childNotFound
 
-        return self.createSimilarFile(fpath.path)
+        return self._factory.create(fpath.path)
 
     def openForReading(self):
         """Open a file and return it."""
@@ -77,34 +94,38 @@
         return self.getsize()
 
     def render(self, request):
+        self.debug('render request %r' % request)
         def terminateSimpleRequest(res, request):
             if res != server.NOT_DONE_YET:
                 request.finish()
 
-        d = self.component.startAuthentication(request)
+        d = self._component.startAuthentication(request)
         d.addCallback(self.renderAuthenticated, request)
         d.addCallback(terminateSimpleRequest, request)
 
         return server.NOT_DONE_YET
 
-    def renderAuthenticated(self, res, request):
-        """ 
-        Now that we're authenticated (or authentication wasn't requested), 
-        write the file (or appropriate other response) to the client.
-        We override static.File to implement Range requests, and to get access
-        to the transfer object to abort it later; the bulk of this is a direct
-        copy, though.
-        """
+    def renderAuthenticated(self, _, request):
+        # Now that we're authenticated (or authentication wasn't requested), 
+        # write the file (or appropriate other response) to the client.
+        # We override static.File to implement Range requests, and to get access
+        # to the transfer object to abort it later; the bulk of this is a direct
+        # copy of static.File.render, though.
+        # 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)
+        contentType = self.contentTypes.get(ext, self.defaultType)
 
         if not self.exists():
-            self.debug("Couldn't find resource %s", self.basename())
+            self.debug("Couldn't find resource %s", self.path)
             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...        
@@ -112,12 +133,14 @@
         # that the client must not issue further requests. 
         # We do this because future requests on this server might actually need
         # to go to a different process (because of the porter)
+        request.setHeader('Server', 'Flumotion/%s' % configure.version)
         request.setHeader('Connection', 'close')
         # We can do range requests, in bytes.
         request.setHeader('Accept-Ranges', 'bytes')
 
-        if type:
-            request.setHeader('content-type', type)
+        if contentType:
+            self.debug('content type %r' % contentType)
+            request.setHeader('content-type', contentType)
 
         try:
             f = self.openForReading()
@@ -131,60 +154,137 @@
         if request.setLastModified(self.getmtime()) is http.CACHED:
             return ''
 
-        tsize = fsize = size = self.getFileSize()
+        fileSize = self.getFileSize()
+        # first and last byte offset we will write
+        first = 0
+        last = fileSize - 1
+
         range = request.getHeader('range')
-        start = 0
         if range is not None:
-            # TODO: Add a unit test - or several - for this stuff.
-            # We have a partial-data request...
-            # Some variables... we start at byte offset 'start', end at byte 
-            # offset 'end'. fsize is the number of bytes we're sending. tsize 
-            # is the total size of the file. 'size' is the byte offset we will
-            # stop at, plus 1.
-            bytesrange = string.split(range, '=')
-            if len(bytesrange) != 2:
+            # We have a partial data request.
+            # for interpretation of range, see RFC 2068 14.36
+            # examples: bytes=500-999; bytes=-500 (suffix mode; last 500)
+            self.log('range request, %r', range)
+            rangeKeyValue = string.split(range, '=')
+            if len(rangeKeyValue) != 2:
                 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
                 return ''
 
-            start, end = string.split(bytesrange[1], '-', 1)
+            if rangeKeyValue[0] != 'bytes':
+                request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
+                return ''
+
+            # ignore a set of range requests for now, only take the first
+            ranges = rangeKeyValue[1].split(',')[0]
+            l = ranges.split('-')
+            if len(l) != 2:
+                request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
+                return ''
+
+            start, end = l
+
             if start:
-                start = int(start)
-                f.seek(start)
+                # byte-range-spec
+                first = int(start)
                 if end:
-                    end = int(end)
-                else:
-                    end = size - 1
-                fsize = end - start + 1
+                    last = int(end)
             elif end:
-                lastbytes = int(end)
-                if size < lastbytes:
-                    lastbytes = size
-                start = size - lastbytes
-                f.seek(start)
-                fsize = lastbytes
-                end = size - 1
+                # suffix-byte-range-spec
+                count = int(end)
+                # we can't serve more than there are in the file
+                if count > fileSize:
+                    count = fileSize
+                first = fileSize - count
             else:
+                # need at least start or end
                 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
                 return ''
-            size = end + 1
 
+            # FIXME: is it still partial if the request was for the complete
+            # file ? Couldn't find a conclusive answer in the spec.
             request.setResponseCode(http.PARTIAL_CONTENT)
             request.setHeader('Content-Range', "bytes %d-%d/%d" % 
-                (start, end, tsize))
-
-        request.setHeader("Content-Length", str(fsize))
+                (first, last, fileSize))
+            # Start sending from the requested position in the file
+            if first:
+                f.seek(first)
+          
+        self.do_prepareBody(request, f, first, last)
 
         if request.method == 'HEAD':
-             return ''
-           
-        request._transfer = FileTransfer(f, size, request)
+            return ''
+
+        request._transfer = FileTransfer(f, last + 1, request)
 
         return server.NOT_DONE_YET
 
-    def createSimilarFile(self, path):
-        self.debug("createSimilarFile at %r", path)
-        f = self.__class__(path, self.component)
-        return f
+    def do_prepareBody(self, request, f, first, last):
+        """
+        I am called before the body of the response gets written,
+        and after generic header setting has been done.
+
+        I set Content-Length.
+
+        Override me to send additional headers, or to prefix the body
+        with data headers.
+        """
+        request.setHeader("Content-Length", str(last - first + 1))
+
+class MimedFileFactory(log.Loggable):
+    """
+    I create File subclasses based on the mime type of the given path.
+    """
+    contentTypes = loadMimeTypes()
+    defaultType = "application/octet-stream"
+
+    def __init__(self, component, mimeToResource=None):
+        self._component = component
+        self._mimeToResource = mimeToResource or {}
+
+    def create(self, path):
+        """
+        Creates and returns an instance of a File subclass based on the mime
+        type/extension of the given path.
+        """
+
+        self.debug("createMimedFile at %r", path)
+        ext = os.path.splitext(path)[1].lower()
+        mimeType = self.contentTypes.get(ext, self.defaultType)
+        klazz = self._mimeToResource.get(mimeType, File)
+        self.debug("mimetype %s, class %r" % (mimeType, klazz))
+        return klazz(path, self._component, mimeToResource=self._mimeToResource)
+
+class FLVFile(File):
+    """
+    I am a File resource for FLV files.
+    I can handle requests with a 'start' GET parameter.
+    This parameter represents the byte offset from where to start.
+    If it is non-zero, I will output an FLV header so the result is
+    playable.
+    """
+    header = 'FLV\x01\x01\000\000\000\x09\000\000\000\x09'
+
+    def do_prepareBody(self, request, f, first, last):
+        self.log('do_prepareBody for FLV')
+        length = last - first + 1
+
+        # if there is a non-zero start get parameter, prefix the body with
+        # our FLV header
+        # each value is a list
+        start = int(request.args.get('start', ['0'])[0])
+        # range request takes precedence over our start parsing
+        if first == 0 and start:
+            self.debug('start %d passed, seeking', start)
+            f.seek(start)
+            length = last - start + 1 + len(self.header)
+
+        request.setHeader("Content-Length", str(length))
+
+        if request.method == 'HEAD':
+            return ''
+
+        if first == 0 and start:
+            request.write(self.header)
 
 class FileTransfer:
     """

Modified: flumotion/branches/platform-3/flumotion/component/misc/httpfile/httpfile.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/misc/httpfile/httpfile.py	(original)
+++ flumotion/branches/platform-3/flumotion/component/misc/httpfile/httpfile.py	Wed May 16 11:57:57 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,17 +147,27 @@
 
         self._pbclient = None
 
+        self._twistedPort = None
+        self._timeoutRequestsCallLater = None
+
+        # FIXME: maybe we want to allow the configuration to specify
+        # additional mime -> File class mapping ?
+        self._mimeToResource = {
+            'video/x-flv': file.FLVFile,
+        }
+
         # 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']
 
-        mountPoint = props.get('mount-point', '')
+        # always make sure the mount point starts with /
+        mountPoint = props.get('mount-point', '/')
         if not mountPoint.startswith('/'):
             mountPoint = '/' + mountPoint
         self.mountPoint = mountPoint
@@ -171,8 +183,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 +194,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 +238,33 @@
                 "Can't specify porter details in master mode")
 
     def do_start(self, *args, **kwargs):
-        #root = HTTPRoot()
-        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
-        for child in children[:-1]:
-            res = resource.Resource()
-            current_resource.putChild(child, res)
-            current_resource = res
-        fileResource = file.File(self.filePath, self)
-        self.debug("Putting File resource at %r", children[-1:][0])
-        current_resource.putChild(children[-1:][0], fileResource)
+        self.debug('Starting with mount point "%s"' % self.mountPoint)
+        factory = file.MimedFileFactory(self,
+            mimeToResource=self._mimeToResource)
+        if self.mountPoint == '/':
+            self.debug('mount point / - create File resource as root')
+            # directly create a File resource for the path
+            root = factory.create(self.filePath)
+        else:
+            # split path on / and add iteratively twisted.web resources
+            # Asking for '' or '/' will retrieve the root Resource's '' child,
+            # so the split on / returning a first list value '' is correct
+            self.debug('mount point %s - creating root Resource and children',
+                self.mountPoint)
+            root = resource.Resource()
+            children = string.split(self.mountPoint[1:], '/')
+            parent = root
+            for child in children[:-1]:
+                res = resource.Resource()
+                self.debug("Putting Resource at %s", child)
+                parent.putChild(child, res)
+                parent = res
+            fileResource = factory.create(self.filePath)
+            self.debug("Putting resource %r at %r", fileResource, children[-1])
+            parent.putChild(children[-1], 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 +286,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 +327,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 +362,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 +376,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 +397,7 @@
         else:
             return {
                 'protocol': 'HTTP',
-                'description': self.description,
+                'description': self._description,
                 'url' : self.getUrl()
                 }
 
@@ -403,7 +431,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/branches/platform-3/flumotion/test/Makefile.am
==============================================================================
--- flumotion/branches/platform-3/flumotion/test/Makefile.am	(original)
+++ flumotion/branches/platform-3/flumotion/test/Makefile.am	Wed May 16 11:57:57 2007
@@ -27,6 +27,7 @@
 	test_common_pygobject.py 	\
 	test_component.py 		\
 	test_component_init.py 		\
+	test_component_httpserver.py 	\
 	test_component_httpstreamer.py 	\
 	test_config.py 			\
 	test_credentials.py 		\
@@ -45,6 +46,7 @@
 	test_manager_depgraph.py	\
 	test_manager_manager.py 	\
 	test_manager_worker.py		\
+	test_misc_httpfile.py		\
 	test_parts.py	 		\
 	test_pb.py	 		\
 	test_pbstream.py		\

Copied: flumotion/branches/platform-3/flumotion/test/test_misc_httpfile.py (from r4829, flumotion/trunk/flumotion/test/test_misc_httpfile.py)
==============================================================================
--- flumotion/trunk/flumotion/test/test_misc_httpfile.py	(original)
+++ flumotion/branches/platform-3/flumotion/test/test_misc_httpfile.py	Wed May 16 11:57:57 2007
@@ -82,6 +82,8 @@
         self.assertEquals(request.data, data)
         self.assertEquals(int(request.getHeader('Content-Length') or '0'),
             length)
+        self.assertEquals(request.getHeader('content-type'),
+            'application/octet-stream')
 
     def finishPartialCallback(self, result, request, data, start, end):
         self.finishCallback(result, request, http.PARTIAL_CONTENT, data)
@@ -168,7 +170,6 @@
             'a text file', 0, 10)
         return fr.finishDeferred
 
-
     def testRangeHead(self):
         fr = FakeRequest(method='HEAD', headers={'range': 'bytes=2-5'})
         self.assertEquals(self.resource.render(fr), server.NOT_DONE_YET)
@@ -176,4 +177,70 @@
             http.PARTIAL_CONTENT, '', 4)
         return fr.finishDeferred
 
+class TestDirectory(unittest.TestCase):
+    def setUp(self):
+        self.path = tempfile.mkdtemp()
+        h = open(os.path.join(self.path, 'test.flv'), 'w')
+        h.write('a fake FLV file')
+        h.close()
+        self.component = FakeComponent()
+        # a directory resource
+        self.resource = file.File(self.path, self.component,
+            { 'video/x-flv': file.FLVFile } )
+
+    def tearDown(self):
+        os.system('rm -r %s' % self.path)
 
+    def testGetChild(self):
+        fr = FakeRequest()
+        r = self.resource.getChild('test.flv', fr)
+        self.assertEquals(r.__class__, file.FLVFile)
+
+    def testFLV(self):
+        fr = FakeRequest()
+        self.assertEquals(self.resource.getChild('test.flv', fr).render(fr),
+            server.NOT_DONE_YET)
+        def finish(result):
+            self.assertEquals(fr.getHeader('content-type'), 'video/x-flv')
+            self.assertEquals(fr.data, 'a fake FLV file')
+        fr.finishDeferred.addCallback(finish)
+
+        return fr.finishDeferred
+
+    def testFLVStart(self):
+        fr = FakeRequest(args={'start': [2]})
+        self.assertEquals(self.resource.getChild('test.flv', fr).render(fr),
+            server.NOT_DONE_YET)
+        def finish(result):
+            self.assertEquals(fr.getHeader('content-type'), 'video/x-flv')
+            expected = file.FLVFile.header + 'fake FLV file'
+            self.assertEquals(fr.data, expected)
+            self.assertEquals(fr.getHeader('Content-Length'),
+                str(len(expected)))
+        fr.finishDeferred.addCallback(finish)
+
+        return fr.finishDeferred
+        
+    def testFLVStartZero(self):
+        fr = FakeRequest(args={'start': [0]})
+        self.assertEquals(self.resource.getChild('test.flv', fr).render(fr),
+            server.NOT_DONE_YET)
+        def finish(result):
+            self.assertEquals(fr.getHeader('content-type'), 'video/x-flv')
+            self.assertEquals(fr.data, 'a fake FLV file')
+        fr.finishDeferred.addCallback(finish)
+        return fr.finishDeferred
+
+    def testFLVRangeStart(self):
+        # range should take precedence over start parameter
+        fr = FakeRequest(headers={'range': 'bytes=7-'}, args={'start': [2]})
+        self.assertEquals(self.resource.getChild('test.flv', fr).render(fr),
+            server.NOT_DONE_YET)
+        def finish(result):
+            self.assertEquals(fr.getHeader('content-type'), 'video/x-flv')
+            expected = 'FLV file'
+            self.assertEquals(fr.data, expected)
+            self.assertEquals(fr.getHeader('Content-Length'),
+                str(len(expected)))
+        fr.finishDeferred.addCallback(finish)
+        return fr.finishDeferred


More information about the flumotion-commit mailing list