跳到内容

静态文件 - StaticFiles

你可以使用 StaticFiles 类来提供静态文件,例如 JavaScript、CSS、图片等。

阅读更多相关内容,请参阅 FastAPI 静态文件文档

你可以直接从 fastapi.staticfiles 导入它

from fastapi.staticfiles import StaticFiles

fastapi.staticfiles.StaticFiles

StaticFiles(
    *,
    directory=None,
    packages=None,
    html=False,
    check_dir=True,
    follow_symlink=False
)
源代码位于 starlette/staticfiles.py
def __init__(
    self,
    *,
    directory: PathLike | None = None,
    packages: list[str | tuple[str, str]] | None = None,
    html: bool = False,
    check_dir: bool = True,
    follow_symlink: bool = False,
) -> None:
    self.directory = directory
    self.packages = packages
    self.all_directories = self.get_directories(directory, packages)
    self.html = html
    self.config_checked = False
    self.follow_symlink = follow_symlink
    if check_dir and directory is not None and not os.path.isdir(directory):
        raise RuntimeError(f"Directory '{directory}' does not exist")

directory instance-attribute

directory = directory

packages instance-attribute

packages = packages

all_directories instance-attribute

all_directories = get_directories(directory, packages)

html instance-attribute

html = html

config_checked instance-attribute

config_checked = False
follow_symlink = follow_symlink

get_directories

get_directories(directory=None, packages=None)

根据 directorypackages 参数,返回用于提供静态文件的所有目录列表。

源代码位于 starlette/staticfiles.py
def get_directories(
    self,
    directory: PathLike | None = None,
    packages: list[str | tuple[str, str]] | None = None,
) -> list[PathLike]:
    """
    Given `directory` and `packages` arguments, return a list of all the
    directories that should be used for serving static files from.
    """
    directories = []
    if directory is not None:
        directories.append(directory)

    for package in packages or []:
        if isinstance(package, tuple):
            package, statics_dir = package
        else:
            statics_dir = "statics"
        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"Package {package!r} could not be found."
        package_directory = os.path.normpath(os.path.join(spec.origin, "..", statics_dir))
        assert os.path.isdir(package_directory), (
            f"Directory '{statics_dir!r}' in package {package!r} could not be found."
        )
        directories.append(package_directory)

    return directories

get_path

get_path(scope)

根据 ASGI 作用域,返回要提供服务的 path 字符串,其中包含操作系统特定的路径分隔符,并移除了任何“..”或“.”组件。

源代码位于 starlette/staticfiles.py
def get_path(self, scope: Scope) -> str:
    """
    Given the ASGI scope, return the `path` string to serve up,
    with OS specific path separators, and any '..', '.' components removed.
    """
    route_path = get_route_path(scope)
    return os.path.normpath(os.path.join(*route_path.split("/")))

get_response async

get_response(path, scope)

根据传入的路径、方法和请求头,返回 HTTP 响应。

源代码位于 starlette/staticfiles.py
async def get_response(self, path: str, scope: Scope) -> Response:
    """
    Returns an HTTP response, given the incoming path, method and request headers.
    """
    if scope["method"] not in ("GET", "HEAD"):
        raise HTTPException(status_code=405)

    try:
        full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, path)
    except PermissionError:
        raise HTTPException(status_code=401)
    except OSError as exc:
        # Filename is too long, so it can't be a valid static file.
        if exc.errno == errno.ENAMETOOLONG:
            raise HTTPException(status_code=404)

        raise exc

    if stat_result and stat.S_ISREG(stat_result.st_mode):
        # We have a static file to serve.
        return self.file_response(full_path, stat_result, scope)

    elif stat_result and stat.S_ISDIR(stat_result.st_mode) and self.html:
        # We're in HTML mode, and have got a directory URL.
        # Check if we have 'index.html' file to serve.
        index_path = os.path.join(path, "index.html")
        full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, index_path)
        if stat_result is not None and stat.S_ISREG(stat_result.st_mode):
            if not scope["path"].endswith("/"):
                # Directory URLs should redirect to always end in "/".
                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:
        # Check for '404.html' if we're in HTML mode.
        full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, "404.html")
        if stat_result and stat.S_ISREG(stat_result.st_mode):
            return FileResponse(full_path, stat_result=stat_result, status_code=404)
    raise HTTPException(status_code=404)

lookup_path

lookup_path(path)
源代码位于 starlette/staticfiles.py
def lookup_path(self, path: str) -> tuple[str, os.stat_result | None]:
    for directory in self.all_directories:
        joined_path = os.path.join(directory, path)
        if self.follow_symlink:
            full_path = os.path.abspath(joined_path)
            directory = os.path.abspath(directory)
        else:
            full_path = os.path.realpath(joined_path)
            directory = os.path.realpath(directory)
        if os.path.commonpath([full_path, directory]) != str(directory):
            # Don't allow misbehaving clients to break out of the static files directory.
            continue
        try:
            return full_path, os.stat(full_path)
        except (FileNotFoundError, NotADirectoryError):
            continue
    return "", None

file_response

file_response(
    full_path, stat_result, scope, status_code=200
)
源代码位于 starlette/staticfiles.py
def file_response(
    self,
    full_path: PathLike,
    stat_result: os.stat_result,
    scope: Scope,
    status_code: int = 200,
) -> Response:
    request_headers = Headers(scope=scope)

    response = FileResponse(full_path, status_code=status_code, stat_result=stat_result)
    if self.is_not_modified(response.headers, request_headers):
        return NotModifiedResponse(response.headers)
    return response

check_config async

check_config()

执行一次性配置检查,确保 StaticFiles 确实指向一个目录,以便我们可以抛出明确的错误,而不是仅仅返回 404 响应。

源代码位于 starlette/staticfiles.py
async def check_config(self) -> None:
    """
    Perform a one-off configuration check that StaticFiles is actually
    pointed at a directory, so that we can raise loud errors rather than
    just returning 404 responses.
    """
    if self.directory is None:
        return

    try:
        stat_result = await anyio.to_thread.run_sync(os.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.")

is_not_modified

is_not_modified(response_headers, request_headers)

根据请求和响应头,如果可以返回 HTTP “未修改 (Not Modified)” 响应,则返回 True

源代码位于 starlette/staticfiles.py
def is_not_modified(self, response_headers: Headers, request_headers: Headers) -> bool:
    """
    Given the request and response headers, return `True` if an HTTP
    "Not Modified" response could be returned instead.
    """
    if if_none_match := request_headers.get("if-none-match"):
        # The "etag" header is added by FileResponse, so it's always present.
        etag = response_headers["etag"]
        return etag in [tag.strip(" W/") for tag in if_none_match.split(",")]

    try:
        if_modified_since = parsedate(request_headers["if-modified-since"])
        last_modified = parsedate(response_headers["last-modified"])
        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