v0.142.0
  1from __future__ import annotations
  2
  3import gzip
  4import os
  5import shutil
  6from collections.abc import Iterator
  7from pathlib import Path
  8
  9from plain.runtime import PLAIN_TEMP_PATH
 10
 11from .finders import Asset, _iter_assets
 12from .manifest import AssetsManifest, compute_fingerprint
 13
 14_SKIP_COMPRESS_EXTENSIONS = (
 15    # Images
 16    ".jpg",
 17    ".jpeg",
 18    ".png",
 19    ".gif",
 20    ".webp",
 21    # Compressed files
 22    ".zip",
 23    ".gz",
 24    ".tgz",
 25    ".bz2",
 26    ".tbz",
 27    ".xz",
 28    ".br",
 29    # Fonts
 30    ".woff",
 31    ".woff2",
 32    # Video
 33    ".3gp",
 34    ".3gpp",
 35    ".asf",
 36    ".avi",
 37    ".m4v",
 38    ".mov",
 39    ".mp4",
 40    ".mpeg",
 41    ".mpg",
 42    ".webm",
 43    ".wmv",
 44)
 45
 46
 47def get_compiled_path() -> Path:
 48    """
 49    Get the path at runtime to the compiled assets directory.
 50
 51    There's no reason currently for this to be a user-facing setting.
 52    """
 53    return PLAIN_TEMP_PATH / "assets" / "compiled"
 54
 55
 56def compile_assets(
 57    *, target_dir: str, keep_original: bool, fingerprint: bool, compress: bool
 58) -> Iterator[tuple[str, str, list[str]]]:
 59    """
 60    Compile all assets to the target directory and save a JSON manifest.
 61
 62    Manifest format:
 63    - original path → fingerprinted path (if fingerprinting enabled)
 64    - fingerprinted path → None (terminal, no redirect)
 65    - original path → None (if no fingerprinting, terminal)
 66    """
 67    manifest = AssetsManifest()
 68
 69    for asset in _iter_assets():
 70        url_path = asset.url_path
 71        fingerprinted_path, compiled_paths = compile_asset(
 72            asset=asset,
 73            target_dir=target_dir,
 74            keep_original=keep_original,
 75            fingerprint=fingerprint,
 76            compress=compress,
 77        )
 78
 79        if fingerprinted_path:
 80            manifest.add_fingerprinted(url_path, fingerprinted_path)
 81            resolved_path = fingerprinted_path
 82        else:
 83            manifest.add_non_fingerprinted(url_path)
 84            resolved_path = url_path
 85
 86        yield url_path, resolved_path, compiled_paths
 87
 88    manifest.save()
 89
 90
 91def compile_asset(
 92    *,
 93    asset: Asset,
 94    target_dir: str,
 95    keep_original: bool,
 96    fingerprint: bool,
 97    compress: bool,
 98) -> tuple[str | None, list[str]]:
 99    """
100    Compile an asset to multiple output paths.
101
102    Returns the fingerprinted URL path (or None) and the list of compiled file paths.
103    """
104    compiled_paths: list[str] = []
105    fingerprinted_url_path: str | None = None
106
107    # The expected destination for the original asset
108    target_path = os.path.join(target_dir, asset.url_path)
109
110    # Make sure all the expected directories exist
111    os.makedirs(os.path.dirname(target_path), exist_ok=True)
112
113    base, extension = os.path.splitext(asset.url_path)
114
115    # Copy the original asset if requested
116    if keep_original:
117        shutil.copy(asset.absolute_path, target_path)
118        compiled_paths.append(target_path)
119
120    # Create fingerprinted version if requested
121    if fingerprint:
122        fingerprint_hash = compute_fingerprint(asset.absolute_path)
123
124        fingerprinted_basename = f"{base}.{fingerprint_hash}{extension}"
125        fingerprinted_path = os.path.join(target_dir, fingerprinted_basename)
126        shutil.copy(asset.absolute_path, fingerprinted_path)
127        compiled_paths.append(fingerprinted_path)
128
129        fingerprinted_url_path = str(os.path.relpath(fingerprinted_path, target_dir))
130
131    if compress and extension.lower() not in _SKIP_COMPRESS_EXTENSIONS:
132        for path in compiled_paths.copy():
133            gzip_path = f"{path}.gz"
134            with gzip.GzipFile(gzip_path, "wb", mtime=0) as f:
135                with open(path, "rb") as f2:
136                    f.write(f2.read())
137            compiled_paths.append(gzip_path)
138
139    return fingerprinted_url_path, compiled_paths