import os.path import typing from wsgitools.internal import Environ, HeaderList, StartResponse __all__ = [] __all__.append("StaticContent") class StaticContent: """ This wsgi application provides static content on whatever request it receives with method GET or HEAD (content stripped). If not present, a content-length header is computed. """ content: typing.Iterable[bytes] def __init__( self, status: str, headers: HeaderList, content: typing.Union[bytes, typing.Iterable[bytes]], anymethod: bool = False, ): """ @param status: is the HTTP status returned to the browser (ex: "200 OK") @param headers: is a list of C{(header, value)} pairs being delivered as HTTP headers @param content: contains the data to be delivered to the client. It is either a string or some kind of iterable yielding strings. @param anymethod: determines whether any request method should be answered with this response instead of a 501 """ assert isinstance(status, str) assert isinstance(headers, list) assert isinstance(content, bytes) or hasattr(content, "__iter__") self.status = status self.headers = headers self.anymethod = anymethod length = -1 if isinstance(content, bytes): self.content = [content] length = len(content) else: self.content = content if isinstance(self.content, list): length = sum(map(len, self.content)) if length >= 0: if not [v for h, v in headers if h.lower() == "content-length"]: headers.append(("Content-length", str(length))) def __call__( self, environ: Environ, start_response: StartResponse ) -> typing.Iterable[bytes]: """wsgi interface""" assert isinstance(environ, dict) if environ["REQUEST_METHOD"].upper() not in ["GET", "HEAD"] and \ not self.anymethod: resp = b"Request method not implemented" start_response("501 Not Implemented", [("Content-length", str(len(resp)))]) return [resp] start_response(self.status, list(self.headers)) if environ["REQUEST_METHOD"].upper() == "HEAD": return [] return self.content __all__.append("StaticFile") class StaticFile: """ This wsgi application provides the content of a static file on whatever request it receives with method GET or HEAD (content stripped). If not present, a content-length header is computed. """ def __init__( self, filelike: typing.Union[str, typing.BinaryIO], status: str = "200 OK", headers: HeaderList = list(), blocksize: int = 4096, ): """ @param status: is the HTTP status returned to the browser @param headers: is a list of C{(header, value)} pairs being delivered as HTTP headers @param filelike: may either be an path in the local file system or a file-like that must support C{read(size)} and C{seek(0)}. If C{tell()} is present, C{seek(0, 2)} and C{tell()} will be used to compute the content-length. @param blocksize: the content is provided in chunks of this size """ self.filelike = filelike self.status = status self.headers = headers self.blocksize = blocksize def _serve_in_chunks( self, stream: typing.BinaryIO ) -> typing.Iterator[bytes]: """internal method yielding data from the given stream""" while True: data = stream.read(self.blocksize) if not data: break yield data if isinstance(self.filelike, str): stream.close() def __call__( self, environ: Environ, start_response: StartResponse ) -> typing.Iterable[bytes]: """wsgi interface""" assert isinstance(environ, dict) if environ["REQUEST_METHOD"].upper() not in ["GET", "HEAD"]: resp = b"Request method not implemented" start_response("501 Not Implemented", [("Content-length", str(len(resp)))]) return [resp] stream: typing.Optional[typing.BinaryIO] = None size = -1 try: if isinstance(self.filelike, str): # raises IOError stream = open(self.filelike, "rb") size = os.path.getsize(self.filelike) else: stream = self.filelike if hasattr(stream, "tell"): stream.seek(0, 2) size = stream.tell() stream.seek(0) except IOError: resp = b"File not found" start_response("404 File not found", [("Content-length", str(len(resp)))]) return [resp] headers = list(self.headers) if size >= 0: if not [v for h, v in headers if h.lower() == "content-length"]: headers.append(("Content-length", str(size))) start_response(self.status, headers) if environ["REQUEST_METHOD"].upper() == "HEAD": if isinstance(self.filelike, str): stream.close() return [] if isinstance(self.filelike, str) and 'wsgi.file_wrapper' in environ: return environ['wsgi.file_wrapper'](stream, self.blocksize) if 0 <= size <= self.blocksize: data = stream.read(size) if isinstance(self.filelike, str): stream.close() return [data] return self._serve_in_chunks(stream)