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 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): class Cache(ABC):
_rendering_manager: RenderingManager
@abstractmethod def __init__(self, rendering_manager: RenderingManager):
def get(self, key: bytes) -> Optional[bytes]: self._rendering_manager = rendering_manager
pass
@abstractmethod def get(self, key: bytes, file: PurePath) -> Optional[Tuple[str, bytes]]:
def put(self, key: bytes, value: bytes): if self.filter(file):
pass return self.load(key, file)
else:
return self._rendering_manager.render(file)
def __contains__(self, key: bytes): def filter(self, file: PurePath) -> bool:
return self.get(key) is not None 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 hashlib import md5
from mimetypes import guess_type from mimetypes import guess_type
from pathlib import Path, PurePath 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.os import listdir
from aiofiles.ospath import isdir, isfile from aiofiles.ospath import isdir, isfile
from bugis.core import HttpContext, BugisApp from bugis.core import HttpContext, BugisApp
from pwo import Maybe from pwo import Maybe
from .cache import Cache, InMemoryCache
from .renderer import RenderingManager
if TYPE_CHECKING: if TYPE_CHECKING:
from _typeshed import StrOrBytesPath from _typeshed import StrOrBytesPath
@@ -31,8 +33,13 @@ def static_resources(app: BugisApp,
path: str, path: str,
root: 'StrOrBytesPath', root: 'StrOrBytesPath',
favicon: PurePath = None, 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): async def no_filter(_: PurePath):
return True return True
@@ -66,24 +73,33 @@ def static_resources(app: BugisApp,
if await isfile(resource): if await isfile(resource):
e_tag_header = ((Maybe.of_nullable(context.headers.get('if-none-match')) proposed_etag = ((Maybe.of_nullable(context.headers.get('if-none-match'))
.filter(lambda it: len(it) > 0) .filter(lambda it: len(it) > 0)
.map(lambda it: it[-1]) .map(lambda it: it[-1])
.map(parse_etag)) .map(parse_etag))
.or_none()) .or_none())
current_etag = compute_etag(resource) current_etag = compute_etag(resource)
if e_tag_header == current_etag: if proposed_etag == current_etag:
return await context.send_empty(304) return await context.send_empty(304)
else: else:
mime_type = (Maybe.of(guess_type(resource.name)) cache_result = cache.get(b64decode(current_etag), resource)
.map(lambda it: it[0]) if cache_result is None:
.or_else('application/octet-stream')) mime_type = (Maybe.of(guess_type(resource.name))
return await context.send_file(200, resource, { .map(lambda it: it[0])
'content-type': mime_type or 'application/octet-stream', .or_else('application/octet-stream'))
'etag': 'W/' + current_etag, return await context.send_file(200, resource, {
'cache-control': 'no-cache', '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): elif isdir(resource):
headers = { headers = {
'content-type': 'text/html' 'content-type': 'text/html'