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