From ba1b8a783a05b7f6172040d4b6e6c8f602cb6dd1 Mon Sep 17 00:00:00 2001 From: blankie Date: Fri, 19 Jun 2026 02:17:11 +0000 Subject: [PATCH] base tag + LRU cache for RNS browse, sync status fixes --- src/tinyweb/handlers/rns.py | 100 +++++++++++++++----------- src/tinyweb/handlers/subscriptions.py | 25 ++++++- 2 files changed, 79 insertions(+), 46 deletions(-) diff --git a/src/tinyweb/handlers/rns.py b/src/tinyweb/handlers/rns.py index 8941f99..53a5616 100644 --- a/src/tinyweb/handlers/rns.py +++ b/src/tinyweb/handlers/rns.py @@ -1,10 +1,54 @@ import json +import time +import threading import traceback from tinyweb.db import get_db, return_db from tinyweb.rns_client import fetch_remote_page from tinyweb.templates import esc +class _PageCache: + def __init__(self, maxsize=50, ttl=300): + self._maxsize = maxsize + self._ttl = ttl + self._cache = {} + self._lock = threading.Lock() + + def get(self, key): + with self._lock: + entry = self._cache.get(key) + if entry is None: + return None + if time.time() - entry["time"] > self._ttl: + del self._cache[key] + return None + self._cache.pop(key) + self._cache[key] = entry + return entry["value"] + + def put(self, key, value): + with self._lock: + if key in self._cache: + self._cache.pop(key) + elif len(self._cache) >= self._maxsize: + oldest = next(iter(self._cache)) + del self._cache[oldest] + self._cache[key] = {"value": value, "time": time.time()} + + +_page_cache = _PageCache() + + +def _inject_base_tag(html, dest_hash): + base = f'' + head_start = html.find("= 0: + close = html.find(">", head_start) + if close >= 0: + return html[:close + 1] + base + html[close + 1:] + return f"{base}{html}" + + def _get_mesh_sites(): db = get_db() try: @@ -71,54 +115,22 @@ def handle_rns_delete_hash(body): return _redirect("/") -def _rewrite_links(html, dest_hash): - out = [] - i = 0 - while i < len(html): - href_start = html.find('href="', i) - src_start = html.find('src="', i) - action_start = html.find('action="', i) - - candidates = [] - if href_start >= 0: - candidates.append((href_start, "href", 'href="')) - if src_start >= 0: - candidates.append((src_start, "src", 'src="')) - if action_start >= 0: - candidates.append((action_start, "action", 'action="')) - - if not candidates: - out.append(html[i:]) - break - - candidates.sort() - pos, attr, prefix = candidates[0] - out.append(html[i:pos + len(prefix)]) - - value_start = pos + len(prefix) - value_end = html.find('"', value_start) - if value_end < 0: - out.append(html[value_start:]) - break - value = html[value_start:value_end] - - if value.startswith("/"): - out.append(f"/rns/{dest_hash}{value}") - else: - out.append(value) - - out.append('"') - i = value_end + 1 - - return "".join(out) - - def handle_rns_browse(path, dest_hash): prefix = f"/rns/{dest_hash}" sub_path = path[len(prefix):] if path.startswith(prefix) else "/" if not sub_path: sub_path = "/" + cache_key = (dest_hash, sub_path) + cached = _page_cache.get(cache_key) + if cached is not None: + return { + "status": 200, + "content_type": "text/html; charset=utf-8", + "body": cached, + "headers": {}, + } + try: resp = fetch_remote_page(dest_hash, sub_path) except ConnectionError as e: @@ -152,8 +164,10 @@ def handle_rns_browse(path, dest_hash): body = f"
{esc(json.dumps(data, indent=2))}
" except (json.JSONDecodeError, TypeError): body = f"
{esc(body[:2000])}
" + else: + body = _inject_base_tag(body, dest_hash) - body = _rewrite_links(body, dest_hash) + _page_cache.put(cache_key, body) return { "status": 200, diff --git a/src/tinyweb/handlers/subscriptions.py b/src/tinyweb/handlers/subscriptions.py index 0113d07..a7ba389 100644 --- a/src/tinyweb/handlers/subscriptions.py +++ b/src/tinyweb/handlers/subscriptions.py @@ -1,4 +1,5 @@ import threading +import time from datetime import datetime from tinyweb.db import get_db, return_db, get_setting, set_setting, get_site_name, index_url, clean_url @@ -10,6 +11,8 @@ from ._helpers import ( ) _sync_threads = {} +_sync_starts = {} +_SYNC_TIMEOUT = 120 MAX_API_SITES = 5000 MAX_BROWSE = 5000 @@ -142,6 +145,13 @@ def handle_subscriptions(msg=""): subs = db.execute("SELECT * FROM subscriptions ORDER BY id DESC").fetchall() finally: return_db(db) + now_t = time.time() + for sub_id, start_t in list(_sync_starts.items()): + if now_t - start_t > _SYNC_TIMEOUT: + set_setting(f"sync_status_{sub_id}", "error:Timed out") + _sync_threads.pop(sub_id, None) + _sync_starts.pop(sub_id, None) + cards = "" for s in subs: sub_id = s["id"] @@ -152,6 +162,9 @@ def handle_subscriptions(msg=""): if is_syncing: status_html = '
syncing...
' + elif sync_status.startswith("done:"): + count = sync_status[5:] + status_html = f'
synced {esc(count)} site(s)
' elif sync_status.startswith("error:"): err_msg = sync_status[6:] status_html = f'
{esc(err_msg)}
' @@ -349,9 +362,10 @@ def handle_subscription_pick(body): def _sync_subscription(sub_id): - set_setting(f"sync_status_{sub_id}", "syncing") - db = get_db() + db = None try: + set_setting(f"sync_status_{sub_id}", "syncing") + db = get_db() sub = db.execute("SELECT * FROM subscriptions WHERE id = ?", (sub_id,)).fetchone() if not sub: set_setting(f"sync_status_{sub_id}", "error:Subscription not found.") @@ -407,13 +421,17 @@ def _sync_subscription(sub_id): except Exception as e: set_setting(f"sync_status_{sub_id}", f"error:{e}") finally: - return_db(db) + if db: + return_db(db) + _sync_threads.pop(sub_id, None) + _sync_starts.pop(sub_id, None) def handle_subscription_sync(sub_id): if sub_id in _sync_threads and _sync_threads[sub_id].is_alive(): return _redirect("/subscriptions") set_setting(f"sync_status_{sub_id}", "syncing") + _sync_starts[sub_id] = time.time() t = threading.Thread(target=_sync_subscription, args=(sub_id,), daemon=True) _sync_threads[sub_id] = t t.start() @@ -454,6 +472,7 @@ def handle_subscription_syncall(): if sub_id in _sync_threads and _sync_threads[sub_id].is_alive(): continue set_setting(f"sync_status_{sub_id}", "syncing") + _sync_starts[sub_id] = time.time() t = threading.Thread(target=_sync_subscription, args=(sub_id,), daemon=True) _sync_threads[sub_id] = t t.start()