Package flumotion :: Package component :: Package misc :: Package httpserver :: Module httpfile
[hide private]

Source Code for Module flumotion.component.misc.httpserver.httpfile

  1  # -*- Mode: Python; test-case-name: flumotion.test.test_misc_httpserver -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3  # 
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com). 
  6  # All rights reserved. 
  7   
  8  # This file may be distributed and/or modified under the terms of 
  9  # the GNU General Public License version 2 as published by 
 10  # the Free Software Foundation. 
 11  # This file is distributed without any warranty; without even the implied 
 12  # warranty of merchantability or fitness for a particular purpose. 
 13  # See "LICENSE.GPL" in the source distribution for more information. 
 14   
 15  # Licensees having purchased or holding a valid Flumotion Advanced 
 16  # Streaming Server license may use this file in accordance with the 
 17  # Flumotion Advanced Streaming Server Commercial License Agreement. 
 18  # See "LICENSE.Flumotion" in the source distribution for more information. 
 19   
 20  # Headers in this file shall remain intact. 
 21   
 22  import string 
 23  import os 
 24   
 25  from twisted.web import resource, server, http 
 26  from twisted.web import error as weberror, static 
 27  from twisted.internet import defer, reactor, error, abstract 
 28  from twisted.python import filepath 
 29  from twisted.cred import credentials 
 30   
 31  from flumotion.configure import configure 
 32  from flumotion.component import component 
 33  from flumotion.common import log, messages, errors, netutils 
 34  from flumotion.component.component import moods 
 35  from flumotion.component.misc.porter import porterclient 
 36  from flumotion.component.base import http as httpbase 
 37  from flumotion.twisted import fdserver 
 38   
 39  __version__ = "$Rev: 6984 $" 
 40   
 41   
 42  # add our own mime types to the ones parsed from /etc/mime.types 
43 -def loadMimeTypes():
44 d = static.loadMimeTypes() 45 d['.flv'] = 'video/x-flv' 46 return d
47 48 # this file is inspired by/adapted from twisted.web.static 49
50 -class File(resource.Resource, filepath.FilePath, log.Loggable):
51 contentTypes = loadMimeTypes() 52 defaultType = "application/octet-stream" 53 54 childNotFound = weberror.NoResource("File not found.") 55
56 - def __init__(self, path, httpauth, mimeToResource=None, 57 rateController=None):
58 resource.Resource.__init__(self) 59 filepath.FilePath.__init__(self, path) 60 61 self._httpauth = httpauth 62 # mapping of mime type -> File subclass 63 self._mimeToResource = mimeToResource or {} 64 self._rateController = rateController 65 self._factory = MimedFileFactory(httpauth, self._mimeToResource, 66 rateController)
67
68 - def getChild(self, path, request):
69 self.log('getChild: self %r, path %r', self, path) 70 # we handle a request ending in '/' as well; this is how those come in 71 if path == '': 72 return self 73 74 self.restat() 75 76 if not self.isdir(): 77 return self.childNotFound 78 79 if path: 80 fpath = self.child(path) 81 else: 82 return self.childNotFound 83 84 if not fpath.exists(): 85 return self.childNotFound 86 87 return self._factory.create(fpath.path)
88
89 - def openForReading(self):
90 """Open a file and return the handle.""" 91 f = self.open() 92 self.debug("[fd %5d] opening file %s", f.fileno(), self.path) 93 return f
94
95 - def getFileSize(self):
96 """Return file size.""" 97 return self.getsize()
98
99 - def render(self, request):
100 self.debug('[fd %5d] render incoming request %r', 101 request.transport.fileno(), request) 102 def terminateSimpleRequest(res, request): 103 if res != server.NOT_DONE_YET: 104 self.debug('finish request %r' % request) 105 request.finish()
106 107 d = self._httpauth.startAuthentication(request) 108 d.addCallback(self.renderAuthenticated, request) 109 d.addCallback(terminateSimpleRequest, request) 110 # Authentication failed; nothing more to do. 111 d.addErrback(lambda x: None) 112 113 return server.NOT_DONE_YET
114
115 - def renderAuthenticated(self, _, request):
116 # Now that we're authenticated (or authentication wasn't requested), 117 # write the file (or appropriate other response) to the client. 118 # We override static.File to implement Range requests, and to get access 119 # to the transfer object to abort it later; the bulk of this is a direct 120 # copy of static.File.render, though. 121 # self.restat() 122 self.debug('renderAuthenticated request %r' % request) 123 124 # make sure we notice changes in the file 125 self.restat() 126 127 ext = os.path.splitext(self.basename())[1].lower() 128 contentType = self.contentTypes.get(ext, self.defaultType) 129 130 if not self.exists(): 131 self.debug("Couldn't find resource %s", self.path) 132 return self.childNotFound.render(request) 133 134 if self.isdir(): 135 self.debug("%s is a directory, can't be GET", self.path) 136 return self.childNotFound.render(request) 137 138 # Different headers not normally set in static.File... 139 # Specify that we will close the connection after this request, and 140 # that the client must not issue further requests. 141 # We do this because future requests on this server might actually need 142 # to go to a different process (because of the porter) 143 request.setHeader('Server', 'Flumotion/%s' % configure.version) 144 request.setHeader('Connection', 'close') 145 # We can do range requests, in bytes. 146 request.setHeader('Accept-Ranges', 'bytes') 147 148 if contentType: 149 self.debug('content type %r' % contentType) 150 request.setHeader('content-type', contentType) 151 152 try: 153 f = self.openForReading() 154 except IOError, e: 155 import errno 156 if e[0] == errno.EACCES: 157 return weberror.ForbiddenResource().render(request) 158 else: 159 raise 160 161 if request.setLastModified(self.getmtime()) is http.CACHED: 162 return '' 163 164 fileSize = self.getFileSize() 165 # first and last byte offset we will write 166 first = 0 167 last = fileSize - 1 168 169 requestRange = request.getHeader('range') 170 if requestRange is not None: 171 # We have a partial data request. 172 # for interpretation of range, see RFC 2068 14.36 173 # examples: bytes=500-999; bytes=-500 (suffix mode; last 500) 174 self.log('range request, %r', requestRange) 175 rangeKeyValue = string.split(requestRange, '=') 176 if len(rangeKeyValue) != 2: 177 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE) 178 return '' 179 180 if rangeKeyValue[0] != 'bytes': 181 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE) 182 return '' 183 184 # ignore a set of range requests for now, only take the first 185 ranges = rangeKeyValue[1].split(',')[0] 186 l = ranges.split('-') 187 if len(l) != 2: 188 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE) 189 return '' 190 191 start, end = l 192 193 if start: 194 # byte-range-spec 195 first = int(start) 196 if end: 197 last = int(end) 198 elif end: 199 # suffix-byte-range-spec 200 count = int(end) 201 # we can't serve more than there are in the file 202 if count > fileSize: 203 count = fileSize 204 first = fileSize - count 205 else: 206 # need at least start or end 207 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE) 208 return '' 209 210 # FIXME: is it still partial if the request was for the complete 211 # file ? Couldn't find a conclusive answer in the spec. 212 request.setResponseCode(http.PARTIAL_CONTENT) 213 request.setHeader('Content-Range', "bytes %d-%d/%d" % 214 (first, last, fileSize)) 215 # Start sending from the requested position in the file 216 if first: 217 # TODO: logs suggest this is called with negative values, figure 218 # out how 219 self.debug("Request for range \"%s\" of file, seeking to " 220 "%d of total file size %d", ranges, first, fileSize) 221 f.seek(first) 222 223 self.do_prepareBody(request, f, first, last) 224 225 if request.method == 'HEAD': 226 return '' 227 228 if self._rateController: 229 self.log("Creating RateControl object using plug %r", 230 self._rateController) 231 # What do we want to pass to this? The consumer we proxy to, 232 # perhaps the request object too? This object? The file itself? 233 234 # We probably want the filename part of the request URL - the bit 235 # after the mount-point. e.g. in /customer1/videos/video1.ogg, we 236 # probably want to provide /videos/video1.ogg to this.. 237 d = defer.maybeDeferred( 238 self._rateController.createProducerConsumerProxy, 239 request, request) 240 else: 241 d = defer.succeed(request) 242 243 def attachProxy(consumer): 244 transfer = FileTransfer(f, last + 1, consumer) 245 request._transfer = transfer
246 d.addCallback(attachProxy) 247 248 return server.NOT_DONE_YET 249
250 - def do_prepareBody(self, request, f, first, last):
251 """ 252 I am called before the body of the response gets written, 253 and after generic header setting has been done. 254 255 I set Content-Length. 256 257 Override me to send additional headers, or to prefix the body 258 with data headers. 259 """ 260 request.setHeader("Content-Length", str(last - first + 1))
261
262 -class MimedFileFactory(log.Loggable):
263 """ 264 I create File subclasses based on the mime type of the given path. 265 """ 266 contentTypes = loadMimeTypes() 267 defaultType = "application/octet-stream" 268
269 - def __init__(self, httpauth, mimeToResource=None, rateController=None):
270 self._httpauth = httpauth 271 self._mimeToResource = mimeToResource or {} 272 self._rateController = rateController
273
274 - def create(self, path):
275 """ 276 Creates and returns an instance of a File subclass based on the mime 277 type/extension of the given path. 278 """ 279 280 self.debug("createMimedFile at %r", path) 281 ext = os.path.splitext(path)[1].lower() 282 mimeType = self.contentTypes.get(ext, self.defaultType) 283 klazz = self._mimeToResource.get(mimeType, File) 284 self.debug("mimetype %s, class %r" % (mimeType, klazz)) 285 return klazz(path, self._httpauth, mimeToResource=self._mimeToResource, 286 rateController=self._rateController)
287
288 -class FLVFile(File):
289 """ 290 I am a File resource for FLV files. 291 I can handle requests with a 'start' GET parameter. 292 This parameter represents the byte offset from where to start. 293 If it is non-zero, I will output an FLV header so the result is 294 playable. 295 """ 296 header = 'FLV\x01\x01\000\000\000\x09\000\000\000\x09' 297
298 - def do_prepareBody(self, request, f, first, last):
299 self.log('do_prepareBody for FLV') 300 length = last - first + 1 301 302 # if there is a non-zero start get parameter, prefix the body with 303 # our FLV header 304 # each value is a list 305 start = int(request.args.get('start', ['0'])[0]) 306 # range request takes precedence over our start parsing 307 if first == 0 and start: 308 self.debug('start %d passed, seeking', start) 309 f.seek(start) 310 length = last - start + 1 + len(self.header) 311 312 request.setHeader("Content-Length", str(length)) 313 314 if request.method == 'HEAD': 315 return '' 316 317 if first == 0 and start: 318 request.write(self.header)
319
320 -class FileTransfer(log.Loggable):
321 """ 322 A class to represent the transfer of a file over the network. 323 """ 324 consumer = None 325
326 - def __init__(self, file, size, consumer):
327 """ 328 @param file: a file handle 329 @type file: file 330 @param size: file position to which file should be read 331 @type size: int 332 @param consumer: consumer to receive the data 333 @type consumer: L{twisted.internet.interfaces.IFinishableConsumer} 334 """ 335 self.file = file 336 self.size = size 337 self.consumer = consumer 338 self.written = self.file.tell() 339 self.bytesWritten = 0 340 self.debug("Calling registerProducer on %r", consumer) 341 consumer.registerProducer(self, 0)
342
343 - def resumeProducing(self):
344 if not self.consumer: 345 return 346 data = self.file.read(min(abstract.FileDescriptor.bufferSize, 347 self.size - self.written)) 348 if data: 349 self.written += len(data) 350 self.bytesWritten += len(data) 351 # this .write will spin the reactor, calling .doWrite and then 352 # .resumeProducing again, so be prepared for a re-entrant call 353 self.consumer.write(data) 354 if self.consumer and self.file.tell() == self.size: 355 log.debug('file-transfer', 356 'written entire file of %d bytes from fd %d', 357 self.size, self.file.fileno()) 358 self.consumer.unregisterProducer() 359 self.consumer.finish() 360 self.consumer = None
361
362 - def pauseProducing(self):
363 pass
364
365 - def stopProducing(self):
366 log.debug('file-transfer', 'stop producing from fd %d at %d/%d bytes', 367 self.file.fileno(), self.file.tell(), self.size) 368 self.file.close() 369 # even though it's the consumer stopping us, from looking at 370 # twisted code it looks like we still are required to 371 # unregsiter and notify the request that we're done... 372 self.consumer.unregisterProducer() 373 self.consumer.finish() 374 self.consumer = None
375