feat(server): modularize backend and admin console
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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")
|
||||
TARGET_ARCH = os.getenv("TARGET_ARCH", "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}
|
||||
Reference in New Issue
Block a user