Resolve and caches paths for CachingStaticResource in the executor (#74474)
This commit is contained in:
parent
113ccfe6af
commit
332cf3cd2d
7 changed files with 69 additions and 21 deletions
|
@ -9,11 +9,31 @@ from aiohttp import hdrs
|
||||||
from aiohttp.web import FileResponse, Request, StreamResponse
|
from aiohttp.web import FileResponse, Request, StreamResponse
|
||||||
from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound
|
from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound
|
||||||
from aiohttp.web_urldispatcher import StaticResource
|
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_TIME: Final = 31 * 86400 # = 1 month
|
||||||
CACHE_HEADERS: Final[Mapping[str, str]] = {
|
CACHE_HEADERS: Final[Mapping[str, str]] = {
|
||||||
hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}"
|
hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}"
|
||||||
}
|
}
|
||||||
|
PATH_CACHE = LRU(512)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_file_path(
|
||||||
|
filename: str, 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 HTTPNotFound
|
||||||
|
|
||||||
|
|
||||||
class CachingStaticResource(StaticResource):
|
class CachingStaticResource(StaticResource):
|
||||||
|
@ -21,16 +41,19 @@ class CachingStaticResource(StaticResource):
|
||||||
|
|
||||||
async def _handle(self, request: Request) -> StreamResponse:
|
async def _handle(self, request: Request) -> StreamResponse:
|
||||||
rel_url = request.match_info["filename"]
|
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:
|
try:
|
||||||
filename = Path(rel_url)
|
key = (filename, self._directory, self._follow_symlinks)
|
||||||
if filename.anchor:
|
if (filepath := PATH_CACHE.get(key)) is None:
|
||||||
# rel_url is an absolute name like
|
filepath = PATH_CACHE[key] = await hass.async_add_executor_job(
|
||||||
# /static/\\machine_name\c$ or /static/D:\path
|
_get_file_path, filename, self._directory, self._follow_symlinks
|
||||||
# where the static dir is totally different
|
)
|
||||||
raise HTTPForbidden()
|
|
||||||
filepath = self._directory.joinpath(filename).resolve()
|
|
||||||
if not self._follow_symlinks:
|
|
||||||
filepath.relative_to(self._directory)
|
|
||||||
except (ValueError, FileNotFoundError) as error:
|
except (ValueError, FileNotFoundError) as error:
|
||||||
# relatively safe
|
# relatively safe
|
||||||
raise HTTPNotFound() from error
|
raise HTTPNotFound() from error
|
||||||
|
@ -39,13 +62,10 @@ class CachingStaticResource(StaticResource):
|
||||||
request.app.logger.exception(error)
|
request.app.logger.exception(error)
|
||||||
raise HTTPNotFound() from error
|
raise HTTPNotFound() from error
|
||||||
|
|
||||||
# on opening a dir, load its contents if allowed
|
if filepath:
|
||||||
if filepath.is_dir():
|
|
||||||
return await super()._handle(request)
|
|
||||||
if filepath.is_file():
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
filepath,
|
filepath,
|
||||||
chunk_size=self._chunk_size,
|
chunk_size=self._chunk_size,
|
||||||
headers=CACHE_HEADERS,
|
headers=CACHE_HEADERS,
|
||||||
)
|
)
|
||||||
raise HTTPNotFound
|
return await super()._handle(request)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"domain": "recorder",
|
"domain": "recorder",
|
||||||
"name": "Recorder",
|
"name": "Recorder",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/recorder",
|
"documentation": "https://www.home-assistant.io/integrations/recorder",
|
||||||
"requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0", "lru-dict==1.1.7"],
|
"requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0"],
|
||||||
"codeowners": ["@home-assistant/core"],
|
"codeowners": ["@home-assistant/core"],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
|
|
|
@ -38,6 +38,7 @@ dependencies = [
|
||||||
"httpx==0.23.0",
|
"httpx==0.23.0",
|
||||||
"ifaddr==0.1.7",
|
"ifaddr==0.1.7",
|
||||||
"jinja2==3.1.2",
|
"jinja2==3.1.2",
|
||||||
|
"lru-dict==1.1.7",
|
||||||
"PyJWT==2.4.0",
|
"PyJWT==2.4.0",
|
||||||
# PyJWT has loose dependency. We want the latest one.
|
# PyJWT has loose dependency. We want the latest one.
|
||||||
"cryptography==36.0.2",
|
"cryptography==36.0.2",
|
||||||
|
|
|
@ -13,6 +13,7 @@ ciso8601==2.2.0
|
||||||
httpx==0.23.0
|
httpx==0.23.0
|
||||||
ifaddr==0.1.7
|
ifaddr==0.1.7
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
lru-dict==1.1.7
|
||||||
PyJWT==2.4.0
|
PyJWT==2.4.0
|
||||||
cryptography==36.0.2
|
cryptography==36.0.2
|
||||||
orjson==3.7.5
|
orjson==3.7.5
|
||||||
|
|
|
@ -974,9 +974,6 @@ logi_circle==0.2.3
|
||||||
# homeassistant.components.london_underground
|
# homeassistant.components.london_underground
|
||||||
london-tube-status==0.5
|
london-tube-status==0.5
|
||||||
|
|
||||||
# homeassistant.components.recorder
|
|
||||||
lru-dict==1.1.7
|
|
||||||
|
|
||||||
# homeassistant.components.luftdaten
|
# homeassistant.components.luftdaten
|
||||||
luftdaten==0.7.2
|
luftdaten==0.7.2
|
||||||
|
|
||||||
|
|
|
@ -681,9 +681,6 @@ life360==4.1.1
|
||||||
# homeassistant.components.logi_circle
|
# homeassistant.components.logi_circle
|
||||||
logi_circle==0.2.3
|
logi_circle==0.2.3
|
||||||
|
|
||||||
# homeassistant.components.recorder
|
|
||||||
lru-dict==1.1.7
|
|
||||||
|
|
||||||
# homeassistant.components.luftdaten
|
# homeassistant.components.luftdaten
|
||||||
luftdaten==0.7.2
|
luftdaten==0.7.2
|
||||||
|
|
||||||
|
|
|
@ -578,3 +578,35 @@ async def test_manifest_json(hass, frontend_themes, mock_http_client):
|
||||||
|
|
||||||
json = await resp.json()
|
json = await resp.json()
|
||||||
assert json["theme_color"] != DEFAULT_THEME_COLOR
|
assert json["theme_color"] != DEFAULT_THEME_COLOR
|
||||||
|
|
||||||
|
|
||||||
|
async def test_static_path_cache(hass, mock_http_client):
|
||||||
|
"""Test static paths cache."""
|
||||||
|
resp = await mock_http_client.get("/lovelace/default_view", allow_redirects=False)
|
||||||
|
assert resp.status == 404
|
||||||
|
|
||||||
|
resp = await mock_http_client.get("/frontend_latest/", allow_redirects=False)
|
||||||
|
assert resp.status == 403
|
||||||
|
|
||||||
|
resp = await mock_http_client.get(
|
||||||
|
"/static/icons/favicon.ico", allow_redirects=False
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
# and again to make sure the cache works
|
||||||
|
resp = await mock_http_client.get(
|
||||||
|
"/static/icons/favicon.ico", allow_redirects=False
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
resp = await mock_http_client.get(
|
||||||
|
"/static/fonts/roboto/Roboto-Bold.woff2", allow_redirects=False
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False)
|
||||||
|
assert resp.status == 404
|
||||||
|
|
||||||
|
# and again to make sure the cache works
|
||||||
|
resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False)
|
||||||
|
assert resp.status == 404
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue