Move DirectoryHandler to app instance
This commit is contained in:
parent
faf1ff8d4f
commit
f30f53f67d
|
@ -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"):
|
||||
|
|
|
@ -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-?]*[ -/]*[@-~])")
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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; }
|
||||
|
@ -28,8 +30,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
|
||||
|
||||
|
@ -43,7 +47,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))):
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
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;
|
||||
}
|
||||
a:visited { color: inherit; }
|
||||
a { text-decoration: none; color: #88f; }
|
||||
a:hover, a:focus { text-decoration: underline; outline: none; }
|
||||
#logo { height: 2.5rem; }
|
||||
#logo { height: 2.75rem; padding: 0.25rem 0; }
|
||||
table { width: 100%; max-width: 1200px; }
|
||||
span.icon { margin-right: 1rem; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
@ -30,8 +30,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:
|
||||
|
@ -44,8 +45,11 @@ class BasePage(ABC):
|
|||
|
||||
def _head(self) -> None:
|
||||
self.doc.style(HTML(self.style))
|
||||
with self.doc.header:
|
||||
self.doc(LOGO_SVG).div(self.TITLE, id="hdrtext").div(f"Version {VERSION}", id="hdrver")
|
||||
if self.debug:
|
||||
with self.doc.header:
|
||||
self.doc(HTML(SVG_LOGO)).div(self.TITLE, id="hdrtext").div(
|
||||
f"Version {VERSION}", id="hdrver"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def _body(self) -> None:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user