version updates
This commit is contained in:
@@ -12,6 +12,7 @@ from curl_cffi import requests as impersonate_requests
|
||||
import threading
|
||||
import io
|
||||
import time
|
||||
import hashlib
|
||||
from urllib.parse import urljoin
|
||||
|
||||
# Browser to impersonate at the TLS/HTTP layer. Some origins (e.g. the
|
||||
@@ -355,6 +356,63 @@ def index():
|
||||
def favicon():
|
||||
return send_from_directory(app.static_folder, 'favicon.ico')
|
||||
|
||||
# --- Frontend asset version tracking -------------------------------------
|
||||
# The client polls /api/version and, when a tracked file's content hash
|
||||
# changes, hot-swaps CSS in place or reloads the page. This lets a deploy
|
||||
# reach already-open tabs without a manual refresh.
|
||||
_FRONTEND_DIR = os.path.abspath(app.static_folder)
|
||||
_VERSION_EXTS = ('.html', '.css', '.js')
|
||||
_version_cache = {'mtime': None, 'payload': None}
|
||||
_version_lock = threading.Lock()
|
||||
|
||||
|
||||
def _scan_frontend_files():
|
||||
"""Map served relative paths -> absolute paths for tracked frontend files."""
|
||||
files = {}
|
||||
for root, _dirs, names in os.walk(_FRONTEND_DIR):
|
||||
for name in names:
|
||||
if os.path.splitext(name)[1].lower() not in _VERSION_EXTS:
|
||||
continue
|
||||
path = os.path.join(root, name)
|
||||
rel = os.path.relpath(path, _FRONTEND_DIR).replace(os.sep, '/')
|
||||
files[rel] = path
|
||||
return files
|
||||
|
||||
|
||||
def _compute_version_payload(files):
|
||||
"""Hash each tracked file's contents plus a combined version fingerprint."""
|
||||
file_hashes = {}
|
||||
combined = hashlib.md5()
|
||||
for rel in sorted(files):
|
||||
try:
|
||||
with open(files[rel], 'rb') as fh:
|
||||
digest = hashlib.md5(fh.read()).hexdigest()
|
||||
except OSError:
|
||||
continue
|
||||
file_hashes[rel] = digest
|
||||
combined.update(rel.encode('utf-8'))
|
||||
combined.update(digest.encode('utf-8'))
|
||||
return {'version': combined.hexdigest(), 'files': file_hashes}
|
||||
|
||||
|
||||
@app.route('/api/version', methods=['GET'])
|
||||
def frontend_version():
|
||||
files = _scan_frontend_files()
|
||||
# Use the newest mtime across tracked files as a cheap cache key so frequent
|
||||
# polls only re-hash contents when something on disk actually changed.
|
||||
try:
|
||||
latest_mtime = max((os.path.getmtime(p) for p in files.values()), default=0)
|
||||
except OSError:
|
||||
latest_mtime = 0
|
||||
with _version_lock:
|
||||
if _version_cache['mtime'] != latest_mtime or _version_cache['payload'] is None:
|
||||
_version_cache['payload'] = _compute_version_payload(files)
|
||||
_version_cache['mtime'] = latest_mtime
|
||||
payload = _version_cache['payload']
|
||||
resp = jsonify(payload)
|
||||
resp.headers['Cache-Control'] = 'no-store'
|
||||
return resp
|
||||
|
||||
@app.route('/api/stream', methods=['POST', 'GET', 'HEAD'])
|
||||
def stream_video():
|
||||
# Note: <video> tags perform GET. To support your POST requirement,
|
||||
|
||||
Reference in New Issue
Block a user