Implemented HTTP caching and updates on wwwroot, much faster page loads.
This commit is contained in:
		
							
								
								
									
										71
									
								
								cista/app.py
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								cista/app.py
									
									
									
									
									
								
							| @@ -2,15 +2,21 @@ import mimetypes | ||||
| from importlib.resources import files | ||||
| from urllib.parse import unquote | ||||
|  | ||||
| import sanic.helpers | ||||
| import asyncio | ||||
| import brotli | ||||
| from sanic import Blueprint, Sanic, raw | ||||
| from blake3 import blake3 | ||||
| from sanic import Blueprint, Sanic, raw, empty | ||||
| from sanic.exceptions import Forbidden, NotFound | ||||
| from wsgiref.handlers import format_date_time | ||||
|  | ||||
| from cista import auth, config, session, watching | ||||
| from cista.api import bp | ||||
| from cista.util.apphelpers import handle_sanic_exception | ||||
|  | ||||
| # Workaround until Sanic PR #2824 is merged | ||||
| sanic.helpers._ENTITY_HEADERS = frozenset() | ||||
|  | ||||
| app = Sanic("cista", strict_slashes=True) | ||||
| app.blueprint(auth.bp) | ||||
| app.blueprint(bp) | ||||
| @@ -63,8 +69,12 @@ www = {} | ||||
|  | ||||
|  | ||||
| @app.before_server_start | ||||
| def load_wwwroot(app): | ||||
| async def load_wwwroot(*_ignored): | ||||
|     global www | ||||
|     www = await asyncio.get_event_loop().run_in_executor(None, _load_wwwroot, www) | ||||
|  | ||||
|  | ||||
| def _load_wwwroot(www): | ||||
|     wwwnew = {} | ||||
|     base = files("cista") / "wwwroot" | ||||
|     paths = ["."] | ||||
| @@ -77,36 +87,71 @@ def load_wwwroot(app): | ||||
|                 continue | ||||
|             name = p.relative_to(base).as_posix() | ||||
|             mime = mimetypes.guess_type(name)[0] or "application/octet-stream" | ||||
|             mtime = p.stat().st_mtime | ||||
|             data = p.read_bytes() | ||||
|             etag = blake3(data).hexdigest(length=8) | ||||
|             if name == "index.html": | ||||
|                 name = "" | ||||
|             # Use old data if not changed | ||||
|             if name in www and www[name][0] == data: | ||||
|             if name in www and www[name][2]["etag"] == etag: | ||||
|                 wwwnew[name] = www[name] | ||||
|                 continue | ||||
|             cached = name.startswith("assets/") | ||||
|             headers = { | ||||
|                 "etag": etag, | ||||
|                 "last-modified": format_date_time(mtime), | ||||
|                 "cache-control": "max-age=31536000, immutable" | ||||
|                 if cached | ||||
|                 else "no-cache", | ||||
|                 "content-type": mime, | ||||
|             } | ||||
|             # Precompress with Brotli | ||||
|             br = brotli.compress(data) | ||||
|             if len(br) >= len(data): | ||||
|                 br = False | ||||
|             wwwnew[name] = data, br, mime | ||||
|     www = wwwnew | ||||
|             wwwnew[name] = data, br, headers | ||||
|     return wwwnew | ||||
|  | ||||
|  | ||||
| @app.add_task | ||||
| async def refresh_wwwroot(): | ||||
|     while app.debug: | ||||
|     while True: | ||||
|         try: | ||||
|             wwwold = www | ||||
|             await load_wwwroot() | ||||
|             changes = "" | ||||
|             for name in sorted(www): | ||||
|                 attr = www[name] | ||||
|                 if wwwold.get(name) == attr: | ||||
|                     continue | ||||
|                 headers = attr[2] | ||||
|                 changes += f"{headers['last-modified']} {headers['etag']} /{name}\n" | ||||
|             for name in sorted(set(wwwold) - set(www)): | ||||
|                 changes += f"Deleted /{name}\n" | ||||
|             if changes: | ||||
|                 print(f"Updated wwwroot:\n{changes}", end="", flush=True) | ||||
|         except Exception as e: | ||||
|             print("Error loading wwwroot", e) | ||||
|         if not app.debug: | ||||
|             return | ||||
|         await asyncio.sleep(0.5) | ||||
|         load_wwwroot(app) | ||||
|  | ||||
|  | ||||
| @app.get("/<path:path>", static=True) | ||||
| @app.route("/<path:path>", methods=["GET", "HEAD"]) | ||||
| async def wwwroot(req, path=""): | ||||
|     """Frontend files only""" | ||||
|     name = unquote(path) or "index.html" | ||||
|     name = unquote(path) | ||||
|     if name not in www: | ||||
|         raise NotFound(f"File not found: /{path}", extra={"name": name}) | ||||
|     data, br, mime = www[name] | ||||
|     headers = {} | ||||
|     data, br, headers = www[name] | ||||
|     if req.headers.if_none_match == headers["etag"]: | ||||
|         # The client has it cached, respond 304 Not Modified | ||||
|         return empty(304, headers=headers) | ||||
|     # Brotli compressed? | ||||
|     if br and "br" in req.headers.accept_encoding.split(", "): | ||||
|         headers["content-encoding"] = "br" | ||||
|         headers = { | ||||
|             **headers, | ||||
|             "content-encoding": "br", | ||||
|         } | ||||
|         data = br | ||||
|     return raw(data, content_type=mime, headers=headers) | ||||
|     return raw(data, headers=headers) | ||||
|   | ||||
| @@ -15,6 +15,7 @@ classifiers = [ | ||||
| ] | ||||
| dependencies = [ | ||||
|     "argon2-cffi", | ||||
|     "blake3", | ||||
|     "brotli", | ||||
|     "docopt", | ||||
|     "inotify", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Leo Vasanko
					Leo Vasanko