f
This commit is contained in:
22
API/HTTP.py
22
API/HTTP.py
@@ -52,12 +52,22 @@ def _resolve_verify_value(verify_ssl: bool) -> Union[bool, str]:
|
||||
|
||||
def _try_module_bundle(mod_name: str) -> Optional[str]:
|
||||
# Prefer checking sys.modules first (helps test injection / monkeypatching)
|
||||
try:
|
||||
mod = sys.modules.get(mod_name)
|
||||
if mod is None:
|
||||
mod = __import__(mod_name)
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
return None
|
||||
mod = sys.modules.get(mod_name)
|
||||
if mod is None:
|
||||
# Avoid raising ModuleNotFoundError so debuggers and callers aren't interrupted.
|
||||
# Check for module availability before attempting to import it.
|
||||
try:
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.find_spec(mod_name)
|
||||
if spec is None:
|
||||
return None
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module(mod_name)
|
||||
except Exception:
|
||||
# Treat any import/initialization failure as module not available.
|
||||
return None
|
||||
|
||||
# Common APIs that return a bundle path
|
||||
for attr in ("where", "get_ca_bundle", "bundle_path", "get_bundle_path", "get_bundle"):
|
||||
|
||||
@@ -27,7 +27,8 @@ from Provider.metadata_provider import (
|
||||
)
|
||||
from SYS.utils import unique_path
|
||||
|
||||
_ARCHIVE_VERIFY_VALUE = get_requests_verify_value()
|
||||
# Resolve lazily to avoid import-time module checks (prevents debugger first-chance noise)
|
||||
_ARCHIVE_VERIFY_VALUE = None # will be resolved on first session creation
|
||||
_DEFAULT_ARCHIVE_SCALE = 4
|
||||
_QUALITY_TO_ARCHIVE_SCALE = {
|
||||
"high": 2,
|
||||
@@ -38,6 +39,9 @@ _QUALITY_TO_ARCHIVE_SCALE = {
|
||||
|
||||
def _create_archive_session() -> requests.Session:
|
||||
session = requests.Session()
|
||||
global _ARCHIVE_VERIFY_VALUE
|
||||
if _ARCHIVE_VERIFY_VALUE is None:
|
||||
_ARCHIVE_VERIFY_VALUE = get_requests_verify_value()
|
||||
session.verify = _ARCHIVE_VERIFY_VALUE
|
||||
return session
|
||||
|
||||
|
||||
@@ -725,6 +725,92 @@ def save(config: Dict[str, Any]) -> int:
|
||||
return save_config(config)
|
||||
|
||||
|
||||
def save_config_and_verify(config: Dict[str, Any], retries: int = 3, delay: float = 0.15) -> int:
|
||||
"""Save configuration and verify crucial keys persisted to disk.
|
||||
|
||||
This helper performs a best-effort verification loop that reloads the
|
||||
configuration from disk and confirms that modified API key entries (e.g.
|
||||
AllDebrid) were written successfully. If verification fails after the
|
||||
configured number of retries, a RuntimeError is raised.
|
||||
"""
|
||||
# Detect an API key that should be verified (provider or store-backed)
|
||||
expected_key = None
|
||||
try:
|
||||
providers = config.get("provider", {}) if isinstance(config, dict) else {}
|
||||
if isinstance(providers, dict):
|
||||
entry = providers.get("alldebrid")
|
||||
if entry is not None:
|
||||
# _extract_api_key is a small internal helper; reuse the implementation here
|
||||
if isinstance(entry, dict):
|
||||
for k in ("api_key", "API_KEY", "apikey", "APIKEY"):
|
||||
v = entry.get(k)
|
||||
if isinstance(v, str) and v.strip():
|
||||
expected_key = v.strip()
|
||||
break
|
||||
elif isinstance(entry, str) and entry.strip():
|
||||
expected_key = entry.strip()
|
||||
if not expected_key:
|
||||
store_block = config.get("store", {}) if isinstance(config, dict) else {}
|
||||
debrid = store_block.get("debrid") if isinstance(store_block, dict) else None
|
||||
if isinstance(debrid, dict):
|
||||
srv = debrid.get("all-debrid")
|
||||
if isinstance(srv, dict):
|
||||
for k in ("api_key", "API_KEY", "apikey", "APIKEY"):
|
||||
v = srv.get(k)
|
||||
if isinstance(v, str) and v.strip():
|
||||
expected_key = v.strip()
|
||||
break
|
||||
elif isinstance(srv, str) and srv.strip():
|
||||
expected_key = srv.strip()
|
||||
except Exception:
|
||||
expected_key = None
|
||||
|
||||
last_exc: Exception | None = None
|
||||
for attempt in range(1, max(1, int(retries)) + 1):
|
||||
try:
|
||||
saved = save_config(config)
|
||||
if not expected_key:
|
||||
# Nothing special to verify; return success.
|
||||
return saved
|
||||
|
||||
# Reload directly from disk and compare the canonical debrid/provider keys
|
||||
clear_config_cache()
|
||||
reloaded = load_config()
|
||||
# Provider-level key
|
||||
prov_block = reloaded.get("provider", {}) if isinstance(reloaded, dict) else {}
|
||||
prov_key = None
|
||||
if isinstance(prov_block, dict):
|
||||
aentry = prov_block.get("alldebrid")
|
||||
if isinstance(aentry, dict):
|
||||
for k in ("api_key", "API_KEY", "apikey", "APIKEY"):
|
||||
v = aentry.get(k)
|
||||
if isinstance(v, str) and v.strip():
|
||||
prov_key = v.strip()
|
||||
break
|
||||
elif isinstance(aentry, str) and aentry.strip():
|
||||
prov_key = aentry.strip()
|
||||
|
||||
# Store-level key
|
||||
try:
|
||||
store_key = get_debrid_api_key(reloaded, service="All-debrid")
|
||||
except Exception:
|
||||
store_key = None
|
||||
|
||||
if prov_key == expected_key or store_key == expected_key:
|
||||
return saved
|
||||
|
||||
# Not yet persisted; log and retry
|
||||
log(f"Warning: Post-save verification attempt {attempt} failed (expected key not found in DB). Retrying...")
|
||||
time.sleep(delay * attempt)
|
||||
except Exception as exc:
|
||||
last_exc = exc
|
||||
log(f"Warning: save and verify attempt {attempt} failed: {exc}")
|
||||
time.sleep(delay * attempt)
|
||||
|
||||
# All retries exhausted
|
||||
raise RuntimeError(f"Post-save verification failed after {retries} attempts: {last_exc}")
|
||||
|
||||
|
||||
def count_changed_entries(config: Dict[str, Any]) -> int:
|
||||
"""Return the number of changed configuration entries compared to the last saved snapshot.
|
||||
|
||||
|
||||
@@ -173,6 +173,8 @@ class ConfigModal(ModalScreen):
|
||||
yield ScrollableContainer(id="fields-container")
|
||||
with Horizontal(id="config-actions"):
|
||||
yield Button("Save", variant="success", id="save-btn")
|
||||
# Durable synchronous save: waits and verifies DB persisted critical keys
|
||||
yield Button("Save (durable)", variant="primary", id="save-durable-btn")
|
||||
yield Button("Add Store", variant="primary", id="add-store-btn")
|
||||
yield Button("Add Provider", variant="primary", id="add-provider-btn")
|
||||
yield Button("Add Tool", variant="primary", id="add-tool-btn")
|
||||
@@ -790,6 +792,43 @@ class ConfigModal(ModalScreen):
|
||||
self.refresh_view()
|
||||
except Exception as exc:
|
||||
self.notify(f"Save failed: {exc}", severity="error", timeout=10)
|
||||
elif bid == "save-durable-btn":
|
||||
# Perform a synchronous, verified save and notify status to the user.
|
||||
self._synchronize_inputs_to_config()
|
||||
if not self.validate_current_editor():
|
||||
return
|
||||
if self.editing_item_name and not self._editor_has_changes():
|
||||
self.notify("No changes to save", severity="warning", timeout=3)
|
||||
return
|
||||
try:
|
||||
from SYS.config import save_config_and_verify
|
||||
|
||||
saved = save_config_and_verify(self.config_data, retries=3, delay=0.1)
|
||||
try:
|
||||
self.config_data = reload_config()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if saved == 0:
|
||||
msg = f"Configuration saved (no rows changed) to {db.db_path.name}"
|
||||
else:
|
||||
msg = f"Configuration saved ({saved} change(s)) to {db.db_path.name} (verified)"
|
||||
try:
|
||||
self.notify(msg, timeout=6)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Return to the main list view within the current category
|
||||
self.editing_item_name = None
|
||||
self.editing_item_type = None
|
||||
self.refresh_view()
|
||||
self._editor_snapshot = None
|
||||
except Exception as exc:
|
||||
self.notify(f"Durable save failed: {exc}", severity="error", timeout=10)
|
||||
try:
|
||||
log(f"Durable save failed: {exc}")
|
||||
except Exception:
|
||||
pass
|
||||
elif bid in self._button_id_map:
|
||||
action, itype, name = self._button_id_map[bid]
|
||||
if action == "edit":
|
||||
|
||||
@@ -562,3 +562,8 @@ http://10.162.158.28:45899/get_files/file?hash=5c7296f1a5544522e3d118f60080e0389
|
||||
2026-01-31T03:07:25.275447Z [DEBUG] logger.debug: DEBUG: No resolution path matched. result type=PipeObject
|
||||
2026-01-31T03:07:42.183988Z [DEBUG] add_file._resolve_source: File path could not be resolved
|
||||
2026-01-31T03:07:59.118354Z [DEBUG] logger.debug: DEBUG: [add-file] RESOLVED source: path=None, hash=N/A...
|
||||
2026-01-31T03:12:02.961829Z [DEBUG] logger.debug: DEBUG: [search-file] Calling alldebrid.search(filters={})
|
||||
2026-01-31T03:12:48.309103Z [DEBUG] logger.debug: DEBUG: [search-file] Calling alldebrid.search(filters={})
|
||||
2026-01-31T03:13:05.258091Z [DEBUG] logger.debug: DEBUG: <rich.panel.Panel object at 0x00000161B7383D10>
|
||||
2026-01-31T03:13:22.207331Z [DEBUG] logger.debug: DEBUG: <rich.panel.Panel object at 0x00000161B7383D10>
|
||||
2026-01-31T03:13:39.166965Z [DEBUG] alldebrid.search: [alldebrid] Failed to list account magnets: AllDebrid API error: The auth apikey is invalid
|
||||
|
||||
Reference in New Issue
Block a user