1 # -*- test-case-name: twisted.web.test.test_static -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 Static resources for L{twisted.web}.
16 from zope.interface import implements
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
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
31 dangerousPathError = resource.NoResource("Invalid request URL.")
33 def isDangerous(path):
34 return path == '..' or '/' in path or os.sep in path
37 class Data(resource.Resource):
39 This is a static, in-memory resource.
42 def __init__(self, data, type):
43 resource.Resource.__init__(self)
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":
54 render_HEAD = render_GET
57 def addSlash(request):
59 qindex = request.uri.find('?')
61 qs = request.uri[qindex:]
63 return "http%s://%s%s/%s" % (
64 request.isSecure() and 's' or '',
65 request.getHeader("host"),
66 (request.uri.split('?')[0]),
69 class Redirect(resource.Resource):
70 def __init__(self, request):
71 resource.Resource.__init__(self)
72 self.url = addSlash(request)
74 def render(self, request):
75 return redirectTo(self.url, request)
78 class Registry(components.Componentized, styles.Versioned):
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.
85 components.Componentized.__init__(self)
88 persistenceVersion = 1
90 def upgradeToVersion1(self):
93 def cachePath(self, path, rsrc):
94 self._pathCache[path] = rsrc
96 def getCachedPath(self, path):
97 return self._pathCache.get(path)
100 def loadMimeTypes(mimetype_locations=['/etc/mime.types']):
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
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
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',
121 '.swf': 'application/x-shockwave-flash',
122 '.tgz': 'application/x-gtar',
123 '.wml': 'text/vnd.wap.wml',
124 '.xul': 'application/vnd.mozilla.xul+xml',
126 '.patch': 'text/plain',
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)
135 contentTypes.update(more)
139 def getTypeAndEncoding(filename, types, encodings, defaultType):
140 p, ext = os.path.splitext(filename)
142 if encodings.has_key(ext):
144 ext = os.path.splitext(p)[1].lower()
147 type = types.get(ext, defaultType)
152 class File(resource.Resource, styles.Versioned, filepath.FilePath):
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
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.
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 .
169 @cvar childNotFound: L{Resource} used to render 404 Not Found error pages.
172 contentTypes = loadMimeTypes()
181 indexNames = ["index", "index.html", "index.htm", "index.rpy"]
187 persistenceVersion = 6
189 def upgradeToVersion6(self):
190 self.ignoredExts = []
196 def upgradeToVersion5(self):
197 if not isinstance(self.registry, Registry):
198 self.registry = Registry()
201 def upgradeToVersion4(self):
202 if not hasattr(self, 'registry'):
206 def upgradeToVersion3(self):
207 if not hasattr(self, 'allowExt'):
211 def upgradeToVersion2(self):
212 self.defaultType = "text/html"
215 def upgradeToVersion1(self):
216 if hasattr(self, 'indexName'):
217 self.indexNames = [self.indexName]
221 def __init__(self, path, defaultType="text/html", ignoredExts=(), registry=None, allowExt=0):
223 Create a file with the given path.
225 @param path: The filename of the file from which this L{File} will
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
233 @type defaultType: C{str}
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"}.
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}
246 @param allowExt: Ignored parameter, only present for backwards
247 compatibility. Do not pass a value for this parameter.
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 = ['*']
257 self.ignoredExts = []
259 self.ignoredExts = list(ignoredExts)
260 self.registry = registry or Registry()
263 def ignoreExt(self, ext):
264 """Ignore the given extension.
266 Serve file.ext if file is requested
268 self.ignoredExts.append(ext)
270 childNotFound = resource.NoResource("File not found.")
272 def directoryListing(self):
273 return DirectoryLister(self.path,
276 self.contentEncodings,
280 def getChild(self, path, request):
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.
285 If C{path} is the empty string, return a L{DirectoryLister} instead.
287 self.restat(reraise=False)
290 return self.childNotFound
294 fpath = self.child(path)
295 except filepath.InsecurePath:
296 return self.childNotFound
298 fpath = self.childSearchPreauth(*self.indexNames)
300 return self.directoryListing()
302 if not fpath.exists():
303 fpath = fpath.siblingExtensionSearch(*self.ignoredExts)
305 return self.childNotFound
307 if platformType == "win32":
308 # don't want .RPY to be different than .rpy, since that would allow
310 processor = InsensitiveDict(self.processors).get(fpath.splitext()[1])
312 processor = self.processors.get(fpath.splitext()[1])
314 return resource.IResource(processor(fpath.path, self.registry))
315 return self.createSimilarFile(fpath.path)
318 # methods to allow subclasses to e.g. decrypt files on the fly:
319 def openForReading(self):
320 """Open a file and return it."""
324 def getFileSize(self):
325 """Return file size."""
326 return self.getsize()
329 def _parseRangeHeader(self, range):
331 Parse the value of a Range header into (start, stop) pairs.
333 In a given pair, either of start or stop can be None, signifying that
334 no value was provided, but not both.
336 @return: A list C{[(start, stop)]} of pairs of length at least one.
338 @raise ValueError: if the header is syntactically invalid or if the
339 Bytes-Unit is anything other than 'bytes'.
342 kind, value = range.split('=', 1)
344 raise ValueError("Missing '=' separator")
347 raise ValueError("Unsupported Bytes-Unit: %r" % (kind,))
348 unparsedRanges = filter(None, map(str.strip, value.split(',')))
350 for byteRange in unparsedRanges:
352 start, end = byteRange.split('-', 1)
354 raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
359 raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
366 raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
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,))
374 # One or both of start and end must be specified. Omitting
376 raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
377 parsedRanges.append((start, end))
381 def _rangeToOffsetAndSize(self, start, end):
383 Convert a start and end from a Range header to an offset and size.
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
389 Either but not both of start or end can be C{None}:
391 - Omitted start means that the end value is actually a start value
392 relative to the end of the resource.
394 - Omitted end means the end of the resource should be the end of
397 End is interpreted as inclusive, as per RFC 2616.
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.
402 @param start: The start value from the header, or C{None} if one was
404 @param end: The end value from the header, or C{None} if one was not
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.
410 size = self.getFileSize()
422 return start, (end - start)
425 def _contentRange(self, offset, size):
427 Return a string suitable for the value of a Content-Range header for a
428 range with the given offset and size.
430 The offset and size are not sanity checked in any way.
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
437 return 'bytes %d-%d/%d' % (
438 offset, offset + size - 1, self.getFileSize())
441 def _doSingleRangeRequest(self, request, (start, end)):
443 Set up the response for Range headers that specify a single range.
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.
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.
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)
462 'content-range', 'bytes */%d' % (self.getFileSize(),))
464 request.setResponseCode(http.PARTIAL_CONTENT)
466 'content-range', self._contentRange(offset, size))
470 def _doMultipleRangeRequest(self, request, byteRanges):
472 Set up the response for Range headers that specify a single range.
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
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.
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}
497 matchingRangeFound = False
500 boundary = "%x%x" % (int(time.time()*1000000), os.getpid())
502 contentType = self.type
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:
509 contentLength += partSize
510 matchingRangeFound = True
511 partContentRange = self._contentRange(partOffset, partSize)
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)
523 'content-length', '0')
525 'content-range', 'bytes */%d' % (self.getFileSize(),))
527 finalBoundary = "\r\n--" + boundary + "--\r\n"
528 rangeInfo.append((finalBoundary, 0, 0))
529 request.setResponseCode(http.PARTIAL_CONTENT)
531 'content-type', 'multipart/byteranges; boundary="%s"' % (boundary,))
533 'content-length', contentLength + len(finalBoundary))
537 def _setContentHeaders(self, request, size=None):
539 Set the Content-length and Content-type headers for this request.
541 This method is not appropriate for requests for multiple byte ranges;
542 L{_doMultipleRangeRequest} will set these headers in that case.
544 @param request: The L{Request} object.
545 @param size: The size of the response. If not specified, default to
546 C{self.getFileSize()}.
549 size = self.getFileSize()
550 request.setHeader('content-length', str(size))
552 request.setHeader('content-type', self.type)
554 request.setHeader('content-encoding', self.encoding)
557 def makeProducer(self, request, fileForReading):
559 Make a L{StaticProducer} that will produce the body of this response.
561 This method will also set the response code and Content-* headers.
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.
568 byteRange = request.getHeader('range')
569 if byteRange is None:
570 self._setContentHeaders(request)
571 request.setResponseCode(http.OK)
572 return NoRangeStaticProducer(request, fileForReading)
574 parsedRanges = self._parseRangeHeader(byteRange)
576 log.msg("Ignoring malformed Range header %r" % (byteRange,))
577 self._setContentHeaders(request)
578 request.setResponseCode(http.OK)
579 return NoRangeStaticProducer(request, fileForReading)
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)
588 rangeInfo = self._doMultipleRangeRequest(request, parsedRanges)
589 return MultipleRangeStaticProducer(
590 request, fileForReading, rangeInfo)
593 def render_GET(self, request):
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.
600 if self.type is None:
601 self.type, self.encoding = getTypeAndEncoding(self.basename(),
603 self.contentEncodings,
606 if not self.exists():
607 return self.childNotFound.render(request)
610 return self.redirect(request)
612 request.setHeader('accept-ranges', 'bytes')
615 fileForReading = self.openForReading()
618 if e[0] == errno.EACCES:
619 return resource.ForbiddenResource().render(request)
623 if request.setLastModified(self.getmtime()) is http.CACHED:
627 producer = self.makeProducer(request, fileForReading)
629 if request.method == 'HEAD':
633 # and make sure the connection doesn't get closed
634 return server.NOT_DONE_YET
635 render_HEAD = render_GET
638 def redirect(self, request):
639 return redirectTo(addSlash(request), request)
645 directory = self.listdir()
649 def listEntities(self):
650 return map(lambda fileName, self=self: self.createSimilarFile(os.path.join(self.path, fileName)), self.listNames())
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
663 class StaticProducer(object):
665 Superclass for classes that implement the business of producing.
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.
671 implements(interfaces.IPullProducer)
673 bufferSize = abstract.FileDescriptor.bufferSize
676 def __init__(self, request, fileObject):
678 Initialize the instance.
680 self.request = request
681 self.fileObject = fileObject
685 raise NotImplementedError(self.start)
688 def resumeProducing(self):
689 raise NotImplementedError(self.resumeProducing)
692 def stopProducing(self):
696 L{IPullProducer.stopProducing} is called when our consumer has died,
697 and subclasses also call this method when they are done producing
700 self.fileObject.close()
705 class NoRangeStaticProducer(StaticProducer):
707 A L{StaticProducer} that writes the entire file to the request.
711 self.request.registerProducer(self, False)
714 def resumeProducing(self):
717 data = self.fileObject.read(self.bufferSize)
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)
723 self.request.unregisterProducer()
724 self.request.finish()
729 class SingleRangeStaticProducer(StaticProducer):
731 A L{StaticProducer} that writes a single chunk of a file to the request.
734 def __init__(self, request, fileObject, offset, size):
736 Initialize the instance.
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.
743 StaticProducer.__init__(self, request, fileObject)
749 self.fileObject.seek(self.offset)
750 self.bytesWritten = 0
751 self.request.registerProducer(self, 0)
754 def resumeProducing(self):
757 data = self.fileObject.read(
758 min(self.bufferSize, self.size - self.bytesWritten))
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()
771 class MultipleRangeStaticProducer(StaticProducer):
773 A L{StaticProducer} that writes several chunks of a file to the request.
776 def __init__(self, request, fileObject, rangeInfo):
778 Initialize the instance.
780 @param request: See L{StaticProducer}.
781 @param fileObject: See L{StaticProducer}.
782 @param rangeInfo: A list of tuples C{[(boundary, offset, size)]}
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.
788 StaticProducer.__init__(self, request, fileObject)
789 self.rangeInfo = rangeInfo
793 self.rangeIter = iter(self.rangeInfo)
795 self.request.registerProducer(self, 0)
798 def _nextRange(self):
799 self.partBoundary, partOffset, self._partSize = self.rangeIter.next()
800 self._partBytesWritten = 0
801 self.fileObject.seek(partOffset)
804 def resumeProducing(self):
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)
821 if self.request and self._partBytesWritten == self._partSize:
824 except StopIteration:
827 self.request.write(''.join(data))
829 self.request.unregisterProducer()
830 self.request.finish()
834 class FileTransfer(pb.Viewable):
836 A class to represent the transfer of a file over the network.
840 def __init__(self, file, size, request):
842 "FileTransfer is deprecated since Twisted 9.0. "
843 "Use a subclass of StaticProducer instead.",
844 DeprecationWarning, stacklevel=2)
847 self.request = request
848 self.written = self.file.tell()
849 request.registerProducer(self, 0)
851 def resumeProducing(self):
854 data = self.file.read(min(abstract.FileDescriptor.bufferSize, self.size - self.written))
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()
865 def pauseProducing(self):
868 def stopProducing(self):
872 # Remotely relay producer interface.
874 def view_resumeProducing(self, issuer):
875 self.resumeProducing()
877 def view_pauseProducing(self, issuer):
878 self.pauseProducing()
880 def view_stopProducing(self, issuer):
885 class ASISProcessor(resource.Resource):
887 Serve files exactly as responses without generating a status-line or any
888 headers. Inspired by Apache's mod_asis.
891 def __init__(self, path, registry=None):
892 resource.Resource.__init__(self)
894 self.registry = registry or Registry()
897 def render(self, request):
898 request.startedWriting = 1
899 res = File(self.path, registry=self.registry)
900 return res.render(request)
904 def formatFileSize(size):
906 Format the given file size in bytes to human readable format.
910 elif size < (1024 ** 2):
911 return '%iK' % (size / 1024)
912 elif size < (1024 ** 3):
913 return '%iM' % (size / (1024 ** 2))
915 return '%iG' % (size / (1024 ** 3))
919 class DirectoryLister(resource.Resource):
921 Print the content of a directory.
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}
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}
932 @ivar contentEncodings: a mapping of extensions to encoding types.
933 @type contentEncodings: C{dict}
935 @ivar defaultType: default type used when no mimetype is detected.
936 @type defaultType: C{str}
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
941 @type dirs: C{NoneType} or C{list}
943 @ivar path: directory which content should be listed.
949 <title>%(header)s</title>
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 }
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;}
977 <th>Content type</th>
978 <th>Content encoding</th>
990 linePattern = """<tr class="%(class)s">
991 <td><a href="%(href)s">%(text)s</a></td>
994 <td>%(encoding)s</td>
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
1008 self.path = pathname
1011 def _getFilesAndDirectories(self, directory):
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}.
1017 @return: tuple of (directories, files)
1018 @rtype: C{tuple} of C{list}
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)):
1027 dirs.append({'text': escapedPath + "/", 'href': url,
1028 'size': '', 'type': '[Directory]',
1031 mimetype, encoding = getTypeAndEncoding(path, self.contentTypes,
1032 self.contentEncodings,
1035 size = os.stat(os.path.join(self.path, path)).st_size
1039 'text': escapedPath, "href": url,
1040 'type': '[%s]' % mimetype,
1041 'encoding': (encoding and '[%s]' % encoding or ''),
1042 'size': formatFileSize(size)})
1046 def _buildTableContent(self, elements):
1048 Build a table content using C{self.linePattern} and giving elements odd
1052 rowClasses = itertools.cycle(['odd', 'even'])
1053 for element, rowClass in zip(elements, rowClasses):
1054 element["class"] = rowClass
1055 tableContent.append(self.linePattern % element)
1059 def render(self, request):
1061 Render a listing of the content of C{self.path}.
1063 request.setHeader("content-type", "text/html; charset=utf-8")
1064 if self.dirs is None:
1065 directory = os.listdir(self.path)
1068 directory = self.dirs
1070 dirs, files = self._getFilesAndDirectories(directory)
1072 tableContent = "".join(self._buildTableContent(dirs + files))
1074 header = "Directory listing for %s" % (
1075 cgi.escape(urllib.unquote(request.uri)),)
1077 return self.template % {"header": header, "tableContent": tableContent}
1081 return '<DirectoryLister of %r>' % self.path