mirror of
https://github.com/mkdocs/mkdocs.git
synced 2026-03-27 01:48:30 +07:00
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:
@@ -246,7 +246,7 @@ def _build_page(
|
||||
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."""
|
||||
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.
|
||||
|
||||
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:
|
||||
_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!')
|
||||
|
||||
log.info(f'Documentation built in {time.monotonic() - start:.2f} seconds')
|
||||
return files
|
||||
|
||||
except Exception as e:
|
||||
# Run `build_error` plugin events.
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
import shutil
|
||||
import tempfile
|
||||
from os.path import isdir, isfile, join
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from mkdocs.commands.build import build
|
||||
from mkdocs.config import load_config
|
||||
from mkdocs.livereload import LiveReloadServer, _serve_url
|
||||
from mkdocs.structure.files import Files
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mkdocs.config.defaults import MkDocsConfig
|
||||
@@ -35,8 +36,6 @@ def serve(
|
||||
whenever a file is edited.
|
||||
"""
|
||||
# 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_')
|
||||
|
||||
def get_config():
|
||||
@@ -58,22 +57,42 @@ def serve(
|
||||
mount_path = urlsplit(config.site_url or '/').path
|
||||
config.site_url = serve_url = _serve_url(host, port, mount_path)
|
||||
|
||||
files: Files = Files(())
|
||||
|
||||
def builder(config: MkDocsConfig | None = None):
|
||||
log.info("Building documentation...")
|
||||
if config is None:
|
||||
config = get_config()
|
||||
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(
|
||||
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:
|
||||
if code in (404, 500):
|
||||
error_page = join(site_dir, f'{code}.html')
|
||||
if isfile(error_page):
|
||||
if error_page := get_file(f'{code}.html'):
|
||||
with open(error_page, 'rb') as f:
|
||||
return f.read()
|
||||
return None
|
||||
@@ -108,5 +127,5 @@ def serve(
|
||||
server.shutdown()
|
||||
finally:
|
||||
config.plugins.on_shutdown()
|
||||
if isdir(site_dir):
|
||||
if os.path.isdir(site_dir):
|
||||
shutil.rmtree(site_dir)
|
||||
|
||||
@@ -101,6 +101,8 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
|
||||
host: str,
|
||||
port: int,
|
||||
root: str,
|
||||
*,
|
||||
file_hook: Callable[[str], str | None] = lambda path: None,
|
||||
mount_path: str = "/",
|
||||
polling_interval: float = 0.5,
|
||||
shutdown_delay: float = 0.25,
|
||||
@@ -112,6 +114,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
|
||||
except Exception:
|
||||
pass
|
||||
self.root = os.path.abspath(root)
|
||||
self.file_hook = file_hook
|
||||
self.mount_path = _normalize_mount_path(mount_path)
|
||||
self.url = _serve_url(host, port, mount_path)
|
||||
self.build_delay = 0.1
|
||||
@@ -289,7 +292,6 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
|
||||
rel_file_path += "index.html"
|
||||
# Prevent directory traversal - normalize the path.
|
||||
rel_file_path = posixpath.normpath("/" + rel_file_path).lstrip("/")
|
||||
file_path = os.path.join(self.root, rel_file_path)
|
||||
elif path == "/":
|
||||
start_response("302 Found", [("Location", urllib.parse.quote(self.mount_path))])
|
||||
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.
|
||||
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
|
||||
|
||||
try:
|
||||
file: BinaryIO = open(file_path, "rb")
|
||||
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) + "/")])
|
||||
return []
|
||||
return None # Not found
|
||||
|
||||
@@ -116,7 +116,7 @@ class Files:
|
||||
*,
|
||||
inclusion: Callable[[InclusionLevel], bool] = InclusionLevel.is_included,
|
||||
) -> None:
|
||||
"""Copy static files from source to destination."""
|
||||
"""Soft-deprecated, do not use."""
|
||||
for file in self:
|
||||
if not file.is_documentation_page() and inclusion(file.inclusion):
|
||||
file.copy_file(dirty)
|
||||
@@ -464,6 +464,10 @@ class File:
|
||||
self._content = value
|
||||
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:
|
||||
"""Copy source file to destination, ensuring parent directories exist."""
|
||||
if dirty and not self.is_modified():
|
||||
|
||||
Reference in New Issue
Block a user