~ntt-pf-lab/nova/monkey_patch_notification

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/web/static.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.web.test.test_static -*-
 
2
# Copyright (c) 2001-2010 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.trp", "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 createPickleChild(self, name, child):
 
654
        warnings.warn(
 
655
            "File.createPickleChild is deprecated since Twisted 9.0.  "
 
656
            "Resource persistence is beyond the scope of Twisted Web.",
 
657
            DeprecationWarning, stacklevel=2)
 
658
 
 
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')
 
664
            fl.write(child)
 
665
        else:
 
666
            if '.' not in name:
 
667
                name = name + '.trp'
 
668
            fl = open(os.path.join(self.path, name), 'wb')
 
669
            from pickle import Pickler
 
670
            pk = Pickler(fl)
 
671
            pk.dump(child)
 
672
        fl.close()
 
673
 
 
674
 
 
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
 
681
        return f
 
682
 
 
683
 
 
684
 
 
685
class StaticProducer(object):
 
686
    """
 
687
    Superclass for classes that implement the business of producing.
 
688
 
 
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.
 
691
    """
 
692
 
 
693
    implements(interfaces.IPullProducer)
 
694
 
 
695
    bufferSize = abstract.FileDescriptor.bufferSize
 
696
 
 
697
 
 
698
    def __init__(self, request, fileObject):
 
699
        """
 
700
        Initialize the instance.
 
701
        """
 
702
        self.request = request
 
703
        self.fileObject = fileObject
 
704
 
 
705
 
 
706
    def start(self):
 
707
        raise NotImplementedError(self.start)
 
708
 
 
709
 
 
710
    def resumeProducing(self):
 
711
        raise NotImplementedError(self.resumeProducing)
 
712
 
 
713
 
 
714
    def stopProducing(self):
 
715
        """
 
716
        Stop producing data.
 
717
 
 
718
        L{IPullProducer.stopProducing} is called when our consumer has died,
 
719
        and subclasses also call this method when they are done producing
 
720
        data.
 
721
        """
 
722
        self.fileObject.close()
 
723
        self.request = None
 
724
 
 
725
 
 
726
 
 
727
class NoRangeStaticProducer(StaticProducer):
 
728
    """
 
729
    A L{StaticProducer} that writes the entire file to the request.
 
730
    """
 
731
 
 
732
    def start(self):
 
733
        self.request.registerProducer(self, False)
 
734
 
 
735
 
 
736
    def resumeProducing(self):
 
737
        if not self.request:
 
738
            return
 
739
        data = self.fileObject.read(self.bufferSize)
 
740
        if data:
 
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)
 
744
        else:
 
745
            self.request.unregisterProducer()
 
746
            self.request.finish()
 
747
            self.stopProducing()
 
748
 
 
749
 
 
750
 
 
751
class SingleRangeStaticProducer(StaticProducer):
 
752
    """
 
753
    A L{StaticProducer} that writes a single chunk of a file to the request.
 
754
    """
 
755
 
 
756
    def __init__(self, request, fileObject, offset, size):
 
757
        """
 
758
        Initialize the instance.
 
759
 
 
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.
 
764
        """
 
765
        StaticProducer.__init__(self, request, fileObject)
 
766
        self.offset = offset
 
767
        self.size = size
 
768
 
 
769
 
 
770
    def start(self):
 
771
        self.fileObject.seek(self.offset)
 
772
        self.bytesWritten = 0
 
773
        self.request.registerProducer(self, 0)
 
774
 
 
775
 
 
776
    def resumeProducing(self):
 
777
        if not self.request:
 
778
            return
 
779
        data = self.fileObject.read(
 
780
            min(self.bufferSize, self.size - self.bytesWritten))
 
781
        if data:
 
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()
 
789
            self.stopProducing()
 
790
 
 
791
 
 
792
 
 
793
class MultipleRangeStaticProducer(StaticProducer):
 
794
    """
 
795
    A L{StaticProducer} that writes several chunks of a file to the request.
 
796
    """
 
797
 
 
798
    def __init__(self, request, fileObject, rangeInfo):
 
799
        """
 
800
        Initialize the instance.
 
801
 
 
802
        @param request: See L{StaticProducer}.
 
803
        @param fileObject: See L{StaticProducer}.
 
804
        @param rangeInfo: A list of tuples C{[(boundary, offset, size)]}
 
805
            where:
 
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.
 
809
        """
 
810
        StaticProducer.__init__(self, request, fileObject)
 
811
        self.rangeInfo = rangeInfo
 
812
 
 
813
 
 
814
    def start(self):
 
815
        self.rangeIter = iter(self.rangeInfo)
 
816
        self._nextRange()
 
817
        self.request.registerProducer(self, 0)
 
818
 
 
819
 
 
820
    def _nextRange(self):
 
821
        self.partBoundary, partOffset, self._partSize = self.rangeIter.next()
 
822
        self._partBytesWritten = 0
 
823
        self.fileObject.seek(partOffset)
 
824
 
 
825
 
 
826
    def resumeProducing(self):
 
827
        if not self.request:
 
828
            return
 
829
        data = []
 
830
        dataLength = 0
 
831
        done = False
 
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)
 
841
            dataLength += len(p)
 
842
            data.append(p)
 
843
            if self.request and self._partBytesWritten == self._partSize:
 
844
                try:
 
845
                    self._nextRange()
 
846
                except StopIteration:
 
847
                    done = True
 
848
                    break
 
849
        self.request.write(''.join(data))
 
850
        if done:
 
851
            self.request.unregisterProducer()
 
852
            self.request.finish()
 
853
            self.request = None
 
854
 
 
855
 
 
856
class FileTransfer(pb.Viewable):
 
857
    """
 
858
    A class to represent the transfer of a file over the network.
 
859
    """
 
860
    request = None
 
861
 
 
862
    def __init__(self, file, size, request):
 
863
        warnings.warn(
 
864
            "FileTransfer is deprecated since Twisted 9.0. "
 
865
            "Use a subclass of StaticProducer instead.",
 
866
            DeprecationWarning, stacklevel=2)
 
867
        self.file = file
 
868
        self.size = size
 
869
        self.request = request
 
870
        self.written = self.file.tell()
 
871
        request.registerProducer(self, 0)
 
872
 
 
873
    def resumeProducing(self):
 
874
        if not self.request:
 
875
            return
 
876
        data = self.file.read(min(abstract.FileDescriptor.bufferSize, self.size - self.written))
 
877
        if data:
 
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()
 
885
            self.request = None
 
886
 
 
887
    def pauseProducing(self):
 
888
        pass
 
889
 
 
890
    def stopProducing(self):
 
891
        self.file.close()
 
892
        self.request = None
 
893
 
 
894
    # Remotely relay producer interface.
 
895
 
 
896
    def view_resumeProducing(self, issuer):
 
897
        self.resumeProducing()
 
898
 
 
899
    def view_pauseProducing(self, issuer):
 
900
        self.pauseProducing()
 
901
 
 
902
    def view_stopProducing(self, issuer):
 
903
        self.stopProducing()
 
904
 
 
905
 
 
906
 
 
907
class ASISProcessor(resource.Resource):
 
908
    """
 
909
    Serve files exactly as responses without generating a status-line or any
 
910
    headers.  Inspired by Apache's mod_asis.
 
911
    """
 
912
 
 
913
    def __init__(self, path, registry=None):
 
914
        resource.Resource.__init__(self)
 
915
        self.path = path
 
916
        self.registry = registry or Registry()
 
917
 
 
918
 
 
919
    def render(self, request):
 
920
        request.startedWriting = 1
 
921
        res = File(self.path, registry=self.registry)
 
922
        return res.render(request)
 
923
 
 
924
 
 
925
 
 
926
def formatFileSize(size):
 
927
    """
 
928
    Format the given file size in bytes to human readable format.
 
929
    """
 
930
    if size < 1024:
 
931
        return '%iB' % size
 
932
    elif size < (1024 ** 2):
 
933
        return '%iK' % (size / 1024)
 
934
    elif size < (1024 ** 3):
 
935
        return '%iM' % (size / (1024 ** 2))
 
936
    else:
 
937
        return '%iG' % (size / (1024 ** 3))
 
938
 
 
939
 
 
940
 
 
941
class DirectoryLister(resource.Resource):
 
942
    """
 
943
    Print the content of a directory.
 
944
 
 
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}
 
948
 
 
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}
 
953
 
 
954
    @ivar contentEncodings: a mapping of extensions to encoding types.
 
955
    @type contentEncodings: C{dict}
 
956
 
 
957
    @ivar defaultType: default type used when no mimetype is detected.
 
958
    @type defaultType: C{str}
 
959
 
 
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
 
962
        C{path} is printed).
 
963
    @type dirs: C{NoneType} or C{list}
 
964
 
 
965
    @ivar path: directory which content should be listed.
 
966
    @type path: C{str}
 
967
    """
 
968
 
 
969
    template = """<html>
 
970
<head>
 
971
<title>%(header)s</title>
 
972
<style>
 
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 }
 
978
.listing {
 
979
    margin-left: auto;
 
980
    margin-right: auto;
 
981
    width: 50%%;
 
982
    padding: 0.1em;
 
983
    }
 
984
 
 
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;}
 
987
 
 
988
</style>
 
989
</head>
 
990
 
 
991
<body>
 
992
<h1>%(header)s</h1>
 
993
 
 
994
<table>
 
995
    <thead>
 
996
        <tr>
 
997
            <th>Filename</th>
 
998
            <th>Size</th>
 
999
            <th>Content type</th>
 
1000
            <th>Content encoding</th>
 
1001
        </tr>
 
1002
    </thead>
 
1003
    <tbody>
 
1004
%(tableContent)s
 
1005
    </tbody>
 
1006
</table>
 
1007
 
 
1008
</body>
 
1009
</html>
 
1010
"""
 
1011
 
 
1012
    linePattern = """<tr class="%(class)s">
 
1013
    <td><a href="%(href)s">%(text)s</a></td>
 
1014
    <td>%(size)s</td>
 
1015
    <td>%(type)s</td>
 
1016
    <td>%(encoding)s</td>
 
1017
</tr>
 
1018
"""
 
1019
 
 
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
 
1029
        self.dirs = dirs
 
1030
        self.path = pathname
 
1031
 
 
1032
 
 
1033
    def _getFilesAndDirectories(self, directory):
 
1034
        """
 
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}.
 
1038
 
 
1039
        @return: tuple of (directories, files)
 
1040
        @rtype: C{tuple} of C{list}
 
1041
        """
 
1042
        files = []
 
1043
        dirs = []
 
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)):
 
1048
                url = url + '/'
 
1049
                dirs.append({'text': escapedPath + "/", 'href': url,
 
1050
                             'size': '', 'type': '[Directory]',
 
1051
                             'encoding': ''})
 
1052
            else:
 
1053
                mimetype, encoding = getTypeAndEncoding(path, self.contentTypes,
 
1054
                                                        self.contentEncodings,
 
1055
                                                        self.defaultType)
 
1056
                try:
 
1057
                    size = os.stat(os.path.join(self.path, path)).st_size
 
1058
                except OSError:
 
1059
                    continue
 
1060
                files.append({
 
1061
                    'text': escapedPath, "href": url,
 
1062
                    'type': '[%s]' % mimetype,
 
1063
                    'encoding': (encoding and '[%s]' % encoding or ''),
 
1064
                    'size': formatFileSize(size)})
 
1065
        return dirs, files
 
1066
 
 
1067
 
 
1068
    def _buildTableContent(self, elements):
 
1069
        """
 
1070
        Build a table content using C{self.linePattern} and giving elements odd
 
1071
        and even classes.
 
1072
        """
 
1073
        tableContent = []
 
1074
        rowClasses = itertools.cycle(['odd', 'even'])
 
1075
        for element, rowClass in zip(elements, rowClasses):
 
1076
            element["class"] = rowClass
 
1077
            tableContent.append(self.linePattern % element)
 
1078
        return tableContent
 
1079
 
 
1080
 
 
1081
    def render(self, request):
 
1082
        """
 
1083
        Render a listing of the content of C{self.path}.
 
1084
        """
 
1085
        if self.dirs is None:
 
1086
            directory = os.listdir(self.path)
 
1087
            directory.sort()
 
1088
        else:
 
1089
            directory = self.dirs
 
1090
 
 
1091
        dirs, files = self._getFilesAndDirectories(directory)
 
1092
 
 
1093
        tableContent = "".join(self._buildTableContent(dirs + files))
 
1094
 
 
1095
        header = "Directory listing for %s" % (
 
1096
            cgi.escape(urllib.unquote(request.uri)),)
 
1097
 
 
1098
        return self.template % {"header": header, "tableContent": tableContent}
 
1099
 
 
1100
 
 
1101
    def __repr__(self):
 
1102
        return '<DirectoryLister of %r>' % self.path
 
1103
 
 
1104
    __str__ = __repr__