Provision the isolated game-source writer path, bind source uploads to GameSourceArchive manifests, and require every immutable storage field for pending-to-committed CAS. Reuse the gate-validated runtime manifest snapshot, normalize remote storage errors, and record the staging object/CAS evidence.
134 lines
6.4 KiB
Python
134 lines
6.4 KiB
Python
"""引擎无关游戏源归档同步 CLI。
|
||
|
||
默认只构建本地 GameSourceArchive/1 清单。波次 1 上传必须显式传
|
||
``--external-single-writer``,并且只返回 ``verified_pending``,不能把对象
|
||
marker 当成数据库 committed 状态。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
|
||
from worker.game_source_archive_store import (
|
||
ArtifactError,
|
||
build_source_archive,
|
||
download_source_archive,
|
||
fetch_source_archive,
|
||
upload_source_archive,
|
||
)
|
||
|
||
|
||
def _config() -> dict:
|
||
"""读取独立 game_source_minio 配置,禁止复用制品 writer 或 MinIO root。"""
|
||
try:
|
||
from service import infra_config
|
||
|
||
section = "game_source_minio"
|
||
config = {
|
||
"endpoint": os.getenv("GAME_SOURCE_ENDPOINT", infra_config.get(section, "endpoint", "")),
|
||
"access_key": os.getenv("GAME_SOURCE_ACCESS_KEY", infra_config.get(section, "access_key", "")),
|
||
"secret_key": os.getenv("GAME_SOURCE_SECRET_KEY", infra_config.get(section, "secret_key", "")),
|
||
"secure": os.getenv("GAME_SOURCE_SECURE", str(infra_config.get(section, "secure", True))).lower()
|
||
in {"1", "true", "yes"},
|
||
"artifact_bucket": infra_config.get("game_artifact_minio", "artifact_bucket", "game-artifacts"),
|
||
"evidence_bucket": infra_config.get("game_artifact_minio", "evidence_bucket", "game-evidence"),
|
||
"source_bucket": os.getenv("GAME_SOURCE_BUCKET", infra_config.get(section, "source_bucket", "")),
|
||
}
|
||
if not config["endpoint"] or not config["access_key"] or not config["secret_key"]:
|
||
raise ArtifactError("专用 game_source_minio 配置未就位,拒绝复用制品 writer 或现有 MinIO root 凭据")
|
||
if not config["source_bucket"]:
|
||
raise ArtifactError("game-sources 尚未预建或加入专用身份策略,拒绝联网上传")
|
||
if config["source_bucket"] in {config["artifact_bucket"], config["evidence_bucket"]}:
|
||
raise ArtifactError("source_bucket 不能与 artifact/evidence 桶复用")
|
||
return config
|
||
except Exception as exc: # noqa: BLE001 - CLI 给出稳定错误
|
||
raise ArtifactError(f"读取 MinIO 配置失败:{exc}") from exc
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="构建、上传或下载引擎无关游戏源归档")
|
||
parser.add_argument("--root", type=Path, help="具体游戏源目录(只作为本次上传输入)")
|
||
parser.add_argument("--tenant-id")
|
||
parser.add_argument("--game-id")
|
||
parser.add_argument("--revision-id")
|
||
parser.add_argument("--engine")
|
||
parser.add_argument("--runtime-manifest", default="game-package.json",
|
||
help="与 GameArtifactManifest 同一输入根中的 GamePackage/1 相对路径")
|
||
parser.add_argument("--exclude-path", action="append", default=[], help="不进入源归档的目录内相对路径")
|
||
parser.add_argument("--manifest-out", type=Path)
|
||
parser.add_argument("--upload", action="store_true", help="显式访问 MinIO 并上传")
|
||
parser.add_argument(
|
||
"--external-single-writer", action="store_true",
|
||
help="确认调用方已持有 tenant/game/revision 外部单写锁",
|
||
)
|
||
parser.add_argument("--download-key", help="受信 source-manifest 对象 key;不接受任意 URL")
|
||
parser.add_argument("--expected-manifest-hash", help="数据库登记的 source manifest SHA-256")
|
||
parser.add_argument("--target", type=Path, help="下载物化目录")
|
||
args = parser.parse_args()
|
||
try:
|
||
if args.download_key:
|
||
if args.upload or args.external_single_writer or not all((
|
||
args.target, args.expected_manifest_hash, args.tenant_id,
|
||
args.game_id, args.revision_id,
|
||
)):
|
||
raise ArtifactError("下载模式必须提供 target、期望 hash 和 tenant/game/revision,且不能带上传参数")
|
||
config = _config()
|
||
manifest = fetch_source_archive(
|
||
config, key=args.download_key,
|
||
expected_manifest_hash=args.expected_manifest_hash,
|
||
expected_tenant_id=args.tenant_id,
|
||
expected_game_id=args.game_id,
|
||
expected_revision_id=args.revision_id,
|
||
)
|
||
target = download_source_archive(
|
||
manifest, config, args.target, expected_manifest_hash=args.expected_manifest_hash,
|
||
)
|
||
print(json.dumps({
|
||
"gameId": manifest["gameId"],
|
||
"revisionId": manifest["revisionId"],
|
||
"sourceHash": manifest["sourceHash"],
|
||
"manifestHash": manifest["manifestHash"],
|
||
"target": str(target),
|
||
}, ensure_ascii=False))
|
||
return 0
|
||
|
||
if not all((args.root, args.tenant_id, args.game_id, args.revision_id, args.engine)):
|
||
raise ArtifactError("构建/上传模式缺少 root、tenant/game/revision 或 engine")
|
||
manifest = build_source_archive(
|
||
args.root, args.tenant_id, args.game_id, args.revision_id,
|
||
engine=args.engine, exclude_paths=args.exclude_path,
|
||
runtime_manifest_path=args.runtime_manifest,
|
||
)
|
||
payload = json.dumps(manifest, ensure_ascii=False, indent=2) + "\n"
|
||
if args.manifest_out:
|
||
args.manifest_out.parent.mkdir(parents=True, exist_ok=True)
|
||
args.manifest_out.write_text(payload, encoding="utf-8")
|
||
print(json.dumps({
|
||
"tenantId": manifest["tenantId"],
|
||
"gameId": manifest["gameId"],
|
||
"revisionId": manifest["revisionId"],
|
||
"objects": manifest["totals"]["objects"],
|
||
"bytes": manifest["totals"]["bytes"],
|
||
"sourceHash": manifest["sourceHash"],
|
||
"manifestHash": manifest["manifestHash"],
|
||
"upload": args.upload,
|
||
}, ensure_ascii=False))
|
||
if args.upload:
|
||
if not args.external_single_writer:
|
||
raise ArtifactError("波次 1 上传必须显式确认 --external-single-writer")
|
||
result = upload_source_archive(
|
||
args.root, manifest, _config(), external_single_writer=True,
|
||
)
|
||
print(json.dumps(result, ensure_ascii=False))
|
||
return 0
|
||
except ArtifactError as exc:
|
||
print(f"GAME-SOURCE-ARCHIVE-ERROR:{exc}")
|
||
return 2
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|