core/homeassistant/components/http/static.py

72 lines
2.4 KiB
Python

"""Static file handling for HTTP component."""
from __future__ import annotations
from collections.abc import Mapping
from pathlib import Path
from typing import Final
from aiohttp import hdrs
from aiohttp.web import FileResponse, Request, StreamResponse
from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound
from aiohttp.web_urldispatcher import StaticResource
from lru import LRU # pylint: disable=no-name-in-module
from homeassistant.core import HomeAssistant
from .const import KEY_HASS
CACHE_TIME: Final = 31 * 86400 # = 1 month
CACHE_HEADERS: Final[Mapping[str, str]] = {
hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}"
}
PATH_CACHE = LRU(512)
def _get_file_path(
filename: str | Path, directory: Path, follow_symlinks: bool
) -> Path | None:
filepath = directory.joinpath(filename).resolve()
if not follow_symlinks:
filepath.relative_to(directory)
# on opening a dir, load its contents if allowed
if filepath.is_dir():
return None
if filepath.is_file():
return filepath
raise FileNotFoundError
class CachingStaticResource(StaticResource):
"""Static Resource handler that will add cache headers."""
async def _handle(self, request: Request) -> StreamResponse:
rel_url = request.match_info["filename"]
hass: HomeAssistant = request.app[KEY_HASS]
filename = Path(rel_url)
if filename.anchor:
# rel_url is an absolute name like
# /static/\\machine_name\c$ or /static/D:\path
# where the static dir is totally different
raise HTTPForbidden()
try:
key = (filename, self._directory, self._follow_symlinks)
if (filepath := PATH_CACHE.get(key)) is None:
filepath = PATH_CACHE[key] = await hass.async_add_executor_job(
_get_file_path, filename, self._directory, self._follow_symlinks
)
except (ValueError, FileNotFoundError) as error:
# relatively safe
raise HTTPNotFound() from error
except Exception as error:
# perm error or other kind!
request.app.logger.exception(error)
raise HTTPNotFound() from error
if filepath:
return FileResponse(
filepath,
chunk_size=self._chunk_size,
headers=CACHE_HEADERS,
)
return await super()._handle(request)