temporary commit
This commit is contained in:
@@ -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)
|
||||
|
16
server/src/bugis/server/renderer.py
Normal file
16
server/src/bugis/server/renderer.py
Normal 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
|
@@ -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'
|
||||
|
Reference in New Issue
Block a user