Env var - PAGE_WATCH_LIMIT enhancements (#4359)

This commit is contained in:
dgtlmoon
2026-09-02 13:37:55 +02:00
committed by GitHub
parent af234636b9
commit 68a445df1b
41 changed files with 290 additions and 117 deletions
+45 -17
View File
@@ -5,7 +5,8 @@ from changedetectionio.strtobool import strtobool
from changedetectionio.validate_url import is_safe_valid_url
from flask import (
flash
flash,
has_request_context
)
from flask_babel import gettext
@@ -661,9 +662,8 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore):
# NOTE: dict() is shallow copy but safe since add_watch() deepcopies it
with self.lock:
extras = dict(self.data['watching'][uuid])
new_uuid = self.add_watch(url=url, extras=extras)
watch = self.data['watching'][new_uuid]
return new_uuid
# None when add_watch() refused it (e.g. PAGE_WATCH_LIMIT), having already flashed why
return self.add_watch(url=url, extras=extras)
def url_exists(self, url):
@@ -679,6 +679,38 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore):
self.__data['watching'][uuid].clear_watch()
self.__data['watching'][uuid].commit()
@property
def watch_limit(self):
"""Total watches allowed by PAGE_WATCH_LIMIT, or None when there is no limit.
None means "unlimited" and is the normal case - the env var being absent, empty or
unparseable all leave the limit switched off entirely. There is no default.
"""
limit = os.getenv('PAGE_WATCH_LIMIT')
if not limit:
return None
try:
return int(limit)
except ValueError:
logger.warning(f"Invalid PAGE_WATCH_LIMIT value: {limit}, ignoring limit check")
return None
def watch_limit_reached(self):
"""True when the limit is set and leaves no room for another watch.
add_watch() enforces this on its own, but it can only return None. Callers that can
report something better - a 429 in the API, one flash instead of one per row in the
importers - should check this first.
"""
limit = self.watch_limit
return limit is not None and len(self.__data['watching']) >= limit
def watch_limit_message(self):
"""The single wording for a blocked add, so every UI surface says the same thing."""
return gettext("Watch limit reached ({current}/{limit} watches). Cannot add more watches.").format(
current=len(self.__data['watching']), limit=self.watch_limit
)
def add_watch(self, url, tag='', extras=None, tag_uuids=None, save_immediately=True, seed_data_dir=None):
"""
seed_data_dir: optional path to an existing directory (already in the watch's
@@ -742,25 +774,21 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore):
return False
if not is_safe_valid_url(url):
from flask import has_request_context
if has_request_context():
flash(gettext('Watch protocol is not permitted or invalid URL format'), 'error')
else:
logger.error(f"add_watch: URL '{url}' is not permitted or invalid, skipping.")
return None
# Check PAGE_WATCH_LIMIT if set
page_watch_limit = os.getenv('PAGE_WATCH_LIMIT')
if page_watch_limit:
try:
page_watch_limit = int(page_watch_limit)
current_watch_count = len(self.__data['watching'])
if current_watch_count >= page_watch_limit:
logger.error(f"Watch limit reached: {current_watch_count}/{page_watch_limit} watches. Cannot add {url}")
flash(gettext("Watch limit reached ({current}/{limit} watches). Cannot add more watches.").format(current=current_watch_count, limit=page_watch_limit), 'error')
return None
except ValueError:
logger.warning(f"Invalid PAGE_WATCH_LIMIT value: {page_watch_limit}, ignoring limit check")
# Backstop for PAGE_WATCH_LIMIT - every add path funnels through here, so nothing can
# get past the limit even if a caller forgets to pre-check watch_limit_reached().
if self.watch_limit_reached():
logger.error(f"Watch limit reached: {len(self.__data['watching'])}/{self.watch_limit} watches. Cannot add {url}")
# The CLI (-u) and the API's background import thread have no request context,
# where flash() raises instead of reporting anything
if has_request_context():
flash(self.watch_limit_message(), 'error')
return None
if tag and type(tag) == str:
# Then it's probably a string of the actual tag by name, split and add it