root / trunk / twisted / web / static.py

Revision 27029, 35.4 kB (checked in by mwh, 4 days ago)

Merge t.w.static.File-multiple-ranges-3574-2

Author: mwhudson
Reviewer: therve
Fixes: #3574
Fixes: #3814

Support multiple byteranges in the Range: header in requests for
static resources.

In the process fix a bug with Content-Length being wrong when the
request range only partially overlaps with the resource.

Line 
1 # -*- test-case-name: twisted.web.test.test_static -*-
2 # Copyright (c) 2001-2009 Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 Static resources for L{twisted.web}.
7 """
8
9 import os
10 import warnings
11 import urllib
12 import itertools
13 import cgi
14 import time
15
16 from zope.interface import implements
17
18 from twisted.web import server
19 from twisted.web import resource
20 from twisted.web import http
21 from twisted.web.util import redirectTo
22
23 from twisted.python import components, filepath, log
24 from twisted.internet import abstract, interfaces
25 from twisted.spread import pb
26 from twisted.persisted import styles
27 from twisted.python.util import InsensitiveDict
28 from twisted.python.runtime import platformType
29
30
31 dangerousPathError = resource.NoResource("Invalid request URL.")
32
33 def isDangerous(path):
34     return path == '..' or '/' in path or os.sep in path
35
36
37 class Data(resource.Resource):
38     """
39     This is a static, in-memory resource.
40     """
41
42     def __init__(self, data, type):
43         resource.Resource.__init__(self)
44         self.data = data
45         self.type = type
46
47     def render(self, request):
48         request.setHeader("content-type", self.type)
49         request.setHeader("content-length", str(len(self.data)))
50         if request.method == "HEAD":
51             return ''
52         return self.data
53
54 def addSlash(request):
55     qs = ''
56     qindex = request.uri.find('?')
57     if qindex != -1:
58         qs = request.uri[qindex:]
59
60     return "http%s://%s%s/%s" % (
61         request.isSecure() and 's' or '',
62         request.getHeader("host"),
63         (request.uri.split('?')[0]),
64         qs)
65
66 class Redirect(resource.Resource):
67     def __init__(self, request):
68         resource.Resource.__init__(self)
69         self.url = addSlash(request)
70
71     def render(self, request):
72         return redirectTo(self.url, request)
73
74
75 class Registry(components.Componentized, styles.Versioned):
76     """
77     I am a Componentized object that will be made available to internal Twisted
78     file-based dynamic web content such as .rpy and .epy scripts.
79     """
80
81     def __init__(self):
82         components.Componentized.__init__(self)
83         self._pathCache = {}
84
85     persistenceVersion = 1
86
87     def upgradeToVersion1(self):
88         self._pathCache = {}
89
90     def cachePath(self, path, rsrc):
91         self._pathCache[path] = rsrc
92
93     def getCachedPath(self, path):
94         return self._pathCache.get(path)
95
96
97 def loadMimeTypes(mimetype_locations=['/etc/mime.types']):
98     """
99     Multiple file locations containing mime-types can be passed as a list.
100     The files will be sourced in that order, overriding mime-types from the
101     files sourced beforehand, but only if a new entry explicitly overrides
102     the current entry.
103     """
104     import mimetypes
105     # Grab Python's built-in mimetypes dictionary.
106     contentTypes = mimetypes.types_map
107     # Update Python's semi-erroneous dictionary with a few of the
108     # usual suspects.
109     contentTypes.update(
110         {
111             '.conf''text/plain',
112             '.diff''text/plain',
113             '.exe':   'application/x-executable',
114             '.flac''audio/x-flac',
115             '.java''text/plain',
116             '.ogg':   'application/ogg',
117             '.oz':    'text/x-oz',
118             '.swf':   'application/x-shockwave-flash',
119             '.tgz':   'application/x-gtar',
120             '.wml':   'text/vnd.wap.wml',
121             '.xul':   'application/vnd.mozilla.xul+xml',
122             '.py':    'text/plain',
123             '.patch': 'text/plain',
124         }
125     )
126     # Users can override these mime-types by loading them out configuration
127     # files (this defaults to ['/etc/mime.types']).
128     for location in mimetype_locations:
129         if os.path.exists(location):
130             more = mimetypes.read_mime_types(location)
131             if more is not None:
132                 contentTypes.update(more)
133
134     return contentTypes
135
136 def getTypeAndEncoding(filename, types, encodings, defaultType):
137     p, ext = os.path.splitext(filename)
138     ext = ext.lower()
139     if encodings.has_key(ext):
140         enc = encodings[ext]
141         ext = os.path.splitext(p)[1].lower()
142     else:
143         enc = None
144     type = types.get(ext, defaultType)
145     return type, enc
146
147
148
149 class File(resource.Resource, styles.Versioned, filepath.FilePath):
150     """
151     File is a resource that represents a plain non-interpreted file
152     (although it can look for an extension like .rpy or .cgi and hand the
153     file to a processor for interpretation if you wish). Its constructor
154     takes a file path.
155
156     Alternatively, you can give a directory path to the constructor. In this
157     case the resource will represent that directory, and its children will
158     be files underneath that directory. This provides access to an entire
159     filesystem tree with a single Resource.
160
161     If you map the URL 'http://server/FILE' to a resource created as
162     File('/tmp'), then http://server/FILE/ will return an HTML-formatted
163     listing of the /tmp/ directory, and http://server/FILE/foo/bar.html will
164     return the contents of /tmp/foo/bar.html .
165
166     @cvar childNotFound: L{Resource} used to render 404 Not Found error pages.
167     """
168
169     contentTypes = loadMimeTypes()
170
171     contentEncodings = {
172         ".gz" : "gzip",
173         ".bz2": "bzip2"
174         }
175
176     processors = {}
177
178     indexNames = ["index", "index.html", "index.htm", "index.trp", "index.rpy"]
179
180     type = None
181
182     ### Versioning
183
184     persistenceVersion = 6
185
186     def upgradeToVersion6(self):
187         self.ignoredExts = []
188         if self.allowExt:
189             self.ignoreExt("*")
190         del self.allowExt
191
192     def upgradeToVersion5(self):
193         if not isinstance(self.registry, Registry):
194             self.registry = Registry()
195
196     def upgradeToVersion4(self):
197         if not hasattr(self, 'registry'):
198             self.registry = {}
199
200     def upgradeToVersion3(self):
201         if not hasattr(self, 'allowExt'):
202             self.allowExt = 0
203
204     def upgradeToVersion2(self):
205         self.defaultType = "text/html"
206
207     def upgradeToVersion1(self):
208         if hasattr(self, 'indexName'):
209             self.indexNames = [self.indexName]
210             del self.indexName
211
212     def __init__(self, path, defaultType="text/html", ignoredExts=(), registry=None, allowExt=0):
213         """
214         Create a file with the given path.
215
216         @param path: The filename of the file from which this L{File} will
217             serve data.
218         @type path: C{str}
219
220         @param defaultType: A I{major/minor}-style MIME type specifier
221             indicating the I{Content-Type} with which this L{File}'s data
222             will be served if a MIME type cannot be determined based on
223             C{path}'s extension.
224         @type defaultType: C{str}
225
226         @param ignoredExts: A sequence giving the extensions of paths in the
227             filesystem which will be ignored for the purposes of child
228             lookup.  For example, if C{ignoredExts} is C{(".bar",)} and
229             C{path} is a directory containing a file named C{"foo.bar"}, a
230             request for the C{"foo"} child of this resource will succeed
231             with a L{File} pointing to C{"foo.bar"}.
232
233         @param registry: The registry object being used to handle this
234             request.  If C{None}, one will be created.
235         @type registry: L{Registry}
236
237         @param allowExt: Ignored parameter, only present for backwards
238             compatibility.  Do not pass a value for this parameter.
239         """
240         resource.Resource.__init__(self)
241         filepath.FilePath.__init__(self, path)
242         self.defaultType = defaultType
243         if ignoredExts in (0, 1) or allowExt:
244             warnings.warn("ignoredExts should receive a list, not a boolean")
245             if ignoredExts or allowExt:
246                 self.ignoredExts = ['*']
247             else:
248                 self.ignoredExts = []
249         else:
250             self.ignoredExts = list(ignoredExts)
251         self.registry = registry or Registry()
252
253
254     def ignoreExt(self, ext):
255         """Ignore the given extension.
256
257         Serve file.ext if file is requested
258         """
259         self.ignoredExts.append(ext)
260
261     childNotFound = resource.NoResource("File not found.")
262
263     def directoryListing(self):
264         return DirectoryLister(self.path,
265                                self.listNames(),
266                                self.contentTypes,
267                                self.contentEncodings,
268                                self.defaultType)
269
270
271     def getChild(self, path, request):
272         """
273         If this L{File}'s path refers to a directory, return a L{File}
274         referring to the file named C{path} in that directory.
275
276         If C{path} is the empty string, return a L{DirectoryLister} instead.
277         """
278         self.restat(reraise=False)
279
280         if not self.isdir():
281             return self.childNotFound
282
283         if path:
284             try:
285                 fpath = self.child(path)
286             except filepath.InsecurePath:
287                 return self.childNotFound
288         else:
289             fpath = self.childSearchPreauth(*self.indexNames)
290             if fpath is None:
291                 return self.directoryListing()
292
293         if not fpath.exists():
294             fpath = fpath.siblingExtensionSearch(*self.ignoredExts)
295             if fpath is None:
296                 return self.childNotFound
297
298         if platformType == "win32":
299             # don't want .RPY to be different than .rpy, since that would allow
300             # source disclosure.
301             processor = InsensitiveDict(self.processors).get(fpath.splitext()[1])
302         else:
303             processor = self.processors.get(fpath.splitext()[1])
304         if processor:
305             return resource.IResource(processor(fpath.path, self.registry))
306         return self.createSimilarFile(fpath.path)
307
308
309     # methods to allow subclasses to e.g. decrypt files on the fly:
310     def openForReading(self):
311         """Open a file and return it."""
312         return self.open()
313
314
315     def getFileSize(self):
316         """Return file size."""
317         return self.getsize()
318
319
320     def _parseRangeHeader(self, range):
321         """
322         Parse the value of a Range header into (start, stop) pairs.
323
324         In a given pair, either of start or stop can be None, signifying that
325         no value was provided, but not both.
326
327         @return: A list C{[(start, stop)]} of pairs of length at least one.
328
329         @raise ValueError: if the header is syntactically invalid or if the
330             Bytes-Unit is anything other than 'bytes'.
331         """
332         try:
333             kind, value = range.split('=', 1)
334         except ValueError:
335             raise ValueError("Missing '=' separator")
336         kind = kind.strip()
337         if kind != 'bytes':
338             raise ValueError("Unsupported Bytes-Unit: %r" % (kind,))
339         unparsedRanges = filter(None, map(str.strip, value.split(',')))
340         parsedRanges = []
341         for byteRange in unparsedRanges:
342             try:
343                 start, end = byteRange.split('-', 1)
344             except ValueError:
345                 raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
346             if start:
347                 try:
348                     start = int(start)
349                 except ValueError:
350                     raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
351             else:
352                 start = None
353             if end:
354                 try:
355                     end = int(end)
356                 except ValueError:
357                     raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
358             else:
359                 end = None
360             if start is not None:
361                 if end is not None and start > end:
362                     # Start must be less than or equal to end or it is invalid.
363                     raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
364             elif end is None:
365                 # One or both of start and end must be specified.  Omitting
366                 # both is invalid.
367                 raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
368             parsedRanges.append((start, end))
369         return parsedRanges
370
371
372     def _rangeToOffsetAndSize(self, start, end):
373         """
374         Convert a start and end from a Range header to an offset and size.
375
376         This method checks that the resulting range overlaps with the resource
377         being served (and so has the value of C{getFileSize()} as an indirect
378         input).
379
380         Either but not both of start or end can be C{None}:
381
382          - Omitted start means that the end value is actually a start value
383            relative to the end of the resource.
384
385          - Omitted end means the end of the resource should be the end of
386            the range.
387
388         End is interpreted as inclusive, as per RFC 2616.
389
390         If this range doesn't overlap with any of this resource, C{(0, 0)} is
391         returned, which is not otherwise a value return value.
392
393         @param start: The start value from the header, or C{None} if one was
394             not present.
395         @param end: The end value from the header, or C{None} if one was not
396             present.
397         @return: C{(offset, size)} where offset is how far into this resource
398             this resource the range begins and size is how long the range is,
399             or C{(0, 0)} if the range does not overlap this resource.
400         """
401         size = self.getFileSize()
402         if start is None:
403             start = size - end
404             end = size
405         elif end is None:
406             end = size
407         elif end < size:
408             end += 1
409         elif end > size:
410             end = size
411         if start >= size:
412             start = end = 0
413         return start, (end - start)
414
415
416     def _contentRange(self, offset, size):
417         """
418         Return a string suitable for the value of a Content-Range header for a
419         range with the given offset and size.
420
421         The offset and size are not sanity checked in any way.
422
423         @param offset: How far into this resource the range begins.
424         @param size: How long the range is.
425         @return: The value as appropriate for the value of a Content-Range
426             header.
427         """
428         return 'bytes %d-%d/%d' % (
429             offset, offset + size - 1, self.getFileSize())
430
431
432     def _doSingleRangeRequest(self, request, (start, end)):
433         """
434         Set up the response for Range headers that specify a single range.
435
436         This method checks if the request is satisfiable and sets the response
437         code and Content-Range header appropriately.  The return value
438         indicates which part of the resource to return.
439
440         @param request: The Request object.
441         @param start: The start of the byte range as specified by the header.
442         @param end: The end of the byte range as specified by the header.  At
443             most one of C{start} and C{end} may be C{None}.
444         @return: A 2-tuple of the offset and size of the range to return.
445             offset == size == 0 indicates that the request is not satisfiable.
446         """
447         offset, size  = self._rangeToOffsetAndSize(start, end)
448         if offset == size == 0:
449             # This range doesn't overlap with any of this resource, so the
450             # request is unsatisfiable.
451             request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
452             request.setHeader(
453                 'content-range', 'bytes */%d' % (self.getFileSize(),))
454         else:
455             request.setResponseCode(http.PARTIAL_CONTENT)
456             request.setHeader(
457                 'content-range', self._contentRange(offset, size))
458         return offset, size
459
460
461     def _doMultipleRangeRequest(self, request, byteRanges):
462         """
463         Set up the response for Range headers that specify a single range.
464
465         This method checks if the request is satisfiable and sets the response
466         code and Content-Type and Content-Length headers appropriately.  The
467         return value, which is a little complicated, indicates which parts of
468         the resource to return and the boundaries that should separate the
469         parts.
470
471         In detail, the return value is a tuple rangeInfo C{rangeInfo} is a
472         list of 3-tuples C{(partSeparator, partOffset, partSize)}.  The
473         response to this request should be, for each element of C{rangeInfo},
474         C{partSeparator} followed by C{partSize} bytes of the resource
475         starting at C{partOffset}.  Each C{partSeparator} includes the
476         MIME-style boundary and the part-specific Content-type and
477         Content-range headers.  It is convenient to return the separator as a
478         concrete string from this method, becasue this method needs to compute
479         the number of bytes that will make up the response to be able to set
480         the Content-Length header of the response accurately.
481
482         @param request: The Request object.
483         @param byteRanges: A list of C{(start, end)} values as specified by
484             the header.  For each range, at most one of C{start} and C{end}
485             may be C{None}.
486         @return: See above.
487         """
488         matchingRangeFound = False
489         rangeInfo = []
490         contentLength = 0
491         boundary = "%x%x" % (int(time.time()*1000000), os.getpid())
492         if self.type:
493             contentType = self.type
494         else:
495             contentType = 'bytes' # It's what Apache does...
496         for start, end in byteRanges:
497             partOffset, partSize = self._rangeToOffsetAndSize(start, end)
498             if partOffset == partSize == 0:
499                 continue
500             contentLength += partSize
501             matchingRangeFound = True
502             partContentRange = self._contentRange(partOffset, partSize)
503             partSeparator = (
504                 "\r\n"
505                 "--%s\r\n"
506                 "Content-type: %s\r\n"
507                 "Content-range: %s\r\n"
508                 "\r\n") % (boundary, contentType, partContentRange)
509             contentLength += len(partSeparator)
510             rangeInfo.append((partSeparator, partOffset, partSize))
511         if not matchingRangeFound:
512             request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
513             request.setHeader(
514                 'content-length', '0')
515             request.setHeader(
516                 'content-range', 'bytes */%d' % (self.getFileSize(),))
517             return [], ''
518         finalBoundary = "\r\n--" + boundary + "--\r\n"
519         rangeInfo.append((finalBoundary, 0, 0))
520         request.setResponseCode(http.PARTIAL_CONTENT)
521         request.setHeader(
522             'content-type', 'multipart/byteranges; boundary="%s"' % (boundary,))
523         request.setHeader(
524             'content-length', contentLength + len(finalBoundary))
525         return rangeInfo
526
527
528     def _setContentHeaders(self, request, size=None):
529         """
530         Set the Content-length and Content-type headers for this request.
531
532         This method is not appropriate for requests for multiple byte ranges;
533         L{_doMultipleRangeRequest} will set these headers in that case.
534
535         @param request: The L{Request} object.
536         @param size: The size of the response.  If not specified, default to
537             C{self.getFileSize()}.
538         """
539         if size is None:
540             size = self.getFileSize()
541         request.setHeader('content-length', str(size))
542         if self.type:
543             request.setHeader('content-type', self.type)
544         if self.encoding:
545             request.setHeader('content-encoding', self.encoding)
546
547
548     def makeProducer(self, request, fileForReading):
549         """
550         Make a L{StaticProducer} that will produce the body of this response.
551
552         This method will also set the response code and Content-* headers.
553
554         @param request: The L{Request} object.
555         @param fileForReading: The file object containing the resource.
556         @return: A L{StaticProducer}.  Calling C{.start()} on this will begin
557             producing the response.
558         """
559         byteRange = request.getHeader('range')
560         if byteRange is None:
561             self._setContentHeaders(request)
562             request.setResponseCode(http.OK)
563             return NoRangeStaticProducer(request, fileForReading)
564         try:
565             parsedRanges = self._parseRangeHeader(byteRange)
566         except ValueError:
567             log.msg("Ignoring malformed Range header %r" % (byteRange,))
568             self._setContentHeaders(request)
569             request.setResponseCode(http.OK)
570             return NoRangeStaticProducer(request, fileForReading)
571
572         if len(parsedRanges) == 1:
573             offset, size = self._doSingleRangeRequest(
574                 request, parsedRanges[0])
575             self._setContentHeaders(request, size)
576             return SingleRangeStaticProducer(
577                 request, fileForReading, offset, size)
578         else:
579             rangeInfo = self._doMultipleRangeRequest(request, parsedRanges)
580             return MultipleRangeStaticProducer(
581                 request, fileForReading, rangeInfo)
582
583
584     def render(self, request):
585         """
586         Begin sending the contents of this L{File} (or a subset of the
587         contents, based on the 'range' header) to the given request.
588         """
589         self.restat(False)
590
591         if self.type is None:
592             self.type, self.encoding = getTypeAndEncoding(self.basename(),
593                                                           self.contentTypes,
594                                                           self.contentEncodings,
595                                                           self.defaultType)
596
597         if not self.exists():
598             return self.childNotFound.render(request)
599
600         if self.isdir():
601             return self.redirect(request)
602
603         request.setHeader('accept-ranges', 'bytes')
604
605         try:
606             fileForReading = self.openForReading()
607         except IOError, e:
608             import errno
609             if e[0] == errno.EACCES:
610                 return resource.ForbiddenResource().render(request)
611             else:
612                 raise
613
614         if request.setLastModified(self.getmtime()) is http.CACHED:
615             return ''
616
617
618         producer = self.makeProducer(request, fileForReading)
619
620         if request.method == 'HEAD':
621             return ''
622
623         producer.start()
624         # and make sure the connection doesn't get closed
625         return server.NOT_DONE_YET
626
627
628     def redirect(self, request):
629         return redirectTo(addSlash(request), request)
630
631     def listNames(self):
632         if not self.isdir():
633             return []
634         directory = self.listdir()
635         directory.sort()
636         return directory
637
638     def listEntities(self):
639         return map(lambda fileName, self=self: self.createSimilarFile(os.path.join(self.path, fileName)), self.listNames())
640
641     def createPickleChild(self, name, child):
642         warnings.warn(
643             "File.createPickleChild is deprecated since Twisted 9.0.  "
644             "Resource persistence is beyond the scope of Twisted Web.",
645             DeprecationWarning, stacklevel=2)
646
647         if not os.path.isdir(self.path):
648             resource.Resource.putChild(self, name, child)
649         # xxx use a file-extension-to-save-function dictionary instead
650         if type(child) == type(""):
651             fl = open(os.path.join(self.path, name), 'wb')
652             fl.write(child)
653         else:
654             if '.' not in name:
655                 name = name + '.trp'
656             fl = open(os.path.join(self.path, name), 'wb')
657             from pickle import Pickler
658             pk = Pickler(fl)
659             pk.dump(child)
660         fl.close()
661
662     def createSimilarFile(self, path):
663         f = self.__class__(path, self.defaultType, self.ignoredExts, self.registry)
664         # refactoring by steps, here - constructor should almost certainly take these
665         f.processors = self.processors
666         f.indexNames = self.indexNames[:]
667         f.childNotFound = self.childNotFound
668         return f
669
670
671
672 class StaticProducer(object):
673     """
674     Superclass for classes that implement the business of producing.
675     """
676
677     implements(interfaces.IPullProducer)
678
679     bufferSize = abstract.FileDescriptor.bufferSize
680
681     def start(self):
682         raise NotImplementedError(self.start)
683
684     def resumeProducing(self):
685         raise NotImplementedError(self.resumeProducing)
686
687
688
689 class NoRangeStaticProducer(StaticProducer):
690     """
691     A L{StaticProducer} that writes the entire file to the request.
692     """
693
694     def __init__(self, request, fileObject):
695         """
696         Initialize the instance.
697
698         @param request: The L{IRequest} to write the contents of the file to.
699         @param fileObject: The file the contents of which to write to the
700             request.
701         """
702         self.request = request
703         self.fileObject = fileObject
704
705     def start(self):
706         self.request.registerProducer(self, False)
707
708     def resumeProducing(self):
709         if not self.request:
710             return
711         data = self.fileObject.read(self.bufferSize)
712         if data:
713             # this .write will spin the reactor, calling .doWrite and then
714             # .resumeProducing again, so be prepared for a re-entrant call
715             self.request.write(data)
716         else:
717             self.request.unregisterProducer()
718             self.request.finish()
719             self.request = None
720
721
722
723 class SingleRangeStaticProducer(StaticProducer):
724     """
725     A L{StaticProducer} that writes a single chunk of a file to the request.
726     """
727
728     def __init__(self, request, fileObject, offset, size):
729         """
730         Initialize the instance.
731
732         @param request: The L{IRequest} to write a chunk of the file to.
733         @param fileObject: The file a chunk of the contents of which to write
734             to the request.
735         @param offset: The offset into the file of the chunk to be written.
736         @param size: The size of the chunk to write.
737         """
738         self.request = request
739         self.fileObject = fileObject
740         self.offset = offset
741         self.size = size
742
743     def start(self):
744         self.fileObject.seek(self.offset)
745         self.bytesWritten = 0
746         self.request.registerProducer(self, 0)
747
748     def resumeProducing(self):
749         if not self.request:
750             return
751         data = self.fileObject.read(
752             min(self.bufferSize, self.size - self.bytesWritten))
753         if data:
754             self.bytesWritten += len(data)
755             # this .write will spin the reactor, calling .doWrite and then
756             # .resumeProducing again, so be prepared for a re-entrant call
757             self.request.write(data)
758         if self.request and self.bytesWritten == self.size:
759             self.request.unregisterProducer()
760             self.request.finish()
761             self.request = None
762
763
764
765 class MultipleRangeStaticProducer(StaticProducer):
766     """
767     A L{StaticProducer} that writes several chunks of a file to the request.
768     """
769
770     def __init__(self, request, fileObject, rangeInfo):
771         """
772         Initialize the instance.
773
774         @param request: The L{IRequest} to write the contents of the file to.
775         @param fileObject: The file the contents of which to write to the
776             request.
777         @param rangeInfo: A list of tuples C{[(boundary, offset, size)]}
778             where:
779              - C{boundary} will be written to the request first.
780              - C{offset} the offset into the file of chunk to write.
781              - C{size} the size of the chunk to write.
782         """
783         self.request = request
784         self.fileObject = fileObject
785         self.rangeInfo = rangeInfo
786
787     def start(self):
788         self.rangeIter = iter(self.rangeInfo)
789         self._nextRange()
790         self.request.registerProducer(self, 0)
791
792     def _nextRange(self):
793         self.partBoundary, partOffset, self._partSize = self.rangeIter.next()
794         self._partBytesWritten = 0
795         self.fileObject.seek(partOffset)
796
797     def resumeProducing(self):
798         if not self.request:
799             return
800         data = []
801         dataLength = 0
802         done = False
803         while dataLength < self.bufferSize:
804             if self.partBoundary:
805                 dataLength += len(self.partBoundary)
806                 data.append(self.partBoundary)
807                 self.partBoundary = None
808             p = self.fileObject.read(
809                 min(self.bufferSize - dataLength,
810                     self._partSize - self._partBytesWritten))
811             self._partBytesWritten += len(p)
812             dataLength += len(p)
813             data.append(p)
814             if self.request and self._partBytesWritten == self._partSize:
815                 try:
816                     self._nextRange()
817                 except StopIteration:
818                     done = True
819                     break
820         self.request.write(''.join(data))
821         if done:
822             self.request.unregisterProducer()
823             self.request.finish()
824             self.request = None
825
826
827 class FileTransfer(pb.Viewable):
828     """
829     A class to represent the transfer of a file over the network.
830     """
831     request = None
832
833     def __init__(self, file, size, request):
834         warnings.warn(
835             "FileTransfer is deprecated since Twisted 9.0. "
836             "Use a subclass of StaticProducer instead.",
837             DeprecationWarning, stacklevel=2)
838         self.file = file
839         self.size = size
840         self.request = request
841         self.written = self.file.tell()
842         request.registerProducer(self, 0)
843
844     def resumeProducing(self):
845         if not self.request:
846             return
847         data = self.file.read(min(abstract.FileDescriptor.bufferSize, self.size - self.written))
848         if data:
849             self.written += len(data)
850             # this .write will spin the reactor, calling .doWrite and then
851             # .resumeProducing again, so be prepared for a re-entrant call
852             self.request.write(data)
853         if self.request and self.file.tell() == self.size:
854             self.request.unregisterProducer()
855             self.request.finish()
856             self.request = None
857
858     def pauseProducing(self):
859         pass
860
861     def stopProducing(self):
862         self.file.close()
863         self.request = None
864
865     # Remotely relay producer interface.
866
867     def view_resumeProducing(self, issuer):
868         self.resumeProducing()
869
870     def view_pauseProducing(self, issuer):
871         self.pauseProducing()
872
873     def view_stopProducing(self, issuer):
874         self.stopProducing()
875
876
877
878 class ASISProcessor(resource.Resource):
879     """
880     Serve files exactly as responses without generating a status-line or any
881     headers.  Inspired by Apache's mod_asis.
882     """
883
884     def __init__(self, path, registry=None):
885         resource.Resource.__init__(self)
886         self.path = path
887         self.registry = registry or Registry()
888
889
890     def render(self, request):
891         request.startedWriting = 1
892         res = File(self.path, registry=self.registry)
893         return res.render(request)
894
895
896
897 def formatFileSize(size):
898     """
899     Format the given file size in bytes to human readable format.
900     """
901     if size < 1024:
902         return '%iB' % size
903     elif size < (1024 ** 2):
904         return '%iK' % (size / 1024)
905     elif size < (1024 ** 3):
906         return '%iM' % (size / (1024 ** 2))
907     else:
908         return '%iG' % (size / (1024 ** 3))
909
910
911
912 class DirectoryLister(resource.Resource):
913     """
914     Print the content of a directory.
915
916     @ivar template: page template used to render the content of the directory.
917         It must contain the format keys B{header} and B{tableContent}.
918     @type template: C{str}
919
920     @ivar linePattern: template used to render one line in the listing table.
921         It must contain the format keys B{class}, B{href}, B{text}, B{size},
922         B{type} and B{encoding}.
923     @type linePattern: C{str}
924
925     @ivar contentEncodings: a mapping of extensions to encoding types.
926     @type contentEncodings: C{dict}
927
928     @ivar defaultType: default type used when no mimetype is detected.
929     @type defaultType: C{str}
930
931     @ivar dirs: filtered content of C{path}, if the whole content should not be
932         displayed (default to C{None}, which means the actual content of
933         C{path} is printed).
934     @type dirs: C{NoneType} or C{list}
935
936     @ivar path: directory which content should be listed.
937     @type path: C{str}
938     """
939
940     template = """<html>
941 <head>
942 <title>%(header)s</title>
943 <style>
944 .even-dir { background-color: #efe0ef }
945 .even { background-color: #eee }
946 .odd-dir {background-color: #f0d0ef }
947 .odd { background-color: #dedede }
948 .icon { text-align: center }
949 .listing {
950     margin-left: auto;
951     margin-right: auto;
952     width: 50%%;
953     padding: 0.1em;
954     }
955
956 body { border: 0; padding: 0; margin: 0; background-color: #efefef; }
957 h1 {padding: 0.1em; background-color: #777; color: white; border-bottom: thin white dashed;}
958
959 </style>
960 </head>
961
962 <body>
963 <h1>%(header)s</h1>
964
965 <table>
966     <thead>
967         <tr>
968             <th>Filename</th>
969             <th>Size</th>
970             <th>Content type</th>
971             <th>Content encoding</th>
972         </tr>
973     </thead>
974     <tbody>
975 %(tableContent)s
976     </tbody>
977 </table>
978
979 </body>
980 </html>
981 """
982
983     linePattern = """<tr class="%(class)s">
984     <td><a href="%(href)s">%(text)s</a></td>
985     <td>%(size)s</td>
986     <td>%(type)s</td>
987     <td>%(encoding)s</td>
988 </tr>
989 """
990
991     def __init__(self, pathname, dirs=None,
992                  contentTypes=File.contentTypes,
993                  contentEncodings=File.contentEncodings,
994                  defaultType='text/html'):
995         resource.Resource.__init__(self)
996         self.contentTypes = contentTypes
997         self.contentEncodings = contentEncodings
998         self.defaultType = defaultType
999         # dirs allows usage of the File to specify what gets listed
1000         self.dirs = dirs
1001         self.path = pathname
1002
1003
1004     def _getFilesAndDirectories(self, directory):
1005         """
1006         Helper returning files and directories in given directory listing, with
1007         attributes to be used to build a table content with
1008         C{self.linePattern}.
1009
1010         @return: tuple of (directories, files)
1011         @rtype: C{tuple} of C{list}
1012         """
1013         files = []
1014         dirs = []
1015         for path in directory:
1016             url = urllib.quote(path, "/")
1017             escapedPath = cgi.escape(path)
1018             if os.path.isdir(os.path.join(self.path, path)):
1019                 url = url + '/'
1020                 dirs.append({'text': escapedPath + "/", 'href': url,
1021                              'size': '', 'type': '[Directory]',
1022                              'encoding': ''})
1023             else:
1024                 mimetype, encoding = getTypeAndEncoding(path, self.contentTypes,
1025                                                         self.contentEncodings,
1026                                                         self.defaultType)
1027                 try:
1028                     size = os.stat(os.path.join(self.path, path)).st_size
1029                 except OSError:
1030                     continue
1031                 files.append({
1032                     'text': escapedPath, "href": url,
1033                     'type': '[%s]' % mimetype,
1034                     'encoding': (encoding and '[%s]' % encoding or ''),
1035                     'size': formatFileSize(size)})
1036         return dirs, files
1037
1038
1039     def _buildTableContent(self, elements):
1040         """
1041         Build a table content using C{self.linePattern} and giving elements odd
1042         and even classes.
1043         """
1044         tableContent = []
1045         rowClasses = itertools.cycle(['odd', 'even'])
1046         for element, rowClass in zip(elements, rowClasses):
1047             element["class"] = rowClass
1048             tableContent.append(self.linePattern % element)
1049         return tableContent
1050
1051
1052     def render(self, request):
1053         """
1054         Render a listing of the content of C{self.path}.
1055         """
1056         if self.dirs is None:
1057             directory = os.listdir(self.path)
1058             directory.sort()
1059         else:
1060             directory = self.dirs
1061
1062         dirs, files = self._getFilesAndDirectories(directory)
1063
1064         tableContent = "".join(self._buildTableContent(dirs + files))
1065
1066         header = "Directory listing for %s" % (
1067             cgi.escape(urllib.unquote(request.uri)),)
1068
1069         return self.template % {"header": header, "tableContent": tableContent}
1070
1071
1072     def __repr__(self):
1073         return '<DirectoryLister of %r>' % self.path
1074
1075     __str__ = __repr__
Note: See TracBrowser for help on using the browser.