86 lines
3.1 KiB
Python
86 lines
3.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""桌面应用版本信息与云端升级策略判断。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
|
|
APP_VERSION = "1.4.25"
|
|
_VERSION_PATTERN = re.compile(
|
|
r"^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
|
|
r"(?:[-+][0-9A-Za-z.-]+)?$"
|
|
)
|
|
|
|
|
|
def normalize_version(value: Any) -> str:
|
|
"""校验并统一版本号格式,允许用户输入可选的 v 前缀。"""
|
|
text = str(value or "").strip()
|
|
match = _VERSION_PATTERN.fullmatch(text)
|
|
if not match:
|
|
return ""
|
|
return text[1:] if text.lower().startswith("v") else text
|
|
|
|
|
|
def version_key(value: Any) -> tuple[int, int, int] | None:
|
|
"""返回用于升级方向判断的三段版本;无效版本不参与比较。"""
|
|
|
|
normalized = normalize_version(value)
|
|
if not normalized:
|
|
return None
|
|
core = normalized.split("-", 1)[0].split("+", 1)[0]
|
|
major, minor, patch = core.split(".")
|
|
return int(major), int(minor), int(patch)
|
|
|
|
|
|
def is_newer_version(candidate: Any, current: Any) -> bool:
|
|
"""只有候选版本更高才升级,避免配置错误导致客户端降级。"""
|
|
|
|
candidate_key = version_key(candidate)
|
|
current_key = version_key(current)
|
|
return bool(candidate_key and current_key and candidate_key > current_key)
|
|
|
|
|
|
def _bounded_int(value: Any, default: int, minimum: int, maximum: int) -> int:
|
|
try:
|
|
parsed = int(value)
|
|
except (TypeError, ValueError):
|
|
parsed = default
|
|
return max(minimum, min(maximum, parsed))
|
|
|
|
|
|
def release_status(release: Any, *, local_version: str = APP_VERSION) -> dict[str, Any]:
|
|
"""把云端发布策略转换为桌面端可直接消费的状态。"""
|
|
source = release if isinstance(release, dict) else {}
|
|
local = normalize_version(local_version) or APP_VERSION
|
|
latest = normalize_version(source.get("latest_version")) or local
|
|
update_available = is_newer_version(latest, local)
|
|
download_url = str(source.get("download_url") or "").strip()
|
|
force_upgrade = bool(source.get("force_upgrade")) and update_available
|
|
package_sha256 = str(source.get("package_sha256") or "").strip().lower()
|
|
auto_install = bool(source.get("auto_install")) and update_available
|
|
return {
|
|
"local_version": local,
|
|
"latest_version": latest,
|
|
"download_url": download_url,
|
|
"release_notes": str(source.get("release_notes") or "").strip(),
|
|
"update_available": update_available,
|
|
"force_upgrade": force_upgrade,
|
|
"auto_install": auto_install,
|
|
"package_sha256": package_sha256,
|
|
"package_size": _bounded_int(
|
|
source.get("package_size"), 0, 0, 1024 * 1024 * 1024
|
|
),
|
|
"signature_required": bool(source.get("signature_required")),
|
|
"release_channel": str(source.get("release_channel") or "stable"),
|
|
"rollout_percent": _bounded_int(
|
|
source.get("rollout_percent"), 100, 1, 100
|
|
),
|
|
"status": str(source.get("status") or "published"),
|
|
"ready_for_auto_update": bool(
|
|
update_available and download_url and package_sha256
|
|
),
|
|
"updated_at": str(source.get("updated_at") or ""),
|
|
}
|