Files
douyin/backend/desktop_release.py
T
2026-07-17 09:24:47 +08:00

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