This commit is contained in:
2026-01-18 13:10:31 -08:00
parent 66e6c6eb72
commit 3ab122a55d
5 changed files with 175 additions and 88 deletions

View File

@@ -285,6 +285,10 @@ class HydrusNetwork:
# Some endpoints are naturally "missing" sometimes and should not spam logs.
if status == 404 and spec.endpoint.rstrip("/") == "/get_files/file_path":
# Some Hydrus deployments do not expose local file system paths via
# /get_files/file_path. Treat 404 as 'not supported' and let callers
# fall back to HTTP download URLs instead of raising an error.
logger.debug(f"{self._log_prefix()} /get_files/file_path returned 404 (not supported) - caller should fallback to HTTP")
return {}
logger.error(f"{self._log_prefix()} HTTP {status}: {message}")
@@ -1572,8 +1576,8 @@ def is_available(config: dict[str,
"""Check if Hydrus is available and accessible.
Performs a lightweight probe to verify:
- Hydrus URL is configured
- Can connect to Hydrus URL/port
- At least one configured Hydrus instance has URL + API configured
- A TCP connection to that instance's host/port can be established
Results are cached per session unless use_cache=False.
@@ -1593,16 +1597,22 @@ def is_available(config: dict[str,
# Use new config helpers first, fallback to old method
from SYS.config import get_hydrus_url, get_hydrus_access_key
url = (get_hydrus_url(config, "home") or "").strip()
if not url:
reason = "Hydrus URL not configured (check config.conf store.hydrusnetwork.home.URL)"
_HYDRUS_AVAILABLE = False
_HYDRUS_UNAVAILABLE_REASON = reason
return False, reason
# Collect candidate instances (prioritize 'home')
store_block = (config or {}).get("store") or {}
hydrus_block = store_block.get("hydrusnetwork") if isinstance(store_block, dict) else {}
access_key = get_hydrus_access_key(config, "home") or ""
if not access_key:
reason = "Hydrus access key not configured"
candidate_names: list[str] = []
if isinstance(hydrus_block, dict) and hydrus_block:
# Prefer 'home' first when present for backwards compatibility
names = list(hydrus_block.keys())
if "home" in names:
candidate_names = ["home"] + [n for n in names if n != "home"]
else:
candidate_names = names
# If no configured instances, keep previous behavior for clearer message
if not candidate_names:
reason = "Hydrus URL not configured (check config.conf store.hydrusnetwork.home.URL)"
_HYDRUS_AVAILABLE = False
_HYDRUS_UNAVAILABLE_REASON = reason
return False, reason
@@ -1613,37 +1623,46 @@ def is_available(config: dict[str,
except (TypeError, ValueError):
timeout = 5.0
try:
# Simple TCP connection test to URL/port
import socket
from urllib.parse import urlparse
import socket
from urllib.parse import urlparse
parsed = urlparse(url)
hostname = parsed.hostname or "localhost"
port = parsed.port or (443 if parsed.scheme == "https" else 80)
errors: list[str] = []
for name in candidate_names:
url = (get_hydrus_url(config, name) or "").strip()
access_key = get_hydrus_access_key(config, name) or ""
if not url:
errors.append(f"Hydrus URL not configured for instance '{name}'")
continue
if not access_key:
errors.append(f"Hydrus access key not configured for instance '{name}'")
continue
# Try to connect to the host/port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
result = sock.connect_ex((hostname, port))
if result == 0:
_HYDRUS_AVAILABLE = True
_HYDRUS_UNAVAILABLE_REASON = None
return True, None
else:
reason = f"Cannot connect to {hostname}:{port}"
_HYDRUS_AVAILABLE = False
_HYDRUS_UNAVAILABLE_REASON = reason
return False, reason
finally:
sock.close()
parsed = urlparse(url)
hostname = parsed.hostname or "localhost"
port = parsed.port or (443 if parsed.scheme == "https" else 80)
except Exception as exc:
reason = str(exc)
_HYDRUS_AVAILABLE = False
_HYDRUS_UNAVAILABLE_REASON = reason
return False, reason
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
result = sock.connect_ex((hostname, port))
if result == 0:
_HYDRUS_AVAILABLE = True
_HYDRUS_UNAVAILABLE_REASON = None
return True, None
errors.append(f"Cannot connect to {hostname}:{port} (instance '{name}')")
finally:
sock.close()
except Exception as exc:
errors.append(str(exc))
continue
# No candidate succeeded
_HYDRUS_AVAILABLE = False
reason = "; ".join(errors[:3]) if errors else "No Hydrus instances configured with URL and API"
_HYDRUS_UNAVAILABLE_REASON = reason
return False, reason
def is_hydrus_available(config: dict[str, Any]) -> bool:
@@ -1659,55 +1678,101 @@ def is_hydrus_available(config: dict[str, Any]) -> bool:
return available
def get_client(config: dict[str, Any]) -> HydrusNetwork:
def get_client(config: dict[str, Any], instance_name: str | None = None) -> HydrusNetwork:
"""Create and return a Hydrus client.
If `instance_name` is provided, return a client for that named instance.
If omitted, prefer the 'home' instance when configured, otherwise fall back
to the first configured Hydrus instance found in `config['store']['hydrusnetwork']`.
Uses access-key authentication by default (no session key acquisition).
A session key may still be acquired explicitly by calling
`HydrusNetwork.ensure_session_key()`.
Args:
config: Configuration dict with Hydrus settings
instance_name: Optional named Hydrus instance to use (e.g. 'rpi')
Returns:
HydrusClient instance
HydrusNetwork client instance
Raises:
RuntimeError: If Hydrus is not configured or unavailable
"""
# Check availability first - if unavailable, raise immediately
# Perform a lightweight availability check first - this checks any configured instance.
available, reason = is_available(config)
if not available:
raise RuntimeError(f"Hydrus is unavailable: {reason}")
# We will still attempt to instantiate a client for a configured instance even if the
# availability probe reported unreachable (this keeps behavior resilient in mixed
# network setups), but if no configured instance exists we'll raise below.
from SYS.config import get_hydrus_url, get_hydrus_access_key
# Use new config helpers
hydrus_url = (get_hydrus_url(config, "home") or "").strip()
if not hydrus_url:
raise RuntimeError(
"Hydrus URL is not configured (check config.conf store.hydrusnetwork.home.URL)"
)
chosen_instance: str | None = None
if instance_name:
chosen_instance = str(instance_name).strip()
# Validate existence of configuration
url_candidate = (get_hydrus_url(config, chosen_instance) or "").strip()
if not url_candidate:
raise RuntimeError(f"Hydrus URL is not configured for instance '{chosen_instance}'")
access_key_candidate = get_hydrus_access_key(config, chosen_instance) or ""
if not access_key_candidate:
raise RuntimeError(f"Hydrus access key is not configured for instance '{chosen_instance}'")
else:
# Determine candidate instances from config and prefer 'home' when present
store_block = (config or {}).get("store") or {}
hydrus_block = store_block.get("hydrusnetwork") if isinstance(store_block, dict) else {}
candidate_names: list[str] = []
if isinstance(hydrus_block, dict) and hydrus_block:
names = list(hydrus_block.keys())
if "home" in names:
candidate_names = ["home"] + [n for n in names if n != "home"]
else:
candidate_names = names
# Try to pick the first instance with URL+API configured
for name in candidate_names:
url_candidate = (get_hydrus_url(config, name) or "").strip()
access_key_candidate = get_hydrus_access_key(config, name) or ""
if url_candidate and access_key_candidate:
chosen_instance = name
break
# If nothing suitable found in config, fall back to 'home' behavior (for backwards compatibility)
if chosen_instance is None:
hydrus_url = (get_hydrus_url(config, "home") or "").strip()
if not hydrus_url:
raise RuntimeError(
"Hydrus URL is not configured (check config.conf store.hydrusnetwork.home.URL)"
)
chosen_instance = "home"
# Now we have a chosen instance name; resolve its URL and access key (these validations are defensive)
hydrus_url = (get_hydrus_url(config, chosen_instance) or "").strip()
access_key = get_hydrus_access_key(config, chosen_instance) or ""
if not hydrus_url:
raise RuntimeError(f"Hydrus URL is not configured for instance '{chosen_instance}'")
if not access_key:
raise RuntimeError(f"Hydrus access key is not configured for instance '{chosen_instance}'")
access_key = get_hydrus_access_key(config, "home") or ""
timeout_raw = config.get("HydrusNetwork_Request_Timeout")
try:
timeout = float(timeout_raw) if timeout_raw is not None else 60.0
except (TypeError, ValueError):
timeout = 60.0
# Create cache key from URL and access key
cache_key = f"{hydrus_url}#{access_key}"
# Create cache key from URL, access key, and instance name
cache_key = f"{hydrus_url}#{access_key}#{chosen_instance}"
# Check if we have a cached client
if cache_key in _hydrus_client_cache:
return _hydrus_client_cache[cache_key]
# Create new client
client = HydrusNetwork(hydrus_url, access_key, timeout)
# Cache the client
# Create new client and cache it
client = HydrusNetwork(hydrus_url, access_key, timeout, instance_name=chosen_instance)
_hydrus_client_cache[cache_key] = client
return client
return client