Skip to content

Commit b27240a

Browse files
committed
Don't physically copy static files for mkdocs serve
Just read them from their original location in the live server instead
1 parent e755aae commit b27240a

File tree

4 files changed

+52
-14
lines changed

4 files changed

+52
-14
lines changed

mkdocs/commands/build.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def _build_page(
246246
config._current_page = None
247247

248248

249-
def build(config: MkDocsConfig, *, serve_url: str | None = None, dirty: bool = False) -> None:
249+
def build(config: MkDocsConfig, *, serve_url: str | None = None, dirty: bool = False) -> Files:
250250
"""Perform a full site build."""
251251
logger = logging.getLogger('mkdocs')
252252

@@ -322,7 +322,12 @@ def build(config: MkDocsConfig, *, serve_url: str | None = None, dirty: bool = F
322322
# with lower precedence get written first so that files with higher precedence can overwrite them.
323323

324324
log.debug("Copying static assets.")
325-
files.copy_static_files(dirty=dirty, inclusion=inclusion)
325+
for file in files:
326+
if not file.is_documentation_page() and inclusion(file.inclusion):
327+
if serve_url and file.is_copyless_static_file:
328+
log.debug(f"Skip copying static file: '{file.src_uri}'")
329+
continue
330+
file.copy_file(dirty)
326331

327332
for template in config.theme.static_templates:
328333
_build_theme_template(template, env, files, config, nav)
@@ -351,6 +356,7 @@ def build(config: MkDocsConfig, *, serve_url: str | None = None, dirty: bool = F
351356
raise Abort(f'Aborted with {msg} in strict mode!')
352357

353358
log.info(f'Documentation built in {time.monotonic() - start:.2f} seconds')
359+
return files
354360

355361
except Exception as e:
356362
# Run `build_error` plugin events.

mkdocs/commands/serve.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from __future__ import annotations
22

33
import logging
4+
import os.path
45
import shutil
56
import tempfile
6-
from os.path import isdir, isfile, join
77
from typing import TYPE_CHECKING
88
from urllib.parse import urlsplit
99

1010
from mkdocs.commands.build import build
1111
from mkdocs.config import load_config
1212
from mkdocs.livereload import LiveReloadServer, _serve_url
13+
from mkdocs.structure.files import Files
1314

1415
if TYPE_CHECKING:
1516
from mkdocs.config.defaults import MkDocsConfig
@@ -35,8 +36,6 @@ def serve(
3536
whenever a file is edited.
3637
"""
3738
# Create a temporary build directory, and set some options to serve it
38-
# PY2 returns a byte string by default. The Unicode prefix ensures a Unicode
39-
# string is returned. And it makes MkDocs temp dirs easier to identify.
4039
site_dir = tempfile.mkdtemp(prefix='mkdocs_')
4140

4241
def get_config():
@@ -58,22 +57,42 @@ def get_config():
5857
mount_path = urlsplit(config.site_url or '/').path
5958
config.site_url = serve_url = _serve_url(host, port, mount_path)
6059

60+
files: Files = Files(())
61+
6162
def builder(config: MkDocsConfig | None = None):
6263
log.info("Building documentation...")
6364
if config is None:
6465
config = get_config()
6566
config.site_url = serve_url
6667

67-
build(config, serve_url=None if is_clean else serve_url, dirty=is_dirty)
68+
nonlocal files
69+
files = build(config, serve_url=None if is_clean else serve_url, dirty=is_dirty)
70+
71+
def file_hook(path: str) -> str | None:
72+
f = files.get_file_from_path(path)
73+
if f is not None and f.is_copyless_static_file:
74+
return f.abs_src_path
75+
return None
76+
77+
def get_file(path: str) -> str | None:
78+
if new_path := file_hook(path):
79+
return os.path.join(site_dir, new_path)
80+
if os.path.isfile(try_path := os.path.join(site_dir, path)):
81+
return try_path
82+
return None
6883

6984
server = LiveReloadServer(
70-
builder=builder, host=host, port=port, root=site_dir, mount_path=mount_path
85+
builder=builder,
86+
host=host,
87+
port=port,
88+
root=site_dir,
89+
file_hook=file_hook,
90+
mount_path=mount_path,
7191
)
7292

7393
def error_handler(code) -> bytes | None:
7494
if code in (404, 500):
75-
error_page = join(site_dir, f'{code}.html')
76-
if isfile(error_page):
95+
if error_page := get_file(f'{code}.html'):
7796
with open(error_page, 'rb') as f:
7897
return f.read()
7998
return None
@@ -108,5 +127,5 @@ def error_handler(code) -> bytes | None:
108127
server.shutdown()
109128
finally:
110129
config.plugins.on_shutdown()
111-
if isdir(site_dir):
130+
if os.path.isdir(site_dir):
112131
shutil.rmtree(site_dir)

mkdocs/livereload/__init__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ def __init__(
101101
host: str,
102102
port: int,
103103
root: str,
104+
*,
105+
file_hook: Callable[[str], str | None] = lambda path: None,
104106
mount_path: str = "/",
105107
polling_interval: float = 0.5,
106108
shutdown_delay: float = 0.25,
@@ -112,6 +114,7 @@ def __init__(
112114
except Exception:
113115
pass
114116
self.root = os.path.abspath(root)
117+
self.file_hook = file_hook
115118
self.mount_path = _normalize_mount_path(mount_path)
116119
self.url = _serve_url(host, port, mount_path)
117120
self.build_delay = 0.1
@@ -289,7 +292,6 @@ def condition():
289292
rel_file_path += "index.html"
290293
# Prevent directory traversal - normalize the path.
291294
rel_file_path = posixpath.normpath("/" + rel_file_path).lstrip("/")
292-
file_path = os.path.join(self.root, rel_file_path)
293295
elif path == "/":
294296
start_response("302 Found", [("Location", urllib.parse.quote(self.mount_path))])
295297
return []
@@ -298,13 +300,20 @@ def condition():
298300

299301
# Wait until the ongoing rebuild (if any) finishes, so we're not serving a half-built site.
300302
with self._epoch_cond:
301-
self._epoch_cond.wait_for(lambda: self._visible_epoch == self._wanted_epoch)
303+
file_path = self.file_hook(rel_file_path)
304+
if file_path is None:
305+
file_path = os.path.join(self.root, rel_file_path)
306+
307+
self._epoch_cond.wait_for(lambda: self._visible_epoch == self._wanted_epoch)
302308
epoch = self._visible_epoch
303309

304310
try:
305311
file: BinaryIO = open(file_path, "rb")
306312
except OSError:
307-
if not path.endswith("/") and os.path.isfile(os.path.join(file_path, "index.html")):
313+
if not path.endswith("/") and (
314+
self.file_hook(rel_file_path) is not None
315+
or os.path.isfile(os.path.join(file_path, "index.html"))
316+
):
308317
start_response("302 Found", [("Location", urllib.parse.quote(path) + "/")])
309318
return []
310319
return None # Not found

mkdocs/structure/files.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def copy_static_files(
116116
*,
117117
inclusion: Callable[[InclusionLevel], bool] = InclusionLevel.is_included,
118118
) -> None:
119-
"""Copy static files from source to destination."""
119+
"""Soft-deprecated, do not use."""
120120
for file in self:
121121
if not file.is_documentation_page() and inclusion(file.inclusion):
122122
file.copy_file(dirty)
@@ -464,6 +464,10 @@ def content_string(self, value: str):
464464
self._content = value
465465
self.abs_src_path = None
466466

467+
@utils.weak_property
468+
def is_copyless_static_file(self) -> bool:
469+
return self.abs_src_path is not None and self.dest_uri == self.src_uri
470+
467471
def copy_file(self, dirty: bool = False) -> None:
468472
"""Copy source file to destination, ensuring parent directories exist."""
469473
if dirty and not self.is_modified():

0 commit comments

Comments
 (0)