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