1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
44 d = static.loadMimeTypes()
45 d['.flv'] = 'video/x-flv'
46 return d
47
48
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
63 self._mimeToResource = mimeToResource or {}
64 self._rateController = rateController
65 self._factory = MimedFileFactory(httpauth, self._mimeToResource,
66 rateController)
67
69 self.log('getChild: self %r, path %r', self, path)
70
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
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
96 """Return file size."""
97 return self.getsize()
98
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
111 d.addErrback(lambda x: None)
112
113 return server.NOT_DONE_YET
114
116
117
118
119
120
121
122 self.debug('renderAuthenticated request %r' % request)
123
124
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
139
140
141
142
143 request.setHeader('Server', 'Flumotion/%s' % configure.version)
144 request.setHeader('Connection', 'close')
145
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
166 first = 0
167 last = fileSize - 1
168
169 requestRange = request.getHeader('range')
170 if requestRange is not None:
171
172
173
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
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
195 first = int(start)
196 if end:
197 last = int(end)
198 elif end:
199
200 count = int(end)
201
202 if count > fileSize:
203 count = fileSize
204 first = fileSize - count
205 else:
206
207 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
208 return ''
209
210
211
212 request.setResponseCode(http.PARTIAL_CONTENT)
213 request.setHeader('Content-Range', "bytes %d-%d/%d" %
214 (first, last, fileSize))
215
216 if first:
217
218
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
232
233
234
235
236
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
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
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
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
303
304
305 start = int(request.args.get('start', ['0'])[0])
306
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
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
361
364
375