6c64189c70
- scalar_one_or_none() 在 release_id+platform+architecture 多行时抛异常 - 改为查询全部后取第一个,按 signature desc 排序优先 updater 包
195 lines
5.9 KiB
Python
195 lines
5.9 KiB
Python
"""
|
||
应用更新 API
|
||
============
|
||
|
||
为 Tauri updater 插件提供更新检查接口。
|
||
"""
|
||
|
||
from datetime import UTC, datetime
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.db.session import get_db
|
||
from app.models.update import AppRelease, ReleasePackage
|
||
from app.schemas.update import (
|
||
ReleaseCreate,
|
||
ReleaseListItem,
|
||
ReleaseResponse,
|
||
TauriPlatformInfo,
|
||
TauriUpdateResponse,
|
||
)
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
@router.get("/check", response_model=TauriUpdateResponse)
|
||
async def check_update(
|
||
version: str = Query(..., description="当前应用版本"),
|
||
target: str = Query(..., description="平台:darwin / windows / linux"),
|
||
arch: str = Query(..., description="架构:x86_64 / aarch64"),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""
|
||
检查应用更新 — Tauri updater 插件入口
|
||
|
||
Tauri 启动时会向此接口发送请求,携带当前版本、平台、架构信息。
|
||
如果无需更新,返回 204;如果有更新,返回 Tauri 标准格式的 JSON。
|
||
"""
|
||
# 查询最新版本
|
||
result = await db.execute(
|
||
select(AppRelease).order_by(AppRelease.release_date.desc()).limit(1)
|
||
)
|
||
latest: AppRelease | None = result.scalar_one_or_none()
|
||
|
||
if not latest:
|
||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||
|
||
# 已是最新版本(或更高)
|
||
if latest.version == version:
|
||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||
|
||
# 查询对应平台的包(优先返回 updater 用的包:有 signature 的 .app.tar.gz / .exe)
|
||
result = await db.execute(
|
||
select(ReleasePackage).where(
|
||
ReleasePackage.release_id == latest.id,
|
||
ReleasePackage.platform == target,
|
||
ReleasePackage.architecture == arch,
|
||
).order_by(
|
||
# 有 signature 的排前面(updater 包),空 signature 的排后面(dmg 安装包)
|
||
ReleasePackage.signature.desc()
|
||
)
|
||
)
|
||
pkgs = result.scalars().all()
|
||
# 取第一个:优先有 signature 的 updater 包;如果没有则取任意一个
|
||
pkg: ReleasePackage | None = pkgs[0] if pkgs else None
|
||
|
||
if not pkg:
|
||
# 该平台无包,返回 204(避免报错阻断用户)
|
||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||
|
||
# 构建 Tauri 格式的响应
|
||
platform_key = f"{target}-{arch}"
|
||
|
||
return TauriUpdateResponse(
|
||
version=latest.version,
|
||
notes=latest.notes,
|
||
pub_date=latest.release_date.isoformat() if latest.release_date else None,
|
||
mandatory=latest.mandatory,
|
||
platforms={
|
||
platform_key: TauriPlatformInfo(
|
||
url=pkg.file_url,
|
||
signature=pkg.signature,
|
||
)
|
||
},
|
||
)
|
||
|
||
|
||
@router.post("/releases", response_model=ReleaseResponse, status_code=status.HTTP_201_CREATED)
|
||
async def create_release(
|
||
release: ReleaseCreate,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""
|
||
创建新版本发布(管理员接口)
|
||
|
||
用于手动发版时,将构建产物信息写入数据库。
|
||
"""
|
||
# 检查版本是否已存在
|
||
result = await db.execute(select(AppRelease).where(AppRelease.version == release.version))
|
||
if result.scalar_one_or_none():
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail=f"版本 {release.version} 已存在",
|
||
)
|
||
|
||
# 创建发布记录
|
||
new_release = AppRelease(
|
||
version=release.version,
|
||
release_date=datetime.now(UTC),
|
||
notes=release.notes,
|
||
mandatory=release.mandatory,
|
||
)
|
||
db.add(new_release)
|
||
await db.flush()
|
||
|
||
# 创建包记录
|
||
for pkg in release.packages:
|
||
db.add(
|
||
ReleasePackage(
|
||
release_id=new_release.id,
|
||
platform=pkg.platform,
|
||
architecture=pkg.architecture,
|
||
filename=pkg.filename,
|
||
file_url=pkg.file_url,
|
||
file_size=pkg.file_size,
|
||
signature=pkg.signature,
|
||
)
|
||
)
|
||
|
||
await db.commit()
|
||
await db.refresh(new_release)
|
||
|
||
return ReleaseResponse(
|
||
id=new_release.id,
|
||
version=new_release.version,
|
||
release_date=new_release.release_date,
|
||
notes=new_release.notes,
|
||
mandatory=new_release.mandatory,
|
||
created_at=new_release.created_at,
|
||
packages=[
|
||
{
|
||
"platform": p.platform,
|
||
"architecture": p.architecture,
|
||
"filename": p.filename,
|
||
"file_url": p.file_url,
|
||
"file_size": p.file_size,
|
||
"signature": p.signature,
|
||
}
|
||
for p in new_release.packages
|
||
],
|
||
)
|
||
|
||
|
||
@router.get("/releases", response_model=list[ReleaseListItem])
|
||
async def list_releases(
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取所有版本发布列表(管理员接口)"""
|
||
result = await db.execute(select(AppRelease).order_by(AppRelease.release_date.desc()))
|
||
releases = result.scalars().all()
|
||
|
||
return [
|
||
ReleaseListItem(
|
||
id=r.id,
|
||
version=r.version,
|
||
release_date=r.release_date,
|
||
notes=r.notes,
|
||
mandatory=r.mandatory,
|
||
package_count=len(r.packages),
|
||
)
|
||
for r in releases
|
||
]
|
||
|
||
|
||
@router.delete("/releases/{version}")
|
||
async def delete_release(
|
||
version: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""删除版本发布(管理员接口)"""
|
||
result = await db.execute(select(AppRelease).where(AppRelease.version == version))
|
||
release = result.scalar_one_or_none()
|
||
|
||
if not release:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"版本 {version} 不存在",
|
||
)
|
||
|
||
await db.delete(release)
|
||
await db.commit()
|
||
|
||
return {"status": "success", "message": f"版本 {version} 已删除"}
|