temporary commit

This commit is contained in:
2025-04-02 17:21:30 +08:00
parent 56c8e796b7
commit f158698380
3 changed files with 86 additions and 26 deletions

View File

@@ -1,16 +1,44 @@
from abc import ABC, abstractmethod
from typing import Optional
from typing import Optional, Tuple, Callable
from functools import lru_cache
from pathlib import PurePath
from .renderer import RenderingManager
class Cache(ABC):
_rendering_manager: RenderingManager
@abstractmethod
def get(self, key: bytes) -> Optional[bytes]:
pass
def __init__(self, rendering_manager: RenderingManager):
self._rendering_manager = rendering_manager
@abstractmethod
def put(self, key: bytes, value: bytes):
pass
def get(self, key: bytes, file: PurePath) -> Optional[Tuple[str, bytes]]:
if self.filter(file):
return self.load(key, file)
else:
return self._rendering_manager.render(file)
def __contains__(self, key: bytes):
return self.get(key) is not None
def filter(self, file: PurePath) -> bool:
return file.suffix.lower() in {'md', 'puml', 'dot', 'texi', 'texinfo', 'txi'}
def load(self, key: bytes, file: PurePath) -> Optional[Tuple[str, bytes]]:
return self._rendering_manager.render(file)
class NoCache(Cache):
pass
class InMemoryCache(Cache):
_cached_loader: Callable[[bytes, PurePath], Tuple[str, bytes]]
def __init__(self, rendering_manager: RenderingManager, max_size: int = 1024):
super().__init__(rendering_manager)
@lru_cache(maxsize=max_size)
def cached_loader(key: bytes, file: PurePath) -> Tuple[str, bytes]:
return super().load(key, file)
self._cached_loader = cached_loader
def load(self, key: bytes, file: PurePath) -> Optional[Tuple[str, bytes]]:
return self._cached_loader(key, file)

View File

@@ -0,0 +1,16 @@
from pathlib import PurePath
from typing import Callable, Mapping, Optional, Tuple
from pwo import Maybe
class RenderingManager:
_renderers: Mapping[str, Callable[[PurePath], Tuple[str, bytes]]]
def __init__(self, renderers: Mapping[str, Callable[[PurePath], Tuple[str, bytes]]]):
self._renderers = renderers
def render(self, file: PurePath) -> Optional[Tuple[str, bytes]]:
return Maybe.of_nullable(self._renderers.get(file.suffix.lower())).map(lambda it: it(file)).or_none()
# def register(self, suffix: str, renderer: Callable[[PurePath], Tuple[str, bytes]]) -> None:
# self._renderers[suffix.lower()] = renderer

View File

@@ -1,13 +1,15 @@
from base64 import b64encode
from base64 import b64encode, b64decode
from hashlib import md5
from mimetypes import guess_type
from pathlib import Path, PurePath
from typing import TYPE_CHECKING, Optional, Callable, Awaitable, AsyncGenerator, Any, Unpack
from typing import TYPE_CHECKING, Optional, Callable, Awaitable, AsyncGenerator, Any, Unpack, Mapping, Tuple
from aiofiles.os import listdir
from aiofiles.ospath import isdir, isfile
from bugis.core import HttpContext, BugisApp
from pwo import Maybe
from .cache import Cache, InMemoryCache
from .renderer import RenderingManager
if TYPE_CHECKING:
from _typeshed import StrOrBytesPath
@@ -31,8 +33,13 @@ def static_resources(app: BugisApp,
path: str,
root: 'StrOrBytesPath',
favicon: PurePath = None,
file_filter: Callable[[PurePath], Awaitable[bool]] = None
file_filter: Callable[[PurePath], Awaitable[bool]] = None,
renderers: Mapping[str, Tuple[str, bytes]] = None,
cache_ctor: Callable[[RenderingManager], Cache] = lambda rm: InMemoryCache(rm)
):
renderer = RenderingManager(renderers or {})
cache = cache_ctor(renderer)
async def no_filter(_: PurePath):
return True
@@ -66,24 +73,33 @@ def static_resources(app: BugisApp,
if await isfile(resource):
e_tag_header = ((Maybe.of_nullable(context.headers.get('if-none-match'))
.filter(lambda it: len(it) > 0)
.map(lambda it: it[-1])
.map(parse_etag))
.or_none())
proposed_etag = ((Maybe.of_nullable(context.headers.get('if-none-match'))
.filter(lambda it: len(it) > 0)
.map(lambda it: it[-1])
.map(parse_etag))
.or_none())
current_etag = compute_etag(resource)
if e_tag_header == current_etag:
if proposed_etag == current_etag:
return await context.send_empty(304)
else:
mime_type = (Maybe.of(guess_type(resource.name))
.map(lambda it: it[0])
.or_else('application/octet-stream'))
return await context.send_file(200, resource, {
'content-type': mime_type or 'application/octet-stream',
'etag': 'W/' + current_etag,
'cache-control': 'no-cache',
})
cache_result = cache.get(b64decode(current_etag), resource)
if cache_result is None:
mime_type = (Maybe.of(guess_type(resource.name))
.map(lambda it: it[0])
.or_else('application/octet-stream'))
return await context.send_file(200, resource, {
'content-type': mime_type or 'application/octet-stream',
'etag': 'W/' + current_etag,
'cache-control': 'no-cache',
})
else:
content_type, body = cache_result
await context.send_bytes(200, body, {
'content-type': content_type,
'etag': 'W/' + current_etag,
'cache-control': 'no-cache',
})
elif isdir(resource):
headers = {
'content-type': 'text/html'