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

127 lines
5.2 KiB
Python

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 require_permission
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(require_permission("version:view"))):
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(require_permission("version:view"))):
return {"list": version_repository.list_versions(app_id)}
@router.post("/admin/version/set-protocol")
def admin_set_version_protocol(body: VersionProtocolRequest, auth=Depends(require_permission("version:manage"))):
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(require_permission("version:manage"))):
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(require_permission("version:manage"))):
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}