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