更新
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""桌面客户端在线升级:发布配置(存 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
|
||||
Reference in New Issue
Block a user