Skip to content

Commit

Permalink
Don't physically copy static files for mkdocs serve
Browse files Browse the repository at this point in the history
Just read them from their original location in the live server instead
  • Loading branch information
oprypin committed Feb 12, 2024
1 parent e755aae commit b27240a
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 14 deletions.
10 changes: 8 additions & 2 deletions mkdocs/commands/build.py
Expand Up @@ -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')

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
35 changes: 27 additions & 8 deletions mkdocs/commands/serve.py
@@ -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
Expand All @@ -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():
Expand All @@ -58,22 +57,42 @@ def get_config():
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
Expand Down Expand Up @@ -108,5 +127,5 @@ def error_handler(code) -> bytes | None:
server.shutdown()
finally:
config.plugins.on_shutdown()
if isdir(site_dir):
if os.path.isdir(site_dir):
shutil.rmtree(site_dir)
15 changes: 12 additions & 3 deletions mkdocs/livereload/__init__.py
Expand Up @@ -101,6 +101,8 @@ def __init__(
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,
Expand All @@ -112,6 +114,7 @@ def __init__(
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
Expand Down Expand Up @@ -289,7 +292,6 @@ def condition():
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 []
Expand All @@ -298,13 +300,20 @@ def condition():

# 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
Expand Down
6 changes: 5 additions & 1 deletion mkdocs/structure/files.py
Expand Up @@ -116,7 +116,7 @@ def copy_static_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)
Expand Down Expand Up @@ -464,6 +464,10 @@ def content_string(self, value: str):
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():
Expand Down

0 comments on commit b27240a

Please sign in to comment.