Procházet zdrojové kódy

Cachebusting via include timestamp into SHA256 (#4539)

Related discussion:
https://github.com/zauberzeug/nicegui/discussions/4532#discussioncomment-12644326

TL-DR: 

* NiceGUI about to cache the files served more aggressively, with up to
1 year of immutable cache planned so far.
* For developers of custom components, it means that manual
cache-busting via renaming the JS file would have otherwise needed to be
done. (Think: `class Counter(Element,
component='counter-final-updated-fix2.js'):`)
* This could be troublesome, so cache busting is now handled by NiceGUI,
for which the path the file is served over will be dependent on file
content, so any change in the file content invalidates the browser cache
useless and force re-fetch.

Potential cons: 
* Slightly slower server startup. 
* If `compute_path` is ever invoked with a folder (which I don't see why
you would do that, and in reality it never does), then cache busting
would not work.

Tested with: 

https://github.com/zauberzeug/nicegui/tree/main/examples/custom_vue_component
* Changes in the `.vue` files immediately propagate before and after the
PR.
* Changes in the `.js` files immediately propagate **after the PR
only.**
* _Did not bother testing `resource`, since it would probably also
work._

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Evan Chan před 4 dny
rodič
revize
608c3e8834
3 změnil soubory, kde provedl 33 přidání a 23 odebrání
  1. 16 15
      nicegui/dependencies.py
  2. 10 5
      nicegui/element.py
  3. 7 3
      nicegui/helpers.py

+ 16 - 15
nicegui/dependencies.py

@@ -1,8 +1,9 @@
 from __future__ import annotations
 
+import functools
 from dataclasses import dataclass
 from pathlib import Path
-from typing import TYPE_CHECKING, Dict, Iterable, List, Set, Tuple
+from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
 
 import vbuild
 
@@ -58,14 +59,14 @@ libraries: Dict[str, Library] = {}
 resources: Dict[str, Resource] = {}
 
 
-def register_vue_component(path: Path) -> Component:
+def register_vue_component(path: Path, *, max_time: Optional[float]) -> Component:
     """Register a .vue or .js Vue component.
 
     Single-file components (.vue) are built right away
     to delegate this "long" process to the bootstrap phase
     and to avoid building the component on every single request.
     """
-    key = compute_key(path)
+    key = compute_key(path, max_time=max_time)
     name = _get_name(path)
     if path.suffix == '.vue':
         if key in vue_components and vue_components[key].path == path:
@@ -83,9 +84,9 @@ def register_vue_component(path: Path) -> Component:
     raise ValueError(f'Unsupported component type "{path.suffix}"')
 
 
-def register_library(path: Path, *, expose: bool = False) -> Library:
+def register_library(path: Path, *, expose: bool = False, max_time: Optional[float]) -> Library:
     """Register a *.js library."""
-    key = compute_key(path)
+    key = compute_key(path, max_time=max_time)
     name = _get_name(path)
     if path.suffix in {'.js', '.mjs'}:
         if key in libraries and libraries[key].path == path:
@@ -96,9 +97,9 @@ def register_library(path: Path, *, expose: bool = False) -> Library:
     raise ValueError(f'Unsupported library type "{path.suffix}"')
 
 
-def register_resource(path: Path) -> Resource:
+def register_resource(path: Path, *, max_time: Optional[float]) -> Resource:
     """Register a resource."""
-    key = compute_key(path)
+    key = compute_key(path, max_time=max_time)
     if key in resources and resources[key].path == path:
         return resources[key]
     assert key not in resources, f'Duplicate resource {key}'
@@ -106,20 +107,20 @@ def register_resource(path: Path) -> Resource:
     return resources[key]
 
 
-def compute_key(path: Path) -> str:
+@functools.lru_cache(maxsize=None)
+def compute_key(path: Path, *, max_time: Optional[float]) -> str:
     """Compute a key for a given path using a hash function.
 
     If the path is relative to the NiceGUI base directory, the key is computed from the relative path.
     """
-    nicegui_base = Path(__file__).parent
-    is_file = path.is_file()
+    NICEGUI_BASE = Path(__file__).parent
     try:
-        path = path.relative_to(nicegui_base)
+        rel_path = path.relative_to(NICEGUI_BASE)
     except ValueError:
-        pass
-    if is_file:
-        return f'{hash_file_path(path.parent)}/{path.name}'
-    return f'{hash_file_path(path)}'
+        rel_path = path
+    if path.is_file():
+        return f'{hash_file_path(rel_path.parent, max_time=max_time)}/{path.name}'
+    return hash_file_path(rel_path, max_time=max_time)
 
 
 def _get_name(path: Path) -> str:

+ 10 - 5
nicegui/element.py

@@ -116,17 +116,21 @@ class Element(Visibility):
         cls.extra_libraries = copy(cls.extra_libraries)
         cls.exposed_libraries = copy(cls.exposed_libraries)
         if component:
+            max_time = max((path.stat().st_mtime for path in glob_absolute_paths(component)), default=None)
             for path in glob_absolute_paths(component):
-                cls.component = register_vue_component(path)
+                cls.component = register_vue_component(path, max_time=max_time)
         for library in libraries:
+            max_time = max((path.stat().st_mtime for path in glob_absolute_paths(library)), default=None)
             for path in glob_absolute_paths(library):
-                cls.libraries.append(register_library(path))
+                cls.libraries.append(register_library(path, max_time=max_time))
         for library in extra_libraries:
+            max_time = max((path.stat().st_mtime for path in glob_absolute_paths(library)), default=None)
             for path in glob_absolute_paths(library):
-                cls.extra_libraries.append(register_library(path))
+                cls.extra_libraries.append(register_library(path, max_time=max_time))
         for library in exposed_libraries + dependencies:
+            max_time = max((path.stat().st_mtime for path in glob_absolute_paths(library)), default=None)
             for path in glob_absolute_paths(library):
-                cls.exposed_libraries.append(register_library(path, expose=True))
+                cls.exposed_libraries.append(register_library(path, expose=True, max_time=max_time))
 
         cls._default_props = copy(cls._default_props)
         cls._default_classes = copy(cls._default_classes)
@@ -140,7 +144,8 @@ class Element(Visibility):
 
         :param path: path to the resource (e.g. folder with CSS and JavaScript files)
         """
-        resource = register_resource(Path(path))
+        path_ = Path(path)
+        resource = register_resource(path_, max_time=path_.stat().st_mtime)
         self._props['resource_path'] = f'/_nicegui/{__version__}/resources/{resource.key}'
 
     def add_slot(self, name: str, template: Optional[str] = None) -> Slot:

+ 7 - 3
nicegui/helpers.py

@@ -3,6 +3,7 @@ import functools
 import hashlib
 import os
 import socket
+import struct
 import threading
 import time
 import webbrowser
@@ -52,9 +53,12 @@ def is_file(path: Optional[Union[str, Path]]) -> bool:
         return False
 
 
-def hash_file_path(path: Path) -> str:
-    """Hash the given path."""
-    return hashlib.sha256(path.as_posix().encode()).hexdigest()[:32]
+def hash_file_path(path: Path, *, max_time: Optional[float] = None) -> str:
+    """Hash the given path based on its string representation and optionally the last modification time of given files."""
+    hasher = hashlib.sha256(path.as_posix().encode())
+    if max_time is not None:
+        hasher.update(struct.pack('!d', max_time))
+    return hasher.hexdigest()[:32]
 
 
 def is_port_open(host: str, port: int) -> bool: