mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-08 21:14:47 +09:00
- RegistryService: configurable SERVERKIT_REGISTRY_URL, TTL cache, offline fallback (last-good → bundled app/data/registry_index.json); read-only, never auto-installs. GET /api/v1/marketplace/registry + registry install endpoint. - Checksum-verified install: install_from_url(expected_sha256) hard-fails on mismatch; force= path enables in-place updates. - Update flow v1: check_for_updates() + GET /plugins/updates + POST /plugins/<id>/update; respects min/max_panel_version via new app/utils/version.py (get_panel_version + semver compare). - serverkit-gui listed as the first registry entry (bundled index). - Docs: EXTENSIONS_REGISTRY.md (index format + publisher/review guide). - Tests: test_registry.py (fetch/cache, checksum, compat gate, update flow). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88 lines
2.7 KiB
Python
88 lines
2.7 KiB
Python
"""Panel version helpers.
|
|
|
|
Single source for "what version is this panel" plus semver-ish comparison used by
|
|
extension compatibility gates (min_panel_version / max_panel_version) and the
|
|
update flow. The version string lives in the repo-root VERSION file.
|
|
"""
|
|
import os
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_cached_version = None
|
|
|
|
|
|
def get_panel_version():
|
|
"""Return the panel version from the VERSION file, or '0.0.0' if unknown.
|
|
|
|
Cached after first read (the file doesn't change while the process runs).
|
|
"""
|
|
global _cached_version
|
|
if _cached_version is not None:
|
|
return _cached_version
|
|
|
|
here = os.path.dirname(os.path.abspath(__file__)) # backend/app/utils
|
|
backend_root = os.path.dirname(os.path.dirname(here)) # backend/
|
|
candidates = [
|
|
'/opt/serverkit/VERSION',
|
|
os.path.join(backend_root, '..', 'VERSION'),
|
|
os.path.join(backend_root, 'VERSION'),
|
|
]
|
|
version = '0.0.0'
|
|
for path in candidates:
|
|
try:
|
|
if os.path.exists(path):
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
version = f.read().strip() or version
|
|
break
|
|
except Exception:
|
|
pass
|
|
_cached_version = version
|
|
return version
|
|
|
|
|
|
def _parse(v):
|
|
"""Best-effort parse into a comparable object. Uses packaging when possible,
|
|
falling back to a tuple of leading integer components."""
|
|
if not v:
|
|
return None
|
|
try:
|
|
from packaging.version import Version
|
|
return Version(str(v))
|
|
except Exception:
|
|
parts = []
|
|
for chunk in str(v).split('.'):
|
|
num = ''.join(ch for ch in chunk if ch.isdigit())
|
|
parts.append(int(num) if num else 0)
|
|
return tuple(parts) if parts else None
|
|
|
|
|
|
def compare_versions(a, b):
|
|
"""Return -1/0/1 for a<b / a==b / a>b. Unparseable values sort as equal."""
|
|
pa, pb = _parse(a), _parse(b)
|
|
if pa is None or pb is None:
|
|
return 0
|
|
try:
|
|
if pa < pb:
|
|
return -1
|
|
if pa > pb:
|
|
return 1
|
|
return 0
|
|
except TypeError:
|
|
return 0
|
|
|
|
|
|
def version_satisfies(current, min_version=None, max_version=None):
|
|
"""True if `current` is within [min_version, max_version] (inclusive).
|
|
|
|
Missing bounds are open. Unparseable bounds are ignored (fail open) so a
|
|
malformed manifest never hard-blocks an install for the wrong reason.
|
|
"""
|
|
if min_version and _parse(min_version) is not None:
|
|
if compare_versions(current, min_version) < 0:
|
|
return False
|
|
if max_version and _parse(max_version) is not None:
|
|
if compare_versions(current, max_version) > 0:
|
|
return False
|
|
return True
|