117 lines
3.8 KiB
Python
117 lines
3.8 KiB
Python
"""桌面客户端在线升级:发布配置(存 AppConfig 行)+ 安装包文件管理。
|
|
|
|
发布配置以 JSON 形式存放在 app_config 表的 id=DESKTOP_CONFIG_ROW_ID 行,
|
|
安装包文件落在 backend/uploads/desktop/ 下。仅管理员可读写。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from dataclasses import asdict, dataclass, fields
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from models.models import AppConfig
|
|
|
|
# app_config 表里给桌面发布单独用一行(系统设置用的是 id=1)
|
|
DESKTOP_CONFIG_ROW_ID = 2
|
|
|
|
INSTALLER_DIR = os.path.join(os.path.dirname(__file__), "uploads", "desktop")
|
|
INSTALLER_FILENAME = "DouyinHostedDesktop-Setup.exe"
|
|
|
|
|
|
@dataclass
|
|
class DesktopReleaseData:
|
|
version: str = "" # 已发布的最新版本号,如 1.0.1
|
|
force: bool = False # true=强制升级(不可跳过)
|
|
notes: str = "" # 更新说明
|
|
installer_name: str = "" # 上传时的原始文件名
|
|
installer_size: int = 0 # 安装包字节数
|
|
installer_url: str = "" # 外部安装包直链(填写后优先于本地上传文件)
|
|
updated_at: str = "" # 最近一次发布/上传时间
|
|
|
|
|
|
def package_ready(data: "DesktopReleaseData") -> bool:
|
|
"""是否具备可供客户端下载的安装包:外部直链 或 本地已上传文件。"""
|
|
return bool((data.installer_url or "").strip()) or installer_exists()
|
|
|
|
|
|
def installer_path() -> str:
|
|
return os.path.join(INSTALLER_DIR, INSTALLER_FILENAME)
|
|
|
|
|
|
def installer_exists() -> bool:
|
|
p = installer_path()
|
|
return os.path.isfile(p) and os.path.getsize(p) > 0
|
|
|
|
|
|
def _coerce_bool(value: Any, default: bool) -> bool:
|
|
if isinstance(value, bool):
|
|
return value
|
|
if isinstance(value, str):
|
|
return value.lower() in ("1", "true", "yes", "on")
|
|
if value is None:
|
|
return default
|
|
return bool(value)
|
|
|
|
|
|
def _parse(raw: dict[str, Any]) -> DesktopReleaseData:
|
|
base = DesktopReleaseData()
|
|
allowed = {f.name for f in fields(DesktopReleaseData)}
|
|
merged = {k: raw[k] for k in allowed if k in raw}
|
|
if "force" in merged:
|
|
merged["force"] = _coerce_bool(merged["force"], base.force)
|
|
if "installer_size" in merged:
|
|
try:
|
|
merged["installer_size"] = int(merged["installer_size"])
|
|
except (TypeError, ValueError):
|
|
merged["installer_size"] = base.installer_size
|
|
return DesktopReleaseData(**{**asdict(base), **merged})
|
|
|
|
|
|
async def load_release(db: AsyncSession) -> DesktopReleaseData:
|
|
row = (
|
|
await db.execute(select(AppConfig).where(AppConfig.id == DESKTOP_CONFIG_ROW_ID))
|
|
).scalar_one_or_none()
|
|
if not row or not row.data:
|
|
return DesktopReleaseData()
|
|
try:
|
|
payload = json.loads(row.data)
|
|
except json.JSONDecodeError:
|
|
payload = {}
|
|
return _parse(payload if isinstance(payload, dict) else {})
|
|
|
|
|
|
async def save_release(db: AsyncSession, updates: dict[str, Any]) -> DesktopReleaseData:
|
|
current = await load_release(db)
|
|
merged = asdict(current)
|
|
for key, value in updates.items():
|
|
if key not in merged or value is None:
|
|
continue
|
|
merged[key] = value
|
|
data = _parse(merged)
|
|
data.updated_at = datetime.utcnow().isoformat()
|
|
|
|
row = (
|
|
await db.execute(select(AppConfig).where(AppConfig.id == DESKTOP_CONFIG_ROW_ID))
|
|
).scalar_one_or_none()
|
|
if not row:
|
|
row = AppConfig(id=DESKTOP_CONFIG_ROW_ID, data="{}", updated_at=datetime.utcnow())
|
|
db.add(row)
|
|
row.data = json.dumps(asdict(data), ensure_ascii=False)
|
|
row.updated_at = datetime.utcnow()
|
|
await db.commit()
|
|
return data
|
|
|
|
|
|
def remove_installer() -> None:
|
|
p = installer_path()
|
|
try:
|
|
if os.path.exists(p):
|
|
os.remove(p)
|
|
except OSError:
|
|
pass
|