Don't physically copy static files for mkdocs serve

Just read them from their original location in the live server instead
This commit is contained in:
Oleh Prypin
2024-02-13 00:26:16 +01:00
parent e755aaed7e
commit b27240a2ed
4 changed files with 52 additions and 14 deletions

View File

@@ -246,7 +246,7 @@ def _build_page(
config._current_page = None config._current_page = None
def build(config: MkDocsConfig, *, serve_url: str | None = None, dirty: bool = False) -> None: def build(config: MkDocsConfig, *, serve_url: str | None = None, dirty: bool = False) -> Files:
"""Perform a full site build.""" """Perform a full site build."""
logger = logging.getLogger('mkdocs') logger = logging.getLogger('mkdocs')
@@ -322,7 +322,12 @@ def build(config: MkDocsConfig, *, serve_url: str | None = None, dirty: bool = F
# with lower precedence get written first so that files with higher precedence can overwrite them. # with lower precedence get written first so that files with higher precedence can overwrite them.
log.debug("Copying static assets.") log.debug("Copying static assets.")
files.copy_static_files(dirty=dirty, inclusion=inclusion) for file in files:
if not file.is_documentation_page() and inclusion(file.inclusion):
if serve_url and file.is_copyless_static_file:
log.debug(f"Skip copying static file: '{file.src_uri}'")
continue
file.copy_file(dirty)
for template in config.theme.static_templates: for template in config.theme.static_templates:
_build_theme_template(template, env, files, config, nav) _build_theme_template(template, env, files, config, nav)
@@ -351,6 +356,7 @@ def build(config: MkDocsConfig, *, serve_url: str | None = None, dirty: bool = F
raise Abort(f'Aborted with {msg} in strict mode!') raise Abort(f'Aborted with {msg} in strict mode!')
log.info(f'Documentation built in {time.monotonic() - start:.2f} seconds') log.info(f'Documentation built in {time.monotonic() - start:.2f} seconds')
return files
except Exception as e: except Exception as e:
# Run `build_error` plugin events. # Run `build_error` plugin events.

View File

@@ -1,15 +1,16 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import os.path
import shutil import shutil
import tempfile import tempfile
from os.path import isdir, isfile, join
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from urllib.parse import urlsplit from urllib.parse import urlsplit
from mkdocs.commands.build import build from mkdocs.commands.build import build
from mkdocs.config import load_config from mkdocs.config import load_config
from mkdocs.livereload import LiveReloadServer, _serve_url from mkdocs.livereload import LiveReloadServer, _serve_url
from mkdocs.structure.files import Files
if TYPE_CHECKING: if TYPE_CHECKING:
from mkdocs.config.defaults import MkDocsConfig from mkdocs.config.defaults import MkDocsConfig
@@ -35,8 +36,6 @@ def serve(
whenever a file is edited. whenever a file is edited.
""" """
# Create a temporary build directory, and set some options to serve it # Create a temporary build directory, and set some options to serve it
# PY2 returns a byte string by default. The Unicode prefix ensures a Unicode
# string is returned. And it makes MkDocs temp dirs easier to identify.
site_dir = tempfile.mkdtemp(prefix='mkdocs_') site_dir = tempfile.mkdtemp(prefix='mkdocs_')
def get_config(): def get_config():
@@ -58,22 +57,42 @@ def serve(
mount_path = urlsplit(config.site_url or '/').path mount_path = urlsplit(config.site_url or '/').path
config.site_url = serve_url = _serve_url(host, port, mount_path) config.site_url = serve_url = _serve_url(host, port, mount_path)
files: Files = Files(())
def builder(config: MkDocsConfig | None = None): def builder(config: MkDocsConfig | None = None):
log.info("Building documentation...") log.info("Building documentation...")
if config is None: if config is None:
config = get_config() config = get_config()
config.site_url = serve_url config.site_url = serve_url
build(config, serve_url=None if is_clean else serve_url, dirty=is_dirty) nonlocal files
files = build(config, serve_url=None if is_clean else serve_url, dirty=is_dirty)
def file_hook(path: str) -> str | None:
f = files.get_file_from_path(path)
if f is not None and f.is_copyless_static_file:
return f.abs_src_path
return None
def get_file(path: str) -> str | None:
if new_path := file_hook(path):
return os.path.join(site_dir, new_path)
if os.path.isfile(try_path := os.path.join(site_dir, path)):
return try_path
return None
server = LiveReloadServer( server = LiveReloadServer(
builder=builder, host=host, port=port, root=site_dir, mount_path=mount_path builder=builder,
host=host,
port=port,
root=site_dir,
file_hook=file_hook,
mount_path=mount_path,
) )
def error_handler(code) -> bytes | None: def error_handler(code) -> bytes | None:
if code in (404, 500): if code in (404, 500):
error_page = join(site_dir, f'{code}.html') if error_page := get_file(f'{code}.html'):
if isfile(error_page):
with open(error_page, 'rb') as f: with open(error_page, 'rb') as f:
return f.read() return f.read()
return None return None
@@ -108,5 +127,5 @@ def serve(
server.shutdown() server.shutdown()
finally: finally:
config.plugins.on_shutdown() config.plugins.on_shutdown()
if isdir(site_dir): if os.path.isdir(site_dir):
shutil.rmtree(site_dir) shutil.rmtree(site_dir)

View File

@@ -101,6 +101,8 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
host: str, host: str,
port: int, port: int,
root: str, root: str,
*,
file_hook: Callable[[str], str | None] = lambda path: None,
mount_path: str = "/", mount_path: str = "/",
polling_interval: float = 0.5, polling_interval: float = 0.5,
shutdown_delay: float = 0.25, shutdown_delay: float = 0.25,
@@ -112,6 +114,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
except Exception: except Exception:
pass pass
self.root = os.path.abspath(root) self.root = os.path.abspath(root)
self.file_hook = file_hook
self.mount_path = _normalize_mount_path(mount_path) self.mount_path = _normalize_mount_path(mount_path)
self.url = _serve_url(host, port, mount_path) self.url = _serve_url(host, port, mount_path)
self.build_delay = 0.1 self.build_delay = 0.1
@@ -289,7 +292,6 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
rel_file_path += "index.html" rel_file_path += "index.html"
# Prevent directory traversal - normalize the path. # Prevent directory traversal - normalize the path.
rel_file_path = posixpath.normpath("/" + rel_file_path).lstrip("/") rel_file_path = posixpath.normpath("/" + rel_file_path).lstrip("/")
file_path = os.path.join(self.root, rel_file_path)
elif path == "/": elif path == "/":
start_response("302 Found", [("Location", urllib.parse.quote(self.mount_path))]) start_response("302 Found", [("Location", urllib.parse.quote(self.mount_path))])
return [] return []
@@ -298,13 +300,20 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
# Wait until the ongoing rebuild (if any) finishes, so we're not serving a half-built site. # Wait until the ongoing rebuild (if any) finishes, so we're not serving a half-built site.
with self._epoch_cond: with self._epoch_cond:
self._epoch_cond.wait_for(lambda: self._visible_epoch == self._wanted_epoch) file_path = self.file_hook(rel_file_path)
if file_path is None:
file_path = os.path.join(self.root, rel_file_path)
self._epoch_cond.wait_for(lambda: self._visible_epoch == self._wanted_epoch)
epoch = self._visible_epoch epoch = self._visible_epoch
try: try:
file: BinaryIO = open(file_path, "rb") file: BinaryIO = open(file_path, "rb")
except OSError: except OSError:
if not path.endswith("/") and os.path.isfile(os.path.join(file_path, "index.html")): if not path.endswith("/") and (
self.file_hook(rel_file_path) is not None
or os.path.isfile(os.path.join(file_path, "index.html"))
):
start_response("302 Found", [("Location", urllib.parse.quote(path) + "/")]) start_response("302 Found", [("Location", urllib.parse.quote(path) + "/")])
return [] return []
return None # Not found return None # Not found

View File

@@ -116,7 +116,7 @@ class Files:
*, *,
inclusion: Callable[[InclusionLevel], bool] = InclusionLevel.is_included, inclusion: Callable[[InclusionLevel], bool] = InclusionLevel.is_included,
) -> None: ) -> None:
"""Copy static files from source to destination.""" """Soft-deprecated, do not use."""
for file in self: for file in self:
if not file.is_documentation_page() and inclusion(file.inclusion): if not file.is_documentation_page() and inclusion(file.inclusion):
file.copy_file(dirty) file.copy_file(dirty)
@@ -464,6 +464,10 @@ class File:
self._content = value self._content = value
self.abs_src_path = None self.abs_src_path = None
@utils.weak_property
def is_copyless_static_file(self) -> bool:
return self.abs_src_path is not None and self.dest_uri == self.src_uri
def copy_file(self, dirty: bool = False) -> None: def copy_file(self, dirty: bool = False) -> None:
"""Copy source file to destination, ensuring parent directories exist.""" """Copy source file to destination, ensuring parent directories exist."""
if dirty and not self.is_modified(): if dirty and not self.is_modified():