mirror of
https://github.com/mkdocs/mkdocs.git
synced 2026-03-27 09:58:31 +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
|
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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
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)
|
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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user