Merge branch 'issue2661' into niceback-error-handling

This commit is contained in:
L. Kärkkäinen 2023-01-28 23:29:14 +00:00 committed by GitHub
commit ae757c8ad6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 78 additions and 40 deletions

View File

@ -61,6 +61,7 @@ from sanic.exceptions import (
URLBuildError,
)
from sanic.handlers import ErrorHandler
from sanic.handlers.directory import DirectoryHandler
from sanic.helpers import Default, _default
from sanic.http import Stage
from sanic.log import (
@ -140,6 +141,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
"config",
"configure_logging",
"ctx",
"directory_handler",
"error_handler",
"inspector_class",
"go_fast",
@ -169,6 +171,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
ctx: Optional[Any] = None,
router: Optional[Router] = None,
signal_router: Optional[SignalRouter] = None,
directory_handler: Optional[DirectoryHandler] = None,
error_handler: Optional[ErrorHandler] = None,
env_prefix: Optional[str] = SANIC_PREFIX,
request_class: Optional[Type[Request]] = None,
@ -213,6 +216,9 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
self.blueprints: Dict[str, Blueprint] = {}
self.configure_logging: bool = configure_logging
self.ctx: Any = ctx or SimpleNamespace()
self.directory_handler: DirectoryHandler = (
directory_handler or DirectoryHandler(self.debug)
)
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
self.inspector_class: Type[Inspector] = inspector_class or Inspector
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
@ -1572,6 +1578,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
TouchUp.run(self)
self.state.is_started = True
self.directory_handler.debug = self.debug
def ack(self):
if hasattr(self, "multiplexer"):

View File

@ -40,6 +40,8 @@ FULL_COLOR_LOGO = """
""" # noqa
SVG_LOGO = """<svg id=logo alt=Sanic viewBox="0 0 964 279"><path d="M107 222c9-2 10-20 1-22s-20-2-30-2-17 7-16 14 6 10 15 10h30zm115-1c16-2 30-11 35-23s6-24 2-33-6-14-15-20-24-11-38-10c-7 3-10 13-5 19s17-1 24 4 15 14 13 24-5 15-14 18-50 0-74 0h-17c-6 4-10 15-4 20s16 2 23 3zM251 83q9-1 9-7 0-15-10-16h-13c-10 6-10 20 0 22zM147 60c-4 0-10 3-11 11s5 13 10 12 42 0 67 0c5-3 7-10 6-15s-4-8-9-8zm-33 1c-8 0-16 0-24 3s-20 10-25 20-6 24-4 36 15 22 26 27 78 8 94 3c4-4 4-12 0-18s-69 8-93-10c-8-7-9-23 0-30s12-10 20-10 12 2 16-3 1-15-5-18z" fill="#ff0d68"/><path d="M676 74c0-14-18-9-20 0s0 30 0 39 20 9 20 2zm-297-10c-12 2-15 12-23 23l-41 58H340l22-30c8-12 23-13 30-4s20 24 24 38-10 10-17 10l-68 2q-17 1-48 30c-7 6-10 20 0 24s15-8 20-13 20 -20 58-21h50 c20 2 33 9 52 30 8 10 24-4 16-13L384 65q-3-2-5-1zm131 0c-10 1-12 12-11 20v96c1 10-3 23 5 32s20-5 17-15c0-23-3-46 2-67 5-12 22-14 32-5l103 87c7 5 19 1 18-9v-64c-3-10-20-9-21 2s-20 22-30 13l-97-80c-5-4-10-10-18-10zM701 76v128c2 10 15 12 20 4s0-102 0-124s-20-18-20-7z M850 63c-35 0-69-2-86 15s-20 60-13 66 13 8 16 0 1-10 1-27 12-26 20-32 66-5 85-5 31 4 31-10-18-7-54-7M764 159c-6-2-15-2-16 12s19 37 33 43 23 8 25-4-4-11-11-14q-9-3-22-18c-4-7-3-16-10-19zM828 196c-4 0-8 1-10 5s-4 12 0 15 8 2 12 2h60c5 0 10-2 12-6 3-7-1-16-8-16z" fill="#e1e1e1"/></svg>""" # noqa
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")

View File

@ -34,3 +34,4 @@ CACHEABLE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD)
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
DEFAULT_LOCAL_TLS_KEY = "key.pem"
DEFAULT_LOCAL_TLS_CERT = "cert.pem"
DEFAULT_INDEX = "index.html"

View File

@ -14,33 +14,36 @@ from sanic.response.types import HTTPResponse
class DirectoryHandler:
def __init__(
def __init__(self, debug: bool) -> None:
self.debug = debug
def handle(
self, directory: Path, autoindex: bool, index_name: str, url: str
) -> None:
self.directory = directory
self.autoindex = autoindex
self.index_name = index_name
self.url = url
):
index_file = directory / index_name
if autoindex and (not index_file.exists() or not index_name):
return self.index(directory, url)
def handle(self):
index_file = self.directory / self.index_name
if self.autoindex and (not index_file.exists() or not self.index_name):
return self.index()
if self.index_name:
if index_name:
return file(index_file)
def index(self):
def index(self, directory: Path, url: str):
# Remove empty path elements, append slash
if "//" in self.url or not self.url.endswith("/"):
return redirect("/" + "".join([f"{p}/" for p in self.url.split("/") if p]))
if "//" in url or not url.endswith("/"):
return redirect(
"/" + "".join([f"{p}/" for p in url.split("/") if p])
)
# Render file browser
page = AutoIndex(self._iter_files(), self.url)
page = AutoIndex(self._iter_files(directory), url, self.debug)
return html(page.render())
def _prepare_file(self, path: Path) -> Dict[str, Union[int, str]]:
stat = path.stat()
modified = datetime.fromtimestamp(stat.st_mtime).isoformat()[:19].replace("T", " ")
modified = (
datetime.fromtimestamp(stat.st_mtime)
.isoformat()[:19]
.replace("T", " ")
)
is_dir = S_ISDIR(stat.st_mode)
icon = "📁" if is_dir else "📄"
file_name = path.name
@ -54,8 +57,8 @@ class DirectoryHandler:
"file_size": stat.st_size,
}
def _iter_files(self) -> Iterable[FileInfo]:
prepared = [self._prepare_file(f) for f in self.directory.iterdir()]
def _iter_files(self, directory: Path) -> Iterable[FileInfo]:
prepared = [self._prepare_file(f) for f in directory.iterdir()]
for item in sorted(prepared, key=itemgetter("priority", "file_name")):
del item["priority"]
yield cast(FileInfo, item)
@ -65,9 +68,12 @@ class DirectoryHandler:
cls, request: Request, exception: SanicIsADirectoryError
) -> Optional[Coroutine[Any, Any, HTTPResponse]]:
if exception.autoindex or exception.index_name:
maybe_response = DirectoryHandler(
exception.location, exception.autoindex, exception.index_name, request.path
).handle()
maybe_response = request.app.directory_handler.handle(
exception.location,
exception.autoindex,
exception.index_name,
request.path,
)
if maybe_response:
return maybe_response
return None

View File

@ -25,7 +25,11 @@ from sanic_routing.route import Route
from sanic.base.meta import SanicMeta
from sanic.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.constants import (
DEFAULT_HTTP_CONTENT_TYPE,
DEFAULT_INDEX,
HTTP_METHODS,
)
from sanic.errorpages import RESPONSE_MAPPING
from sanic.exceptions import (
FileNotFound,
@ -34,6 +38,7 @@ from sanic.exceptions import (
SanicIsADirectoryError,
)
from sanic.handlers import ContentRangeHandler
from sanic.helpers import Default, _default
from sanic.log import error_logger
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import RouteHandler
@ -708,7 +713,7 @@ class RouteMixin(metaclass=SanicMeta):
apply: bool = True,
resource_type: Optional[str] = None,
autoindex: bool = False,
index_name: str = "",
index_name: Union[str, Default] = _default,
):
"""
Register a root to serve files from. The input can either be a
@ -747,6 +752,9 @@ class RouteMixin(metaclass=SanicMeta):
f"Static route must be a valid path, not {file_or_directory}"
)
if isinstance(index_name, Default):
index_name = DEFAULT_INDEX
static = FutureStatic(
uri,
file_or_directory,

View File

@ -17,7 +17,9 @@ class AutoIndex(BasePage):
EXTRA_STYLE = dedent(
"""
#breadcrumbs .path-0 a::before { content: "🏠"; }
#breadcrumbs span:has(> a:hover, > a:focus) * { color: #ff0d68; text-shadow: 0 0 1rem; }
#breadcrumbs span:has(> a:hover, > a:focus) * {
color: #ff0d68; text-shadow: 0 0 1rem;
}
main a { color: inherit; font-weight: bold; }
table.autoindex tr { display: flex; }
table.autoindex td { margin: 0 0.5rem; }
@ -29,8 +31,10 @@ class AutoIndex(BasePage):
)
TITLE = "File browser"
def __init__(self, files: Iterable[FileInfo], url: str) -> None:
super().__init__()
def __init__(
self, files: Iterable[FileInfo], url: str, debug: bool
) -> None:
super().__init__(debug)
self.files = files
self.url = url
@ -44,7 +48,8 @@ class AutoIndex(BasePage):
self.doc.p("The folder is empty.")
def _headline(self):
# Implement a heading with the current path, combined with breadcrumb links
"""Implement a heading with the current path, combined with
breadcrumb links"""
with self.doc.h1(id="breadcrumbs"):
p = self.url.split("/")[:-1]
for i in reversed(range(len(p))):

View File

@ -1,20 +1,20 @@
from abc import ABC, abstractmethod
from textwrap import dedent
from html5tagger import HTML, Document
from sanic import __version__ as VERSION
from sanic.application.logo import SVG_LOGO
from html5tagger import Document, HTML
LOGO_SVG = HTML("""<svg id=logo alt=Sanic viewBox="0 0 964 279"><path d="M107 222c9-2 10-20 1-22s-20-2-30-2-17 7-16 14 6 10 15 10h30zm115-1c16-2 30-11 35-23s6-24 2-33-6-14-15-20-24-11-38-10c-7 3-10 13-5 19s17-1 24 4 15 14 13 24-5 15-14 18-50 0-74 0h-17c-6 4-10 15-4 20s16 2 23 3zM251 83q9-1 9-7 0-15-10-16h-13c-10 6-10 20 0 22zM147 60c-4 0-10 3-11 11s5 13 10 12 42 0 67 0c5-3 7-10 6-15s-4-8-9-8zm-33 1c-8 0-16 0-24 3s-20 10-25 20-6 24-4 36 15 22 26 27 78 8 94 3c4-4 4-12 0-18s-69 8-93-10c-8-7-9-23 0-30s12-10 20-10 12 2 16-3 1-15-5-18z" fill="#ff0d68"/><path d="M676 74c0-14-18-9-20 0s0 30 0 39 20 9 20 2zm-297-10c-12 2-15 12-23 23l-41 58H340l22-30c8-12 23-13 30-4s20 24 24 38-10 10-17 10l-68 2q-17 1-48 30c-7 6-10 20 0 24s15-8 20-13 20 -20 58-21h50 c20 2 33 9 52 30 8 10 24-4 16-13L384 65q-3-2-5-1zm131 0c-10 1-12 12-11 20v96c1 10-3 23 5 32s20-5 17-15c0-23-3-46 2-67 5-12 22-14 32-5l103 87c7 5 19 1 18-9v-64c-3-10-20-9-21 2s-20 22-30 13l-97-80c-5-4-10-10-18-10zM701 76v128c2 10 15 12 20 4s0-102 0-124s-20-18-20-7z M850 63c-35 0-69-2-86 15s-20 60-13 66 13 8 16 0 1-10 1-27 12-26 20-32 66-5 85-5 31 4 31-10-18-7-54-7M764 159c-6-2-15-2-16 12s19 37 33 43 23 8 25-4-4-11-11-14q-9-3-22-18c-4-7-3-16-10-19zM828 196c-4 0-8 1-10 5s-4 12 0 15 8 2 12 2h60c5 0 10-2 12-6 3-7-1-16-8-16z" fill="#e1e1e1"/></svg>""")
class BasePage(ABC):
BASE_STYLE = dedent(
"""
body { margin: 0; font: 1.2rem sans-serif; }
body { margin: 0; font: 16px sans-serif; }
body > * { padding: 0 2rem; }
header {
display: flex; align-items: center; justify-content: space-between;
background: #555; color: #e1e1e1;
background: #111; color: #e1e1e1; border-bottom: 1px solid #272727;
}
main { padding-bottom: 3rem; }
h2 { margin: 2rem 0 1rem 0; }
@ -22,9 +22,12 @@ class BasePage(ABC):
a { text-decoration: none; color: #88f; }
a:hover, a:focus { text-decoration: underline; outline: none; }
#logo { height: 2.5rem; }
table { width: 100%; max-width: 1200px; word-break: break-all; }
#logo { height: 2.75rem; padding: 0.25rem 0; }
.smalltext { font-size: 1rem; }
.nobr { white-space: nowrap; }
table { width: 100%; max-width: 1200px; word-break: break-all; }
table { width: 100%; max-width: 1200px; }
span.icon { margin-right: 1rem; }
@media (prefers-color-scheme: dark) {
html { background: #111; color: #ccc; }
}
@ -33,8 +36,9 @@ class BasePage(ABC):
EXTRA_STYLE = ""
TITLE = "Unknown"
def __init__(self) -> None:
def __init__(self, debug: bool = True) -> None:
self.doc = Document(self.TITLE, lang="en")
self.debug = debug
@property
def style(self) -> str:
@ -47,8 +51,11 @@ class BasePage(ABC):
def _head(self) -> None:
self.doc.style(HTML(self.style))
if self.debug:
with self.doc.header:
self.doc(LOGO_SVG).div(self.TITLE, id="hdrtext").div(f"Version {VERSION}", id="hdrver")
self.doc(HTML(SVG_LOGO)).div(self.TITLE, id="hdrtext").div(
f"Version {VERSION}", id="hdrver"
)
@abstractmethod
def _body(self) -> None:

View File

@ -10,7 +10,7 @@ from typing import Any, AnyStr, Callable, Dict, Optional, Union
from urllib.parse import quote_plus
from sanic.compat import Header, open_async, stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, DEFAULT_INDEX
from sanic.exceptions import SanicIsADirectoryError
from sanic.helpers import Default, _default
from sanic.log import logger
@ -166,7 +166,7 @@ async def file(
no_store: Optional[bool] = None,
_range: Optional[Range] = None,
autoindex: bool = False,
index_name: str = "",
index_name: Union[str, Default] = _default,
) -> HTTPResponse:
"""Return a response object with file data.
:param status: HTTP response code. Won't enforce the passed in
@ -241,6 +241,8 @@ async def file(
else:
out_stream = await f.read()
except IsADirectoryError as e:
if isinstance(index_name, Default):
index_name = DEFAULT_INDEX
exc = SanicIsADirectoryError(str(e))
exc.location = Path(location)
exc.autoindex = autoindex