temporary commit
This commit is contained in:
@@ -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)
|
||||||
|
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 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'
|
||||||
|
Reference in New Issue
Block a user