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

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

  1  # -*- Mode: Python; test-case-name: flumotion.test.test_misc_httpfile -*- 
  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  # add our own mime types to the ones parsed from /etc/mime.types 
40 -def loadMimeTypes():
41 d = static.loadMimeTypes() 42 d['.flv'] = 'video/x-flv' 43 return d
44 45 # this file is inspired by/adapted from twisted.web.static 46
47 -class File(resource.Resource, filepath.FilePath, log.Loggable):
48 contentTypes = loadMimeTypes() 49 defaultType = "application/octet-stream" 50 51 childNotFound = weberror.NoResource("File not found.") 52
53 - def __init__(self, path, httpauth, mimeToResource=None):
54 resource.Resource.__init__(self) 55 filepath.FilePath.__init__(self, path) 56 57 self._httpauth = httpauth 58 # mapping of mime type -> File subclass 59 self._mimeToResource = mimeToResource or {} 60 self._factory = MimedFileFactory(httpauth, self._mimeToResource)
61
62 - def getChild(self, path, request):
63 self.log('getChild: self %r, path %r', self, path) 64 # we handle a request ending in '/' as well; this is how those come in 65 if path == '': 66 return self 67 68 self.restat() 69 70 if not self.isdir(): 71 return self.childNotFound 72 73 if path: 74 fpath = self.child(path) 75 else: 76 return self.childNotFound 77 78 if not fpath.exists(): 79 return self.childNotFound 80 81 return self._factory.create(fpath.path)
82
83 - def openForReading(self):
84 """Open a file and return it.""" 85 f = self.open() 86 self.debug("Reading file from FD: %d", f.fileno()) 87 return f
88
89 - def getFileSize(self):
90 """Return file size.""" 91 return self.getsize()
92
93 - def render(self, request):
94 self.debug('render request %r' % request) 95 def terminateSimpleRequest(res, request): 96 if res != server.NOT_DONE_YET: 97 request.finish()
98 99 d = self._httpauth.startAuthentication(request) 100 d.addCallback(self.renderAuthenticated, request) 101 d.addCallback(terminateSimpleRequest, request) 102 # Authentication failed; nothing more to do. 103 d.addErrback(lambda x: None) 104 105 return server.NOT_DONE_YET
106
107 - def renderAuthenticated(self, _, request):
108 # Now that we're authenticated (or authentication wasn't requested), 109 # write the file (or appropriate other response) to the client. 110 # We override static.File to implement Range requests, and to get access 111 # to the transfer object to abort it later; the bulk of this is a direct 112 # copy of static.File.render, though. 113 # self.restat() 114 self.debug('renderAuthenticated request %r' % request) 115 116 # make sure we notice changes in the file 117 self.restat() 118 119 ext = os.path.splitext(self.basename())[1].lower() 120 contentType = self.contentTypes.get(ext, self.defaultType) 121 122 if not self.exists(): 123 self.debug("Couldn't find resource %s", self.path) 124 return self.childNotFound.render(request) 125 126 if self.isdir(): 127 self.debug("%s is a directory, can't be GET", self.path) 128 return self.childNotFound.render(request) 129 130 # Different headers not normally set in static.File... 131 # Specify that we will close the connection after this request, and 132 # that the client must not issue further requests. 133 # We do this because future requests on this server might actually need 134 # to go to a different process (because of the porter) 135 request.setHeader('Server', 'Flumotion/%s' % configure.version) 136 request.setHeader('Connection', 'close') 137 # We can do range requests, in bytes. 138 request.setHeader('Accept-Ranges', 'bytes') 139 140 if contentType: 141 self.debug('content type %r' % contentType) 142 request.setHeader('content-type', contentType) 143 144 try: 145 f = self.openForReading() 146 except IOError, e: 147 import errno 148 if e[0] == errno.EACCES: 149 return weberror.ForbiddenResource().render(request) 150 else: 151 raise 152 153 if request.setLastModified(self.getmtime()) is http.CACHED: 154 return '' 155 156 fileSize = self.getFileSize() 157 # first and last byte offset we will write 158 first = 0 159 last = fileSize - 1 160 161 range = request.getHeader('range') 162 if range is not None: 163 # We have a partial data request. 164 # for interpretation of range, see RFC 2068 14.36 165 # examples: bytes=500-999; bytes=-500 (suffix mode; last 500) 166 self.log('range request, %r', range) 167 rangeKeyValue = string.split(range, '=') 168 if len(rangeKeyValue) != 2: 169 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE) 170 return '' 171 172 if rangeKeyValue[0] != 'bytes': 173 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE) 174 return '' 175 176 # ignore a set of range requests for now, only take the first 177 ranges = rangeKeyValue[1].split(',')[0] 178 l = ranges.split('-') 179 if len(l) != 2: 180 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE) 181 return '' 182 183 start, end = l 184 185 if start: 186 # byte-range-spec 187 first = int(start) 188 if end: 189 last = int(end) 190 elif end: 191 # suffix-byte-range-spec 192 count = int(end) 193 # we can't serve more than there are in the file 194 if count > fileSize: 195 count = fileSize 196 first = fileSize - count 197 else: 198 # need at least start or end 199 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE) 200 return '' 201 202 # FIXME: is it still partial if the request was for the complete 203 # file ? Couldn't find a conclusive answer in the spec. 204 request.setResponseCode(http.PARTIAL_CONTENT) 205 request.setHeader('Content-Range', "bytes %d-%d/%d" % 206 (first, last, fileSize)) 207 # Start sending from the requested position in the file 208 if first: 209 # TODO: logs suggest this is called with negative values, figure 210 # out how 211 self.debug("Request for range \"%s\" of file, seeking to " 212 "%d of total file size %d", ranges, first, fileSize) 213 f.seek(first) 214 215 self.do_prepareBody(request, f, first, last) 216 217 if request.method == 'HEAD': 218 return '' 219 220 request._transfer = FileTransfer(f, last + 1, request) 221 222 return server.NOT_DONE_YET
223
224 - def do_prepareBody(self, request, f, first, last):
225 """ 226 I am called before the body of the response gets written, 227 and after generic header setting has been done. 228 229 I set Content-Length. 230 231 Override me to send additional headers, or to prefix the body 232 with data headers. 233 """ 234 request.setHeader("Content-Length", str(last - first + 1))
235
236 -class MimedFileFactory(log.Loggable):
237 """ 238 I create File subclasses based on the mime type of the given path. 239 """ 240 contentTypes = loadMimeTypes() 241 defaultType = "application/octet-stream" 242
243 - def __init__(self, httpauth, mimeToResource=None):
244 self._httpauth = httpauth 245 self._mimeToResource = mimeToResource or {}
246
247 - def create(self, path):
248 """ 249 Creates and returns an instance of a File subclass based on the mime 250 type/extension of the given path. 251 """ 252 253 self.debug("createMimedFile at %r", path) 254 ext = os.path.splitext(path)[1].lower() 255 mimeType = self.contentTypes.get(ext, self.defaultType) 256 klazz = self._mimeToResource.get(mimeType, File) 257 self.debug("mimetype %s, class %r" % (mimeType, klazz)) 258 return klazz(path, self._httpauth, mimeToResource=self._mimeToResource)
259
260 -class FLVFile(File):
261 """ 262 I am a File resource for FLV files. 263 I can handle requests with a 'start' GET parameter. 264 This parameter represents the byte offset from where to start. 265 If it is non-zero, I will output an FLV header so the result is 266 playable. 267 """ 268 header = 'FLV\x01\x01\000\000\000\x09\000\000\000\x09' 269
270 - def do_prepareBody(self, request, f, first, last):
271 self.log('do_prepareBody for FLV') 272 length = last - first + 1 273 274 # if there is a non-zero start get parameter, prefix the body with 275 # our FLV header 276 # each value is a list 277 start = int(request.args.get('start', ['0'])[0]) 278 # range request takes precedence over our start parsing 279 if first == 0 and start: 280 self.debug('start %d passed, seeking', start) 281 f.seek(start) 282 length = last - start + 1 + len(self.header) 283 284 request.setHeader("Content-Length", str(length)) 285 286 if request.method == 'HEAD': 287 return '' 288 289 if first == 0 and start: 290 request.write(self.header)
291
292 -class FileTransfer:
293 """ 294 A class to represent the transfer of a file over the network. 295 """ 296 request = None 297
298 - def __init__(self, file, size, request):
299 self.file = file 300 self.size = size 301 self.request = request 302 self.written = self.file.tell() 303 self.bytesWritten = 0 304 request.registerProducer(self, 0)
305
306 - def resumeProducing(self):
307 if not self.request: 308 return 309 data = self.file.read(min(abstract.FileDescriptor.bufferSize, 310 self.size - self.written)) 311 if data: 312 self.written += len(data) 313 self.bytesWritten += len(data) 314 # this .write will spin the reactor, calling .doWrite and then 315 # .resumeProducing again, so be prepared for a re-entrant call 316 self.request.write(data) 317 if self.request and self.file.tell() == self.size: 318 self.request.unregisterProducer() 319 self.request.finish() 320 self.request = None
321
322 - def pauseProducing(self):
323 pass
324
325 - def stopProducing(self):
326 self.file.close() 327 self.request = None
328