Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement etag support for category files #1655

Merged
merged 4 commits into from
Nov 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions custom_components/hacs/helpers/functions/file_etag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member
import os

from custom_components.hacs.share import get_hacs
from fnvhash import fnv1a_32
from pathlib import Path


def get_etag(path) -> bool:
file_path = Path(path)
if not file_path.exists():
return None
return fnv1a_32(file_path.read_bytes())


async def async_get_etag(path) -> bool:
hass = get_hacs().hass
fnv = await hass.async_add_executor_job(get_etag, path)
if fnv is None:
return None
return str(hex(fnv))
3 changes: 2 additions & 1 deletion custom_components/hacs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"aiofiles>=0.5.0",
"aiogithubapi>=2.0.0<3.0.0",
"backoff>=1.10.0",
"fnvhash>=0.1.0<1.0.0",
"hacs_frontend==202011221222",
"semantic_version>=2.8.5",
"queueman==0.5"
]
}
}
90 changes: 72 additions & 18 deletions custom_components/hacs/webresponses/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,92 @@

from custom_components.hacs.helpers.functions.logger import getLogger
from custom_components.hacs.helpers.functions.path_exsist import async_path_exsist
from custom_components.hacs.helpers.functions.file_etag import async_get_etag

from custom_components.hacs.share import get_hacs

_LOGGER = getLogger()


async def async_serve_category_file(request, requested_file):
hacs = get_hacs()
response = None

try:
if requested_file.startswith("themes/"):
servefile = f"{hacs.core.config_path}/{requested_file}"
response = await async_serve_static_file(request, servefile, requested_file)
else:
servefile = f"{hacs.core.config_path}/www/community/{requested_file}"

if await async_path_exsist(servefile):
_LOGGER.debug("Serving %s from %s", requested_file, servefile)
response = web.FileResponse(servefile)
if requested_file.startswith("themes/"):
response.headers["Cache-Control"] = "public, max-age=2678400"
else:
response.headers["Cache-Control"] = "no-store, max-age=0"
response.headers["Pragma"] = "no-store"
return response
else:
_LOGGER.error(
"%s tried to request '%s' but the file does not exist",
request.remote,
servefile,
response = await async_serve_static_file_with_etag(
request, servefile, requested_file
)
except (Exception, BaseException):
_LOGGER.exception("Error trying to serve %s", requested_file)

if response is not None:
return response

return web.Response(status=404)


async def async_serve_static_file(request, servefile, requested_file):
"""Serve a static file without an etag."""
if await async_path_exsist(servefile):
_LOGGER.debug("Serving %s from %s", requested_file, servefile)
response = web.FileResponse(servefile)
response.headers["Cache-Control"] = "public, max-age=2678400"
return response

except (Exception, BaseException) as exception:
_LOGGER.error(
"%s tried to request '%s' but the file does not exist",
request.remote,
servefile,
)
return None


async def async_serve_static_file_with_etag(request, servefile, requested_file):
"""Serve a static file with an etag."""
etag = await async_get_etag(servefile)
if_none_match_header = request.headers.get("if-none-match")

if (
etag is not None
and if_none_match_header is not None
and _match_etag(etag, if_none_match_header)
):
_LOGGER.debug(
"there was an issue trying to serve %s - %s", requested_file, exception
"Serving %s from %s with etag %s (not-modified)",
requested_file,
servefile,
etag,
)
return web.Response(status=304)

return web.Response(status=404)
if etag is not None:
_LOGGER.debug(
"Serving %s from %s with etag %s (not cached)",
requested_file,
servefile,
etag,
)
response = web.FileResponse(servefile)
response.headers["Cache-Control"] = "must-revalidate, max-age=0"
response.headers["Etag"] = etag
return response

_LOGGER.error(
"%s tried to request '%s' but the file does not exist",
request.remote,
servefile,
)
return None


def _match_etag(etag, if_none_match_header):
"""Check to see if an etag matches."""
for if_none_match_ele in if_none_match_header.split(","):
if if_none_match_ele.strip() == etag:
return True
return False
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ attrs==20.3.0
backoff==1.10.0
bellybutton==0.3.0
colorlog==4.6.2
fnvhash>=0.1.0<1.0.0
hacs_frontend==202011221222
pre-commit==2.8.2
PyGithub==1.53
Expand Down