#!/usr/bin/env python3 import argparse import json import os import shutil import sys import tempfile import urllib.error import urllib.parse import urllib.request import zipfile from pathlib import Path DRIVERS = [ "mariadb", "oceanbase", "doris", "starrocks", "sphinx", "sqlserver", "sqlite", "duckdb", "dameng", "kingbase", "highgo", "vastbase", "opengauss", "mongodb", "tdengine", "clickhouse", ] BUNDLE_NAME = "GoNavi-DriverAgents.zip" def required_assets(): assets = [] for driver in DRIVERS: assets.extend( [ ("Windows", f"{driver}-driver-agent-windows-amd64.exe"), ("MacOS", f"{driver}-driver-agent-darwin-amd64"), ("MacOS", f"{driver}-driver-agent-darwin-arm64"), ("Linux", f"{driver}-driver-agent-linux-amd64"), ] ) if driver == "duckdb": assets.append(("Windows", "duckdb.dll")) else: assets.append(("Windows", f"{driver}-driver-agent-windows-arm64.exe")) return assets def github_headers(): headers = { "Accept": "application/vnd.github+json", "User-Agent": "GoNavi-CI", } token = os.environ.get("DRIVER_RELEASE_TOKEN") or os.environ.get("GITHUB_TOKEN") if token: headers["Authorization"] = f"Bearer {token}" return headers def fetch_json(url): req = urllib.request.Request(url, headers=github_headers()) with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode("utf-8")) def download_asset(asset, destination): headers = github_headers() headers["Accept"] = "application/octet-stream" req = urllib.request.Request(asset["url"], headers=headers) with urllib.request.urlopen(req, timeout=120) as resp: with open(destination, "wb") as out: shutil.copyfileobj(resp, out) def load_release(repo, source): owner_repo = repo.strip() if not owner_repo: raise ValueError("repo is required") if source == "latest": api_url = f"https://api.github.com/repos/{owner_repo}/releases/latest" else: api_url = f"https://api.github.com/repos/{owner_repo}/releases/tags/{urllib.parse.quote(source, safe='')}" try: return fetch_json(api_url) except urllib.error.HTTPError as exc: if exc.code == 404: print(f"未找到上一版 driver release:{source}", file=sys.stderr) return None raise def asset_map(release): result = {} for asset in release.get("assets", []): name = str(asset.get("name", "")).strip() if name: result[name] = asset return result def copy_missing_from_bundle(bundle_path, target_root): copied = 0 required = { (Path(platform) / file_name).as_posix() for platform, file_name in required_assets() } with zipfile.ZipFile(bundle_path) as zf: members = { Path(info.filename).as_posix(): info for info in zf.infolist() if not info.is_dir() } for item in sorted(required): source = members.get(item) if not source: continue relative = Path(item) target = target_root / relative if target.exists(): continue target.parent.mkdir(parents=True, exist_ok=True) with zf.open(source) as src, open(target, "wb") as out: shutil.copyfileobj(src, out) copied += 1 return copied def copy_missing_standalone(release_assets, target_root): copied = 0 with tempfile.TemporaryDirectory(prefix="gonavi-driver-assets-") as tmp: tmp_root = Path(tmp) for platform, file_name in required_assets(): target = target_root / platform / file_name if target.exists(): continue asset = release_assets.get(file_name) if not asset: continue temp_path = tmp_root / file_name download_asset(asset, temp_path) target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(temp_path, target) copied += 1 return copied def verify_complete(target_root): missing = [] for platform, file_name in required_assets(): if not (target_root / platform / file_name).is_file(): missing.append(f"{platform}/{file_name}") return missing def main(): parser = argparse.ArgumentParser() parser.add_argument("--assets-dir", required=True) parser.add_argument("--repo", default="Syngnat/GoNavi-DriverAgents") parser.add_argument( "--source", action="append", required=True, help="latest or release tag such as dev-latest/v1.0.0; may be passed more than once", ) parser.add_argument("--require-complete", action="store_true") args = parser.parse_args() assets_dir = Path(args.assets_dir) driver_root = assets_dir / "drivers" driver_root.mkdir(parents=True, exist_ok=True) found_source = False total_copied = 0 for source in args.source: release = load_release(args.repo, source) if release is None: continue found_source = True releases_assets = asset_map(release) copied = 0 bundle_asset = releases_assets.get(BUNDLE_NAME) if bundle_asset: with tempfile.TemporaryDirectory(prefix="gonavi-driver-release-") as tmp: bundle_path = Path(tmp) / BUNDLE_NAME download_asset(bundle_asset, bundle_path) copied += copy_missing_from_bundle(bundle_path, driver_root) copied += copy_missing_standalone(releases_assets, driver_root) total_copied += copied print(f"已从 {source} 补齐 driver assets:{copied} 个文件") if not verify_complete(driver_root): break if not found_source: print("未找到可补齐的上一版 driver release。", file=sys.stderr) else: print(f"driver assets 合计补齐:{total_copied} 个文件") if args.require_complete: missing = verify_complete(driver_root) if missing: print("driver assets 不完整:", file=sys.stderr) for item in missing: print(f" - {item}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())