From f158698380f2d0bf3137e45e9cb151e15fbedeea Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Wed, 2 Apr 2025 17:21:30 +0800 Subject: [PATCH] temporary commit --- server/src/bugis/server/cache.py | 46 ++++++++++++++++++++------ server/src/bugis/server/renderer.py | 16 +++++++++ server/src/bugis/server/server.py | 50 +++++++++++++++++++---------- 3 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 server/src/bugis/server/renderer.py diff --git a/server/src/bugis/server/cache.py b/server/src/bugis/server/cache.py index 6c8daa5..b4c08a7 100644 --- a/server/src/bugis/server/cache.py +++ b/server/src/bugis/server/cache.py @@ -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) diff --git a/server/src/bugis/server/renderer.py b/server/src/bugis/server/renderer.py new file mode 100644 index 0000000..6b7d063 --- /dev/null +++ b/server/src/bugis/server/renderer.py @@ -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 diff --git a/server/src/bugis/server/server.py b/server/src/bugis/server/server.py index 1df50dd..9f80513 100644 --- a/server/src/bugis/server/server.py +++ b/server/src/bugis/server/server.py @@ -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'