Index: dav/static.py =================================================================== --- dav/static.py (revision 15506) +++ dav/static.py (working copy) @@ -111,92 +111,9 @@ ## # HTTP - # - # FIXME: Most of these should be in IResource, Resource ## # - # ETag - # - - def etag(self): - """ - If the resource does not exist, returns None. - Otherwise, returns the etag for the resource. - """ - if not self.fp.exists(): return None - - st = self.fp.statinfo - - # - # Mark ETag as weak if it was modified more recently than we can - # measure and report, as it could be modified again in that span - # and we then wouldn't know to provide a new ETag. - # - weak = (time.time() - st.st_mtime <= 1) - - return ETag( - "%X-%X-%X" % (st.st_ino, st.st_size, st.st_mtime), - weak=weak - ) - - def exists(self): - """ - Returns True if the resource exists on the server, False otherwise. - """ - return self.fp.exists() - - def lastModified(self): - """ - If the resource does not exist, returns None. - Otherwise, returns the last modified time of the resource. - """ - if self.fp.exists(): - return self.fp.getmtime() - else: - return None - - def creationDate(self): - """ - If the resource does not exist, returns None. - Otherwise, returns the creation date of the resource. - """ - if self.fp.exists(): - return self.fp.getmtime() - else: - return None - - def contentLength(self): - """ - If the resource does not exist, returns None. - Otherwise, returns the size in bytes of the resource. - """ - if self.fp.exists(): - if self.fp.isdir(): - # Computing this would require rendering the resource; let's - # punt instead. - return None - return self.fp.getsize() - else: - return None - - def displayName(self): - """ - If the resource does not exist, returns None. - Otherwise, returns the display name the resource. - """ - if self.fp.exists(): - return self.fp.basename() - else: - return None - - def update(self): - """ - Update any cached information about the resource from its backing store. - """ - self.fp.restat(False) - - # # Preconditions # @@ -478,18 +395,11 @@ assert response, "No response" - def set_headers(response): + def setHeaders(response): response = IResponse(response) response.headers.setHeader("dav", self.davComplianceClasses) - for (header, value) in ( - ("last-modified", self.lastModified()), - ("etag" , self.etag() ), - ): - if value is not None: - response.headers.setHeader(header, value) - # # If this is a collection and the URI doesn't end in "/", add a # Content-Location header. This is needed even if we redirect such @@ -501,20 +411,9 @@ return response - response.addCallback(set_headers) + response.addCallback(setHeaders) return response - def http_OPTIONS(self, request): - def set_headers(response): - if self.type is not None: - response = IResponse(response) - response.headers.setHeader("content-type", MimeType.fromString(self.type)) - return response - - response = maybeDeferred(File.http_OPTIONS, self, request) - response.addCallback(set_headers) - return response - ## # Workarounds for issues with File ## Index: iweb.py =================================================================== --- iweb.py (revision 15506) +++ iweb.py (working copy) @@ -14,6 +14,56 @@ I am a web resource. """ + def allowedMethods(self): + """ + @return: A tuple of allowed methods. + """ + + def etag(self): + """ + @return: The current etag for the resource if available, None otherwise. + """ + + def exists(self): + """ + @return: True if the resource exists on the server, False otherwise. + """ + + def lastModified(self): + """ + @return: The last modified time of the resource if available, None otherwise. + """ + + def creationDate(self): + """ + @return: The creation date of the resource if available, None otherwise. + """ + + def contentLength(self): + """ + @return: The size in bytes of the resource if available, None otherwise. + """ + + def contentType(self): + """ + @return: The MIME type of the resource if available, None otherwise. + """ + + def contentEncoding(self): + """ + @return: The encoding of the resource if available, None otherwise. + """ + + def displayName(self): + """ + @return: The display name of the resource if available, None otherwise. + """ + + def update(self): + """ + Update any cached information about the resource from its backing store. + """ + def locateChild(self, req, segments): """Locate another object which can be adapted to IResource. Index: static.py =================================================================== --- static.py (revision 15506) +++ static.py (working copy) @@ -30,17 +30,18 @@ def __init__(self, data, type): self.data = data - self.type = type + self.type = MimeType.fromString(self.type) self.created_time = time.time() + def etag(self): + return http_headers.ETag("%X-%X" % (self.created_time, hash(self.data)), + weak=(time.time() - self.created_time <= 1)) + + def contentType(self): + return self.type + def render(self, req): - response = http.Response(responsecode.OK, stream=self.data) - response.headers.setRawHeaders("content-type", (self.type, )) - response.headers.setHeader('etag', - http_headers.ETag("%X-%X" % (self.created_time, hash(self.data)), - weak=(time.time() - self.created_time <= 1))) - - return response + return http.Response(responsecode.OK, stream=self.data) def addSlash(request): return "http%s://%s%s/" % ( @@ -127,30 +128,6 @@ ".bz2": "bzip2" } - def _initTypeAndEncoding(self): - self._type, self._encoding = getTypeAndEncoding( - self.fp.basename(), - self.contentTypes, - self.contentEncodings, - self.defaultType - ) - - # Handle cases not covered by getTypeAndEncoding() - if self.fp.isdir(): self._type = "httpd/unix-directory" - - def _getType(self): - if not hasattr(self, "_type"): - self._initTypeAndEncoding() - return self._type - - def _getEncoding(self): - if not hasattr(self, "_encoding"): - self._initTypeAndEncoding() - return self._encoding - - type = property(_getType ) - encoding = property(_getEncoding) - processors = {} indexNames = ["index", "index.html", "index.htm", "index.trp", "index.rpy"] @@ -174,6 +151,78 @@ if indexNames is not None: self.indexNames = indexNames + def etag(self): + if not self.exists(): return None + + st = self.fp.statinfo + + # + # Mark ETag as weak if it was modified more recently than we can + # measure and report, as it could be modified again in that span + # and we then wouldn't know to provide a new ETag. + # + weak = (time.time() - st.st_mtime <= 1) + + return http_headers.ETag( + "%X-%X-%X" % (st.st_ino, st.st_size, st.st_mtime), + weak=weak + ) + + def exists(self): + return self.fp.exists() + + def lastModified(self): + if self.fp.exists(): + return self.fp.getmtime() + else: + return None + + def creationDate(self): + if self.fp.exists(): + return self.fp.getmtime() + else: + return None + + def contentLength(self): + if self.fp.exists(): + if self.fp.isdir(): + # Computing this would require rendering the resource; let's + # punt instead. + return None + return self.fp.getsize() + else: + return None + + def _initTypeAndEncoding(self): + self._type, self._encoding = getTypeAndEncoding( + self.fp.basename(), + self.contentTypes, + self.contentEncodings, + self.defaultType + ) + + # Handle cases not covered by getTypeAndEncoding() + if self.fp.isdir(): self._type = "httpd/unix-directory" + + def contentType(self): + if not hasattr(self, "_type"): + self._initTypeAndEncoding() + return MimeType.fromString(self._type) + + def contentEncoding(self): + if not hasattr(self, "_encoding"): + self._initTypeAndEncoding() + return self._encoding + + def displayName(self): + if self.fp.exists(): + return self.fp.basename() + else: + return None + + def update(self): + self.fp.restat(False) + def ignoreExt(self, ext): """Ignore the given extension. @@ -230,7 +279,6 @@ def render(self, req): """You know what you doing.""" self.fp.restat() - response = http.Response() if not self.fp.exists(): return responsecode.NOT_FOUND @@ -241,11 +289,6 @@ responsecode.MOVED_PERMANENTLY, {'location': req.unparseURL(path=req.path+'/')}) - if self.type: - response.headers.setRawHeaders('content-type', (self.type,)) - if self.encoding: - response.headers.setHeader('content-encoding', self.encoding) - try: f = self.fp.open() except IOError, e: @@ -255,27 +298,16 @@ else: raise - st = os.fstat(f.fileno()) - - # Be sure it's a regular file. - if not stat.S_ISREG(st.st_mode): - return responsecode.FORBIDDEN - - #for content-length - size = st.st_size - - response.headers.setHeader('last-modified', st.st_mtime) - - # Mark ETag as weak if it was modified recently, as it could - # be modified again without changing mtime. - weak = (time.time() - st.st_mtime <= 1) - - etag = http_headers.ETag( - "%X-%X-%X" % (st.st_ino, st.st_size, st.st_mtime), - weak=weak) - - response.headers.setHeader('etag', etag) - response.headers.setHeader('content-length', size) + response = http.Response() + + for (header, value) in ( + ("content-length", self.contentLength()), + ("content-type", self.contentType()), + ("content-encoding", self.contentType()), + ): + if value is not None: + response.headers.setHeader(header, value) + response.stream = stream.FileStream(f, 0, size) return response Index: resource.py =================================================================== --- resource.py (revision 15506) +++ resource.py (working copy) @@ -8,6 +8,7 @@ # System Imports from twisted.python import components +from twisted.internet.defer import succeed, maybeDeferred from zope.interface import implements from twisted.web2 import iweb, http, server, responsecode @@ -23,10 +24,71 @@ implements(iweb.IResource) addSlash = False - allowedMethods = None # Concrete HTTP interface + def allowedMethods(self): + """ + @return: A tuple of allowed methods. + """ + if not hasattr(self, "_allowed_methods"): + self._allowed_methods = tuple([name[5:] for name in dir(self) if name.startswith('http_')]) + return self._allowed_methods + + def etag(self): + """ + @return: The current etag for the resource if available, None otherwise. + """ + return None + + def exists(self): + """ + @return: True if the resource exists on the server, False otherwise. + """ + return None + + def lastModified(self): + """ + @return: The last modified time of the resource if available, None otherwise. + """ + return None + + def creationDate(self): + """ + @return: The creation date of the resource if available, None otherwise. + """ + return None + + def contentLength(self): + """ + @return: The size in bytes of the resource if available, None otherwise. + """ + return None + + def contentType(self): + """ + @return: The MIME type of the resource if available, None otherwise. + """ + return None + + def contentEncoding(self): + """ + @return: The encoding of the resource if available, None otherwise. + """ + return None + + def displayName(self): + """ + @return: The display name of the resource if available, None otherwise. + """ + return None + + def update(self): + """ + Update any cached information about the resource from its backing store. + """ + return None + def locateChild(self, req, segments): """Return a tuple of (child, segments) representing the Resource below this after consuming the segments which are not returned. @@ -63,7 +125,6 @@ path to be ''. """ setattr(self, 'child_%s' % (path, ), child) - def renderHTTP(self, req): """Render a given resource. See L{IResource}'s render method. @@ -78,18 +139,29 @@ will be the rendered page, or else a deferred that results in a byte string. """ - m = getattr(self, 'http_' + req.method, None) - if not m: - if self.allowedMethods is None: - # Update allowed methods - self.allowedMethods = [name[5:] for name in dir(self) - if name.startswith('http_')] - + method = getattr(self, 'http_' + req.method, None) + if not method: response = http.Response(responsecode.NOT_ALLOWED) - response.headers.setHeader('allow', self.allowedMethods) + response.headers.setHeader("allow", self.allowedMethods()) return response - return m(req) + def setHeaders(response): + response = iweb.IResponse(response) + + # Content-* headers refer to the response content, not (necessarily) to + # the resource content, so then depend on method, and therefore can't be + # set here. + for (header, value) in ( + ("etag", self.etag()), + ("last-modified", self.lastModified()), + ): + if value is not None: + response.headers.setHeader(header, value) + + return response + + return maybeDeferred(method, req).addCallback(setHeaders) + def http_HEAD(self, req): """By default http_HEAD just calls http_GET. The body is discarded when the result is being written. @@ -112,12 +184,8 @@ def http_OPTIONS(self, req): """Sends back OPTIONS response.""" - if self.allowedMethods is None: - # Update allowed methods - self.allowedMethods = [name[5:] for name in dir(self) - if name.startswith('http_')] response = http.Response(responsecode.OK) - response.headers.setHeader('allow', self.allowedMethods) + response.headers.setHeader("allow", self.allowedMethods()) return response def http_TRACE(self, req): @@ -127,6 +195,7 @@ """Your class should implement this method to do default page rendering. """ raise NotImplementedError("Subclass must implement render method.") + components.backwardsCompatImplements(Resource) class PostableResource(Resource):