Starlette 源码阅读 (十三) 静态资源

staticfiles.py

用于服务器静态资源的管理,其本身也是个注册在路由上的app
当输入example.com/static/img/a.jpg
Mount作为子路由会截出img/a.jpg
输入到app中,从而到文件目录中寻找

官方示例
routes=[
    Mount(
        '/static', 
        app=StaticFiles(directory='static', packages=['bootstrap4']), 
        name="static"
    ),
]
app = Starlette(routes=routes)

StaticFiles类

class StaticFiles:

    def __init__(
        self,
        *,
        directory: str = None,
        packages: typing.List[str] = None,
        html: bool = False,
        check_dir: bool = True,
    ) -> None:
        """
        :param directory:    表示目录路径的字符串
        :param packages:     python包的字符串列表
        :param html:         以HTML模式运行。如果存在index.html,则自动为目录加载。
        :param check_dir:    确保目录在实例化时存在。默认为True
        """
        self.directory = directory
        self.packages = packages
        self.all_directories = self.get_directories(directory, packages)
        self.html = html
        self.config_checked = False
        if check_dir and directory is not None and not os.path.isdir(directory):
            raise RuntimeError(f"Directory '{directory}' does not exist")

    def get_directories(
        self, directory: str = None, packages: typing.List[str] = None
    ) -> typing.List[str]:
        """
        给定' directory '和' packages '参数,返回应该用于提供静态文件的所有目录的列表。
        """
        directories = []
        if directory is not None:
            directories.append(directory)

        for package in packages or []:
            spec = importlib.util.find_spec(package)
            assert spec is not None, f"Package {package!r} could not be found."
            assert (
                spec.origin is not None
            ), f"Directory 'statics' in package {package!r} could not be found."
            directory = os.path.normpath(os.path.join(spec.origin, "..", "statics"))
            assert os.path.isdir(
                directory
            ), f"Directory 'statics' in package {package!r} could not be found."
            directories.append(directory)
        # 合成一个路径列表
        return directories

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        """
        ASGI入口点.
        """
        assert scope["type"] == "http"

        if not self.config_checked:
            await self.check_config()
            self.config_checked = True
        # 检查配置

        path = self.get_path(scope)
        # 从scope中获取path路径
        response = await self.get_response(path, scope)
        # 获取文件,以response方式
        await response(scope, receive, send)

    def get_path(self, scope: Scope) -> str:
        """
        给定ASGI作用域,返回要提供的“path”字符串,
        带有操作系统特定的路径分离器,并删除所有 '..', '.' 组件.
        """
        return os.path.normpath(os.path.join(*scope["path"].split("/")))

    async def get_response(self, path: str, scope: Scope) -> Response:
        """
        给定传入路径、方法和请求头,返回 HTTP response。
        """
        if scope["method"] not in ("GET", "HEAD"):
            return PlainTextResponse("Method Not Allowed", status_code=405)
        # 判断方法
        full_path, stat_result = await self.lookup_path(path)
        # 寻找与path路径对应的静态资源
        if stat_result and stat.S_ISREG(stat_result.st_mode):
            # 指定某个文件
            return self.file_response(full_path, stat_result, scope)

        elif stat_result and stat.S_ISDIR(stat_result.st_mode) and self.html:
            # 我们处于HTML模式,并有一个目录URL
            # 指定某个目录,在其中寻找index.html
            index_path = os.path.join(path, "index.html")
            full_path, stat_result = await self.lookup_path(index_path)
            # 寻找index.html
            if stat_result is not None and stat.S_ISREG(stat_result.st_mode):
                # 如果找到了,
                if not scope["path"].endswith("/"):
                    # 目录url应该重定向到始终以“/”结尾.
                    url = URL(scope=scope)
                    url = url.replace(path=url.path + "/")
                    return RedirectResponse(url=url)
                return self.file_response(full_path, stat_result, scope)

        if self.html:
            # 处于html模式下,找不到index.html
            # 寻找404.html
            full_path, stat_result = await self.lookup_path("404.html")
            if stat_result is not None and stat.S_ISREG(stat_result.st_mode):
                return self.file_response(
                    full_path, stat_result, scope, status_code=404
                )
        # 没有能给你的
        return PlainTextResponse("Not Found", status_code=404)

    async def lookup_path(
        self, path: str
    ) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
        for directory in self.all_directories:
            # 从所有路径中查找
            full_path = os.path.realpath(os.path.join(directory, path))
            directory = os.path.realpath(directory)
            # 静态文件夹路径 + http的path
            if os.path.commonprefix([full_path, directory]) != directory:
                # 不要允许行为不端的客户端破坏静态文件目录.
                continue
            # 完成路径拼接
            try:
                stat_result = await aio_stat(full_path)
                # 尝试按照这个路径是否能找到资源
                # 如果找到就返回
                return (full_path, stat_result)
            except FileNotFoundError:
                pass
        return ("", None)

    def file_response(
        self,
        full_path: str,
        stat_result: os.stat_result,
        scope: Scope,
        status_code: int = 200,
    ) -> Response:
        method = scope["method"]
        request_headers = Headers(scope=scope)

        response = FileResponse(
            full_path, status_code=status_code, stat_result=stat_result, method=method
        )
        if self.is_not_modified(response.headers, request_headers):
            return NotModifiedResponse(response.headers)
        #   判断是否为最新资源
        return response

    async def check_config(self) -> None:
        """
        执行一次性的配置检查,使静态文件实际上指向一个目录,
        这样我们就可以触发明确的错误,而不是仅仅返回404响应。.
        """
        if self.directory is None:
            return

        try:
            stat_result = await aio_stat(self.directory)
            # 检查路径是否存在
        except FileNotFoundError:
            raise RuntimeError(
                f"StaticFiles directory '{self.directory}' does not exist."
            )
        if not (stat.S_ISDIR(stat_result.st_mode) or stat.S_ISLNK(stat_result.st_mode)):
            raise RuntimeError(
                f"StaticFiles path '{self.directory}' is not a directory."
            )

    def is_not_modified(
        self, response_headers: Headers, request_headers: Headers
    ) -> bool:
        """
        判断浏览器中的cache是否为最新的,http的headers有关于Modified的记录。
        这里记录了浏览器中资源最后修改的时间戳.
        当其最后修改时间大于服务器资源的最后修改时间
        则代表不需要更新
        """
        try:
            if_none_match = request_headers["if-none-match"]
            etag = response_headers["etag"]
            if if_none_match == etag:
                return True
        except KeyError:
            pass

        try:
            if_modified_since = parsedate(request_headers["if-modified-since"])
            last_modified = parsedate(response_headers["last-modified"])
            # request代表浏览器中资源信息
            # response代表服务器中资源信息
            if (
                if_modified_since is not None
                and last_modified is not None
                and if_modified_since >= last_modified
            ):
                return True
        except KeyError:
            pass

        return False

NotModifiedResponse类

class NotModifiedResponse(Response):
    NOT_MODIFIED_HEADERS = (
        "cache-control",
        "content-location",
        "date",
        "etag",
        "expires",
        "vary",
    )
    def __init__(self, headers: Headers):
        super().__init__(
            status_code=304,
            headers={
                name: value
                for name, value in headers.items()
                if name in self.NOT_MODIFIED_HEADERS
            },
        )

这个模板带给我的收货,并不是其实现原理,而是我对app的理解更加深入了一步

何为app?

只要能接受元数据(scope, send, receive),或处理后传给其他app,或者自己产生response。都可以叫做app
像starlette实例,路由器,中间件,路由节点,cbv,endpoint闭包,本质都是各种各样的app。
他们都具有共同的特点,接受元数据,传给其他app,或者产生response。
只是在设计上,有序的进行了分工安排,就好比人体的细胞一样。大家同样都是由干细胞发育而来。但是各司其职。他们都具有细胞的基本特性。

starlette的主要内容,再有三章左右便可完成。各种各样的中间件属于插件内容。种类繁多,而且和框架有关的部分逻辑都是互通的。所以不做过多解读,可以单独出一章来简单介绍下他们的功能。

后续将对uvicorn的核心代码进行解读,重点是围绕scope,send,receive三者。

两者完成后,将正式开启fastapi的源码解读

你可能感兴趣的:(Starlette 源码阅读 (十三) 静态资源)