Files
update-server/app/api/routes/admin_version.py
T

127 lines
5.1 KiB
Python
Raw Normal View History

import hashlib
import json
import os
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
import minio_tool
from app.core.security import admin_auth
from app.repositories import version_repository
from app.schemas.version import VersionIdRequest, VersionProtocolRequest
from app.services.common_service import is_executable_path
from app.services.signing_service import canonical_manifest_bytes, sign_manifest, sign_policy
TARGET_PLATFORM = os.getenv("TARGET_PLATFORM", "windows").strip().lower() or "windows"
TARGET_ARCH = os.getenv("TARGET_ARCH", "x64").strip() or "x64"
SIGNING_KEY_ID = os.getenv("SIGNING_KEY_ID", "manifest-key-v1")
router = APIRouter(tags=["admin-version"])
@router.post("/admin/version/offline-package")
def admin_offline_package(body: VersionIdRequest, auth=Depends(admin_auth)):
version_id = int(body.version_id or 0)
ver, rows = version_repository.get_version_with_files(version_id)
if not ver or not rows:
raise HTTPException(status_code=404, detail="版本或版本文件不存在")
files = [{"path": r["path"], "sha256": r["sha256"], "size": r["size"], "executable": is_executable_path(r["path"])} for r in rows]
manifest = {
"app_id": ver["app_id"],
"version": ver["version"],
"channel": ver["channel"],
"platform": TARGET_PLATFORM,
"arch": TARGET_ARCH,
"manifest_seq": version_id,
"created_at": ver["create_time"],
"files": files,
}
manifest_text = canonical_manifest_bytes(manifest).decode()
manifest_signature = sign_manifest(manifest)
offset = 0
entries = []
for r in rows:
entries.append({"path": r["path"], "offset": offset, "size": r["size"], "sha256": r["sha256"]})
offset += int(r["size"] or 0)
package = {
"format": "MUPD0001",
"app_id": ver["app_id"],
"channel": ver["channel"],
"version": ver["version"],
"version_id": version_id,
"created_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
"manifest_sha256": hashlib.sha256(manifest_text.encode()).hexdigest(),
"payload_size": offset,
"files": entries,
"signature_alg": "RSA-2048-SHA256",
"key_id": SIGNING_KEY_ID,
}
package_text, package_signature = sign_policy(package)
wrapper = json.dumps(
{
"package_text": package_text,
"package_signature": package_signature,
"manifest_text": manifest_text,
"manifest_signature": manifest_signature,
},
ensure_ascii=False,
separators=(",", ":"),
).encode()
prefix = f"{ver['app_id']}/{ver['channel']}/{ver['version']}/files/"
def stream():
yield b"MUPD0001"
yield len(wrapper).to_bytes(8, "little")
yield wrapper
for r in rows:
yield from minio_tool.iter_file(prefix + r["path"])
filename = f"{ver['app_id']}_{ver['channel']}_{ver['version']}.upd"
return StreamingResponse(
stream(),
media_type="application/octet-stream",
headers={"Content-Disposition": f'attachment; filename="{filename}"', "Content-Length": str(16 + len(wrapper) + offset)},
)
@router.get("/admin/version/list")
def admin_get_version_list(app_id: str, auth=Depends(admin_auth)):
return {"list": version_repository.list_versions(app_id)}
@router.post("/admin/version/set-protocol")
def admin_set_version_protocol(body: VersionProtocolRequest, auth=Depends(admin_auth)):
version_id = int(body.version_id or 0)
client_protocol = int(body.client_protocol or 0)
if version_id < 1 or client_protocol < 1:
raise HTTPException(status_code=400, detail="版本 ID 和客户端协议必须是正整数")
updated = version_repository.set_version_protocol(version_id, client_protocol)
if updated != 1:
raise HTTPException(status_code=404, detail="版本不存在")
return {"msg": "客户端协议已更新", "version_id": version_id, "client_protocol": client_protocol}
@router.post("/admin/version/set-latest")
def admin_set_latest(body: VersionIdRequest, auth=Depends(admin_auth)):
vid = body.version_id
result = version_repository.set_latest_version(vid)
if result == "not_found":
raise HTTPException(status_code=404, detail="版本不存在")
return {"msg": "已切换为渠道最新版本"}
@router.post("/admin/version/delete")
def admin_delete_version(body: VersionIdRequest, auth=Depends(admin_auth)):
vid = body.version_id
v_info = version_repository.get_version_for_delete(vid)
if not v_info:
raise HTTPException(status_code=404, detail="版本不存在")
aid, ch, ver = v_info["app_id"], v_info["channel"], v_info["version"]
was_latest = bool(v_info["latest"])
prefix = f"{aid}/{ch}/{ver}/files/"
minio_tool.remove_prefix(prefix)
promoted_version = version_repository.delete_version_and_promote(vid, aid, ch, was_latest)
return {"msg": "版本、云端文件、数据库记录全部删除完成", "promoted_latest": promoted_version}