1
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
2
# See LICENSE for details.
6
I deal with static resources.
15
from twisted.web2 import http_headers, resource
16
from twisted.web2 import http, iweb, stream, responsecode, server, dirlist
19
from twisted.python import filepath
20
from twisted.internet.defer import maybeDeferred
21
from zope.interface import implements
23
class MetaDataMixin(object):
25
Mix-in class for L{iweb.IResource} which provides methods for accessing resource
26
metadata specified by HTTP.
30
@return: The current etag for the resource if available, None otherwise.
34
def lastModified(self):
36
@return: The last modified time of the resource if available, None otherwise.
40
def creationDate(self):
42
@return: The creation date of the resource if available, None otherwise.
46
def contentLength(self):
48
@return: The size in bytes of the resource if available, None otherwise.
52
def contentType(self):
54
@return: The MIME type of the resource if available, None otherwise.
58
def contentEncoding(self):
60
@return: The encoding of the resource if available, None otherwise.
64
def displayName(self):
66
@return: The display name of the resource if available, None otherwise.
72
@return: True if the resource exists on the server, False otherwise.
76
class StaticRenderMixin(resource.RenderMixin, MetaDataMixin):
77
def checkPreconditions(self, request):
78
# This code replaces the code in resource.RenderMixin
79
if request.method not in ("GET", "HEAD"):
80
http.checkPreconditions(
82
entityExists = self.exists(),
84
lastModified = self.lastModified(),
87
# Check per-method preconditions
88
method = getattr(self, "preconditions_" + request.method, None)
90
return method(request)
92
def renderHTTP(self, request):
94
See L{resource.RenderMixIn.renderHTTP}.
96
This implementation automatically sets some headers on the response
97
based on data available from L{MetaDataMixin} methods.
99
def setHeaders(response):
100
response = iweb.IResponse(response)
102
# Don't provide additional resource information to error responses
103
if response.code < 400:
104
# Content-* headers refer to the response content, not
105
# (necessarily) to the resource content, so they depend on the
106
# request method, and therefore can't be set here.
107
for (header, value) in (
108
("etag", self.etag()),
109
("last-modified", self.lastModified()),
111
if value is not None:
112
response.headers.setHeader(header, value)
117
# If we get an HTTPError, run its response through setHeaders() as
119
f.trap(http.HTTPError)
120
return setHeaders(f.value.response)
122
d = maybeDeferred(super(StaticRenderMixin, self).renderHTTP, request)
123
return d.addCallbacks(setHeaders, onError)
125
class Data(resource.Resource):
127
This is a static, in-memory resource.
129
def __init__(self, data, type):
131
self.type = http_headers.MimeType.fromString(type)
132
self.created_time = time.time()
135
lastModified = self.lastModified()
136
return http_headers.ETag("%X-%X" % (lastModified, hash(self.data)),
137
weak=(time.time() - lastModified <= 1))
139
def lastModified(self):
140
return self.creationDate()
142
def creationDate(self):
143
return self.created_time
145
def contentLength(self):
146
return len(self.data)
148
def contentType(self):
151
def render(self, req):
152
return http.Response(
154
http_headers.Headers({'content-type': self.contentType()}),
158
class File(StaticRenderMixin):
160
File is a resource that represents a plain non-interpreted file
161
(although it can look for an extension like .rpy or .cgi and hand the
162
file to a processor for interpretation if you wish). Its constructor
165
Alternatively, you can give a directory path to the constructor. In this
166
case the resource will represent that directory, and its children will
167
be files underneath that directory. This provides access to an entire
168
filesystem tree with a single Resource.
170
If you map the URL 'http://server/FILE' to a resource created as
171
File('/tmp'), then http://server/FILE/ will return an HTML-formatted
172
listing of the /tmp/ directory, and http://server/FILE/foo/bar.html will
173
return the contents of /tmp/foo/bar.html .
175
implements(iweb.IResource)
177
def _getContentTypes(self):
178
if not hasattr(File, "_sharedContentTypes"):
179
File._sharedContentTypes = loadMimeTypes()
180
return File._sharedContentTypes
182
contentTypes = property(_getContentTypes)
191
indexNames = ["index", "index.html", "index.htm", "index.trp", "index.rpy"]
195
def __init__(self, path, defaultType="text/plain", ignoredExts=(), processors=None, indexNames=None):
196
"""Create a file with the given path.
198
super(File, self).__init__()
200
self.putChildren = {}
201
self.fp = filepath.FilePath(path)
202
# Remove the dots from the path to split
203
self.defaultType = defaultType
204
self.ignoredExts = list(ignoredExts)
205
if processors is not None:
206
self.processors = dict([
208
for key, value in processors.items()
211
if indexNames is not None:
212
self.indexNames = indexNames
215
return self.fp.exists()
218
if not self.fp.exists(): return None
220
st = self.fp.statinfo
223
# Mark ETag as weak if it was modified more recently than we can
224
# measure and report, as it could be modified again in that span
225
# and we then wouldn't know to provide a new ETag.
227
weak = (time.time() - st.st_mtime <= 1)
229
return http_headers.ETag(
230
"%X-%X-%X" % (st.st_ino, st.st_size, st.st_mtime),
234
def lastModified(self):
236
return self.fp.getmtime()
240
def creationDate(self):
242
return self.fp.getmtime()
246
def contentLength(self):
249
return self.fp.getsize()
251
# Computing this would require rendering the resource; let's
257
def _initTypeAndEncoding(self):
258
self._type, self._encoding = getTypeAndEncoding(
261
self.contentEncodings,
265
# Handle cases not covered by getTypeAndEncoding()
266
if self.fp.isdir(): self._type = "httpd/unix-directory"
268
def contentType(self):
269
if not hasattr(self, "_type"):
270
self._initTypeAndEncoding()
271
return http_headers.MimeType.fromString(self._type)
273
def contentEncoding(self):
274
if not hasattr(self, "_encoding"):
275
self._initTypeAndEncoding()
276
return self._encoding
278
def displayName(self):
280
return self.fp.basename()
284
def ignoreExt(self, ext):
285
"""Ignore the given extension.
287
Serve file.ext if file is requested
289
self.ignoredExts.append(ext)
291
def directoryListing(self):
292
return dirlist.DirectoryLister(self.fp.path,
295
self.contentEncodings,
298
def putChild(self, name, child):
300
Register a child with the given name with this resource.
301
@param name: the name of the child (a URI path segment)
302
@param child: the child to register
304
self.putChildren[name] = child
306
def getChild(self, name):
308
Look up a child resource.
309
@return: the child of this resource with the given name.
314
child = self.putChildren.get(name, None)
315
if child: return child
317
child_fp = self.fp.child(name)
318
if child_fp.exists():
319
return self.createSimilarFile(child_fp.path)
323
def listChildren(self):
325
@return: a sequence of the names of all known children of this resource.
327
children = self.putChildren.keys()
329
children += [c for c in self.fp.listdir() if c not in children]
332
def locateChild(self, req, segments):
334
See L{IResource}C{.locateChild}.
336
# If getChild() finds a child resource, return it
337
child = self.getChild(segments[0])
338
if child is not None: return (child, segments[1:])
340
# If we're not backed by a directory, we have no children.
341
# But check for existance first; we might be a collection resource
342
# that the request wants created.
343
self.fp.restat(False)
344
if self.fp.exists() and not self.fp.isdir(): return (None, ())
346
# OK, we need to return a child corresponding to the first segment
350
fpath = self.fp.child(path)
352
# Request is for a directory (collection) resource
353
return (self, server.StopTraversal)
355
# Don't run processors on directories - if someone wants their own
356
# customized directory rendering, subclass File instead.
358
processor = self.processors.get(fpath.splitext()[1].lower())
361
processor(fpath.path),
364
elif not fpath.exists():
365
sibling_fpath = fpath.siblingExtensionSearch(*self.ignoredExts)
366
if sibling_fpath is not None:
367
fpath = sibling_fpath
369
return self.createSimilarFile(fpath.path), segments[1:]
371
def renderHTTP(self, req):
372
self.fp.restat(False)
373
return super(File, self).renderHTTP(req)
375
def render(self, req):
376
"""You know what you doing."""
377
if not self.fp.exists():
378
return responsecode.NOT_FOUND
381
if req.uri[-1] != "/":
382
# Redirect to include trailing '/' in URI
383
return http.RedirectResponse(req.unparseURL(path=req.path+'/'))
385
ifp = self.fp.childSearchPreauth(*self.indexNames)
387
# Render from the index file
388
standin = self.createSimilarFile(ifp.path)
390
# Render from a DirectoryLister
391
standin = dirlist.DirectoryLister(
395
self.contentEncodings,
398
return standin.render(req)
404
if e[0] == errno.EACCES:
405
return responsecode.FORBIDDEN
406
elif e[0] == errno.ENOENT:
407
return responsecode.NOT_FOUND
411
response = http.Response()
412
response.stream = stream.FileStream(f, 0, self.fp.getsize())
414
for (header, value) in (
415
("content-type", self.contentType()),
416
("content-encoding", self.contentEncoding()),
418
if value is not None:
419
response.headers.setHeader(header, value)
423
def createSimilarFile(self, path):
424
return self.__class__(path, self.defaultType, self.ignoredExts,
425
self.processors, self.indexNames[:])
428
class FileSaver(resource.PostableResource):
429
allowedTypes = (http_headers.MimeType('text', 'plain'),
430
http_headers.MimeType('text', 'html'),
431
http_headers.MimeType('text', 'css'))
433
def __init__(self, destination, expectedFields=[], allowedTypes=None, maxBytes=1000000, permissions=0644):
434
self.destination = destination
435
self.allowedTypes = allowedTypes or self.allowedTypes
436
self.maxBytes = maxBytes
437
self.expectedFields = expectedFields
438
self.permissions = permissions
440
def makeUniqueName(self, filename):
441
"""Called when a unique filename is needed.
443
filename is the name of the file as given by the client.
445
Returns the fully qualified path of the file to create. The
446
file must not yet exist.
449
return tempfile.mktemp(suffix=os.path.splitext(filename)[1], dir=self.destination)
451
def isSafeToWrite(self, filename, mimetype, filestream):
452
"""Returns True if it's "safe" to write this file,
453
otherwise it raises an exception.
456
if filestream.length > self.maxBytes:
457
raise IOError("%s: File exceeds maximum length (%d > %d)" % (filename,
461
if mimetype not in self.allowedTypes:
462
raise IOError("%s: File type not allowed %s" % (filename, mimetype))
466
def writeFile(self, filename, mimetype, fileobject):
467
"""Does the I/O dirty work after it calls isWriteable to make
468
sure it's safe to write this file.
470
filestream = stream.FileStream(fileobject)
472
if self.isSafeToWrite(filename, mimetype, filestream):
473
outname = self.makeUniqueName(filename)
475
fileobject = os.fdopen(os.open(outname, os.O_WRONLY | os.O_CREAT | os.O_EXCL,
476
self.permissions), 'w', 0)
478
stream.readIntoFile(filestream, fileobject)
482
def render(self, req):
483
content = ["<html><body>"]
486
for fieldName in req.files:
487
if fieldName in self.expectedFields:
488
for finfo in req.files[fieldName]:
490
outname = self.writeFile(*finfo)
491
content.append("Saved file %s<br />" % outname)
493
content.append(str(err) + "<br />")
495
content.append("%s is not a valid field" % fieldName)
498
content.append("No files given")
500
content.append("</body></html>")
502
return http.Response(responsecode.OK, {}, stream='\n'.join(content))
505
# FIXME: hi there I am a broken class
506
# """I contain AsIsProcessor, which serves files 'As Is'
507
# Inspired by Apache's mod_asis
510
# class ASISProcessor:
511
# implements(iweb.IResource)
513
# def __init__(self, path):
516
# def renderHTTP(self, request):
517
# request.startedWriting = 1
518
# return File(self.path)
520
# def locateChild(self, request):
527
dangerousPathError = http.HTTPError(responsecode.NOT_FOUND) #"Invalid request URL."
529
def isDangerous(path):
530
return path == '..' or '/' in path or os.sep in path
532
def addSlash(request):
533
return "http%s://%s%s/" % (
534
request.isSecure() and 's' or '',
535
request.getHeader("host"),
536
(request.uri.split('?')[0]))
538
def loadMimeTypes(mimetype_locations=['/etc/mime.types']):
540
Multiple file locations containing mime-types can be passed as a list.
541
The files will be sourced in that order, overriding mime-types from the
542
files sourced beforehand, but only if a new entry explicitly overrides
546
# Grab Python's built-in mimetypes dictionary.
547
contentTypes = mimetypes.types_map
548
# Update Python's semi-erroneous dictionary with a few of the
552
'.conf': 'text/plain',
553
'.diff': 'text/plain',
554
'.exe': 'application/x-executable',
555
'.flac': 'audio/x-flac',
556
'.java': 'text/plain',
557
'.ogg': 'application/ogg',
559
'.swf': 'application/x-shockwave-flash',
560
'.tgz': 'application/x-gtar',
561
'.wml': 'text/vnd.wap.wml',
562
'.xul': 'application/vnd.mozilla.xul+xml',
564
'.patch': 'text/plain',
567
# Users can override these mime-types by loading them out configuration
568
# files (this defaults to ['/etc/mime.types']).
569
for location in mimetype_locations:
570
if os.path.exists(location):
571
contentTypes.update(mimetypes.read_mime_types(location))
575
def getTypeAndEncoding(filename, types, encodings, defaultType):
576
p, ext = os.path.splitext(filename)
578
if encodings.has_key(ext):
580
ext = os.path.splitext(p)[1].lower()
583
type = types.get(ext, defaultType)
590
if __name__ == '__builtin__':
591
# Running from twistd -y
592
from twisted.application import service, strports
593
from twisted.web2 import server
595
application = service.Application("demo")
596
s = strports.service('8080', server.Site(res))
597
s.setServiceParent(application)