1
# -*- test-case-name: twisted.web.test.test_static -*-
2
# Copyright (c) 2001-2010 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.trp", "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 createPickleChild(self, name, child):
655
"File.createPickleChild is deprecated since Twisted 9.0. "
656
"Resource persistence is beyond the scope of Twisted Web.",
657
DeprecationWarning, stacklevel=2)
659
if not os.path.isdir(self.path):
660
resource.Resource.putChild(self, name, child)
661
# xxx use a file-extension-to-save-function dictionary instead
662
if type(child) == type(""):
663
fl = open(os.path.join(self.path, name), 'wb')
668
fl = open(os.path.join(self.path, name), 'wb')
669
from pickle import Pickler
675
def createSimilarFile(self, path):
676
f = self.__class__(path, self.defaultType, self.ignoredExts, self.registry)
677
# refactoring by steps, here - constructor should almost certainly take these
678
f.processors = self.processors
679
f.indexNames = self.indexNames[:]
680
f.childNotFound = self.childNotFound
685
class StaticProducer(object):
687
Superclass for classes that implement the business of producing.
689
@ivar request: The L{IRequest} to write the contents of the file to.
690
@ivar fileObject: The file the contents of which to write to the request.
693
implements(interfaces.IPullProducer)
695
bufferSize = abstract.FileDescriptor.bufferSize
698
def __init__(self, request, fileObject):
700
Initialize the instance.
702
self.request = request
703
self.fileObject = fileObject
707
raise NotImplementedError(self.start)
710
def resumeProducing(self):
711
raise NotImplementedError(self.resumeProducing)
714
def stopProducing(self):
718
L{IPullProducer.stopProducing} is called when our consumer has died,
719
and subclasses also call this method when they are done producing
722
self.fileObject.close()
727
class NoRangeStaticProducer(StaticProducer):
729
A L{StaticProducer} that writes the entire file to the request.
733
self.request.registerProducer(self, False)
736
def resumeProducing(self):
739
data = self.fileObject.read(self.bufferSize)
741
# this .write will spin the reactor, calling .doWrite and then
742
# .resumeProducing again, so be prepared for a re-entrant call
743
self.request.write(data)
745
self.request.unregisterProducer()
746
self.request.finish()
751
class SingleRangeStaticProducer(StaticProducer):
753
A L{StaticProducer} that writes a single chunk of a file to the request.
756
def __init__(self, request, fileObject, offset, size):
758
Initialize the instance.
760
@param request: See L{StaticProducer}.
761
@param fileObject: See L{StaticProducer}.
762
@param offset: The offset into the file of the chunk to be written.
763
@param size: The size of the chunk to write.
765
StaticProducer.__init__(self, request, fileObject)
771
self.fileObject.seek(self.offset)
772
self.bytesWritten = 0
773
self.request.registerProducer(self, 0)
776
def resumeProducing(self):
779
data = self.fileObject.read(
780
min(self.bufferSize, self.size - self.bytesWritten))
782
self.bytesWritten += len(data)
783
# this .write will spin the reactor, calling .doWrite and then
784
# .resumeProducing again, so be prepared for a re-entrant call
785
self.request.write(data)
786
if self.request and self.bytesWritten == self.size:
787
self.request.unregisterProducer()
788
self.request.finish()
793
class MultipleRangeStaticProducer(StaticProducer):
795
A L{StaticProducer} that writes several chunks of a file to the request.
798
def __init__(self, request, fileObject, rangeInfo):
800
Initialize the instance.
802
@param request: See L{StaticProducer}.
803
@param fileObject: See L{StaticProducer}.
804
@param rangeInfo: A list of tuples C{[(boundary, offset, size)]}
806
- C{boundary} will be written to the request first.
807
- C{offset} the offset into the file of chunk to write.
808
- C{size} the size of the chunk to write.
810
StaticProducer.__init__(self, request, fileObject)
811
self.rangeInfo = rangeInfo
815
self.rangeIter = iter(self.rangeInfo)
817
self.request.registerProducer(self, 0)
820
def _nextRange(self):
821
self.partBoundary, partOffset, self._partSize = self.rangeIter.next()
822
self._partBytesWritten = 0
823
self.fileObject.seek(partOffset)
826
def resumeProducing(self):
832
while dataLength < self.bufferSize:
833
if self.partBoundary:
834
dataLength += len(self.partBoundary)
835
data.append(self.partBoundary)
836
self.partBoundary = None
837
p = self.fileObject.read(
838
min(self.bufferSize - dataLength,
839
self._partSize - self._partBytesWritten))
840
self._partBytesWritten += len(p)
843
if self.request and self._partBytesWritten == self._partSize:
846
except StopIteration:
849
self.request.write(''.join(data))
851
self.request.unregisterProducer()
852
self.request.finish()
856
class FileTransfer(pb.Viewable):
858
A class to represent the transfer of a file over the network.
862
def __init__(self, file, size, request):
864
"FileTransfer is deprecated since Twisted 9.0. "
865
"Use a subclass of StaticProducer instead.",
866
DeprecationWarning, stacklevel=2)
869
self.request = request
870
self.written = self.file.tell()
871
request.registerProducer(self, 0)
873
def resumeProducing(self):
876
data = self.file.read(min(abstract.FileDescriptor.bufferSize, self.size - self.written))
878
self.written += len(data)
879
# this .write will spin the reactor, calling .doWrite and then
880
# .resumeProducing again, so be prepared for a re-entrant call
881
self.request.write(data)
882
if self.request and self.file.tell() == self.size:
883
self.request.unregisterProducer()
884
self.request.finish()
887
def pauseProducing(self):
890
def stopProducing(self):
894
# Remotely relay producer interface.
896
def view_resumeProducing(self, issuer):
897
self.resumeProducing()
899
def view_pauseProducing(self, issuer):
900
self.pauseProducing()
902
def view_stopProducing(self, issuer):
907
class ASISProcessor(resource.Resource):
909
Serve files exactly as responses without generating a status-line or any
910
headers. Inspired by Apache's mod_asis.
913
def __init__(self, path, registry=None):
914
resource.Resource.__init__(self)
916
self.registry = registry or Registry()
919
def render(self, request):
920
request.startedWriting = 1
921
res = File(self.path, registry=self.registry)
922
return res.render(request)
926
def formatFileSize(size):
928
Format the given file size in bytes to human readable format.
932
elif size < (1024 ** 2):
933
return '%iK' % (size / 1024)
934
elif size < (1024 ** 3):
935
return '%iM' % (size / (1024 ** 2))
937
return '%iG' % (size / (1024 ** 3))
941
class DirectoryLister(resource.Resource):
943
Print the content of a directory.
945
@ivar template: page template used to render the content of the directory.
946
It must contain the format keys B{header} and B{tableContent}.
947
@type template: C{str}
949
@ivar linePattern: template used to render one line in the listing table.
950
It must contain the format keys B{class}, B{href}, B{text}, B{size},
951
B{type} and B{encoding}.
952
@type linePattern: C{str}
954
@ivar contentEncodings: a mapping of extensions to encoding types.
955
@type contentEncodings: C{dict}
957
@ivar defaultType: default type used when no mimetype is detected.
958
@type defaultType: C{str}
960
@ivar dirs: filtered content of C{path}, if the whole content should not be
961
displayed (default to C{None}, which means the actual content of
963
@type dirs: C{NoneType} or C{list}
965
@ivar path: directory which content should be listed.
971
<title>%(header)s</title>
973
.even-dir { background-color: #efe0ef }
974
.even { background-color: #eee }
975
.odd-dir {background-color: #f0d0ef }
976
.odd { background-color: #dedede }
977
.icon { text-align: center }
985
body { border: 0; padding: 0; margin: 0; background-color: #efefef; }
986
h1 {padding: 0.1em; background-color: #777; color: white; border-bottom: thin white dashed;}
999
<th>Content type</th>
1000
<th>Content encoding</th>
1012
linePattern = """<tr class="%(class)s">
1013
<td><a href="%(href)s">%(text)s</a></td>
1016
<td>%(encoding)s</td>
1020
def __init__(self, pathname, dirs=None,
1021
contentTypes=File.contentTypes,
1022
contentEncodings=File.contentEncodings,
1023
defaultType='text/html'):
1024
resource.Resource.__init__(self)
1025
self.contentTypes = contentTypes
1026
self.contentEncodings = contentEncodings
1027
self.defaultType = defaultType
1028
# dirs allows usage of the File to specify what gets listed
1030
self.path = pathname
1033
def _getFilesAndDirectories(self, directory):
1035
Helper returning files and directories in given directory listing, with
1036
attributes to be used to build a table content with
1037
C{self.linePattern}.
1039
@return: tuple of (directories, files)
1040
@rtype: C{tuple} of C{list}
1044
for path in directory:
1045
url = urllib.quote(path, "/")
1046
escapedPath = cgi.escape(path)
1047
if os.path.isdir(os.path.join(self.path, path)):
1049
dirs.append({'text': escapedPath + "/", 'href': url,
1050
'size': '', 'type': '[Directory]',
1053
mimetype, encoding = getTypeAndEncoding(path, self.contentTypes,
1054
self.contentEncodings,
1057
size = os.stat(os.path.join(self.path, path)).st_size
1061
'text': escapedPath, "href": url,
1062
'type': '[%s]' % mimetype,
1063
'encoding': (encoding and '[%s]' % encoding or ''),
1064
'size': formatFileSize(size)})
1068
def _buildTableContent(self, elements):
1070
Build a table content using C{self.linePattern} and giving elements odd
1074
rowClasses = itertools.cycle(['odd', 'even'])
1075
for element, rowClass in zip(elements, rowClasses):
1076
element["class"] = rowClass
1077
tableContent.append(self.linePattern % element)
1081
def render(self, request):
1083
Render a listing of the content of C{self.path}.
1085
if self.dirs is None:
1086
directory = os.listdir(self.path)
1089
directory = self.dirs
1091
dirs, files = self._getFilesAndDirectories(directory)
1093
tableContent = "".join(self._buildTableContent(dirs + files))
1095
header = "Directory listing for %s" % (
1096
cgi.escape(urllib.unquote(request.uri)),)
1098
return self.template % {"header": header, "tableContent": tableContent}
1102
return '<DirectoryLister of %r>' % self.path