127 lines
5.2 KiB
Python
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}
|