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