v0.142.0
 1from __future__ import annotations
 2
 3import hashlib
 4import json
 5from functools import cache
 6
 7from plain.runtime import PLAIN_TEMP_PATH
 8
 9_FINGERPRINT_LENGTH = 7
10
11
12class AssetsManifest(dict[str, str | None]):
13    """
14    A manifest of compiled assets.
15
16    Keys are all compiled asset paths. Values are either:
17    - A string path to redirect to (original → fingerprinted)
18    - None if this is the final path (no redirect needed)
19
20    Assets not in the manifest were not compiled.
21    """
22
23    def __init__(self):
24        self.path = PLAIN_TEMP_PATH / "assets" / "manifest.json"
25        self.fingerprinted_paths: set[str] = set()
26
27    def load(self) -> None:
28        if self.path.exists():
29            with open(self.path) as f:
30                self.update(json.load(f))
31        # Build set of fingerprinted paths (redirect targets from original → fingerprinted mappings)
32        self.fingerprinted_paths = {v for v in self.values() if v is not None}
33
34    def save(self) -> None:
35        with open(self.path, "w") as f:
36            json.dump(self, f, indent=2)
37
38    def add_fingerprinted(self, original_path: str, fingerprinted_path: str) -> None:
39        """Add a fingerprinted asset.
40
41        Creates two entries:
42        - original_path -> fingerprinted_path (redirect)
43        - fingerprinted_path -> None (terminal)
44        """
45        self[original_path] = fingerprinted_path
46        self[fingerprinted_path] = None
47        self.fingerprinted_paths.add(fingerprinted_path)
48
49    def add_non_fingerprinted(self, path: str) -> None:
50        """Add a non-fingerprinted asset (terminal, no redirect)."""
51        self[path] = None
52
53    def is_fingerprinted(self, path: str) -> bool:
54        """Check if a path is a fingerprinted path (pointed to by another entry)."""
55        return path in self.fingerprinted_paths
56
57    def resolve(self, url_path: str) -> str | None:
58        """
59        Get the best compiled path for an asset URL path.
60
61        Returns the redirect target if one exists, otherwise the path itself if compiled.
62        Returns None if the asset is not in the manifest (was not compiled).
63        """
64        if url_path not in self:
65            return None
66        # If there's a redirect target, use it; otherwise use the path itself
67        return self[url_path] or url_path
68
69
70@cache
71def get_manifest() -> AssetsManifest:
72    """
73    A cached function for loading the assets manifest,
74    so we don't have to keep loading it from disk over and over.
75    """
76    manifest = AssetsManifest()
77    manifest.load()
78    return manifest
79
80
81def compute_fingerprint(file_path: str) -> str:
82    """Compute an MD5-based fingerprint hash for a file."""
83    with open(file_path, "rb") as f:
84        content = f.read()
85
86    return hashlib.md5(content, usedforsecurity=False).hexdigest()[:_FINGERPRINT_LENGTH]