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的源码解读

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341