♻️ refactor(driver-delivery): 重构可选驱动分发为总包+索引模式

- 工作流统一收敛驱动产物并打包单一压缩包
- 新增驱动总包索引读取与缓存合并逻辑
- 保留原单文件直链兼容并增加总包提取回退
This commit is contained in:
Syngnat
2026-02-14 11:45:51 +08:00
parent 5329f212f7
commit 663717d738
2 changed files with 281 additions and 5 deletions

View File

@@ -104,6 +104,8 @@ jobs:
GOOS="${TARGET_PLATFORM%%/*}"
GOARCH="${TARGET_PLATFORM##*/}"
DRIVERS=(mariadb diros sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase mongodb tdengine)
OUTDIR="drivers/${{ matrix.os_name }}"
mkdir -p "$OUTDIR"
for DRIVER in "${DRIVERS[@]}"; do
TAG="gonavi_${DRIVER}_driver"
@@ -111,20 +113,21 @@ jobs:
if [ "$GOOS" = "windows" ]; then
OUTPUT="${OUTPUT}.exe"
fi
echo "🔧 构建 ${OUTPUT} (tag=${TAG})"
OUTPUT_PATH="${OUTDIR}/${OUTPUT}"
echo "🔧 构建 ${OUTPUT_PATH} (tag=${TAG})"
if [ "$DRIVER" = "duckdb" ]; then
set +e
CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" go build \
-tags "${TAG}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT}" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
DUCKDB_RC=$?
set -e
if [ "${DUCKDB_RC}" -ne 0 ]; then
echo "⚠️ DuckDB 代理构建失败(平台 ${GOOS}/${GOARCH}),跳过该资产,不阻断发布"
rm -f "${OUTPUT}"
rm -f "${OUTPUT_PATH}"
continue
fi
else
@@ -132,7 +135,7 @@ jobs:
-tags "${TAG}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT}" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
fi
done
@@ -295,7 +298,7 @@ jobs:
GoNavi-*.zip
GoNavi-*.tar.gz
GoNavi-*.AppImage
*-driver-agent-*
drivers/**
retention-days: 1
# Phase 2: Collect all artifacts and Publish Release (Single Job)
@@ -314,6 +317,59 @@ jobs:
- name: List Assets
run: ls -R release-assets
- name: Package Driver Agents Bundle
shell: bash
run: |
set -euo pipefail
cd release-assets
if [ ! -d drivers ]; then
echo "⚠️ 未找到 drivers 目录,跳过驱动总包打包"
exit 0
fi
if [ -z "$(find drivers -type f 2>/dev/null | head -n 1)" ]; then
echo "⚠️ drivers 目录为空,跳过驱动总包打包"
rm -rf drivers
exit 0
fi
echo "📦 打包驱动总包GoNavi-DriverAgents.zip"
python3 - <<'PY'
import json
import os
import zipfile
from pathlib import Path
out_name = "GoNavi-DriverAgents.zip"
index_name = "GoNavi-DriverAgents-Index.json"
base = Path("drivers")
out_path = Path(out_name)
index_path = Path(index_name)
if out_path.exists():
out_path.unlink()
if index_path.exists():
index_path.unlink()
size_index = {}
with zipfile.ZipFile(out_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for p in base.rglob("*"):
if not p.is_file():
continue
arcname = p.relative_to(base).as_posix()
zf.write(p, arcname)
size_index[p.name] = p.stat().st_size
index_path.write_text(
json.dumps({"assets": size_index}, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"created {out_name} size={out_path.stat().st_size} bytes")
print(f"created {index_name} entries={len(size_index)}")
PY
# Release 只发布一个驱动总包,避免大量平铺资产污染 Release 页面
rm -rf drivers
- name: Generate SHA256SUMS
shell: bash
run: |

View File

@@ -1,6 +1,7 @@
package app
import (
"archive/zip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@@ -111,13 +112,20 @@ type driverReleaseAssetSizeCacheEntry struct {
Err string
}
type driverBundleAssetIndex struct {
Assets map[string]int64 `json:"assets"`
}
const (
// 默认使用内置 manifest避免依赖网络与外部仓库 404。
defaultDriverManifestURLValue = "builtin://manifest"
optionalDriverBundleAssetName = "GoNavi-DriverAgents.zip"
optionalDriverBundleIndexAssetName = "GoNavi-DriverAgents-Index.json"
driverManifestCacheTTL = 5 * time.Minute
driverReleaseAssetSizeCacheTTL = 30 * time.Minute
driverReleaseAssetSizeErrorCacheTTL = 30 * time.Second
driverReleaseAssetSizeProbeTimeout = 4 * time.Second
driverBundleIndexMaxSize = 1 << 20
driverManifestMaxSize = 2 << 20
driverChecksumPolicyStrict = "strict"
driverChecksumPolicyWarn = "warn"
@@ -1123,6 +1131,19 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
downloadErrs = append(downloadErrs, fmt.Sprintf("%s: %s", candidateURL, strings.TrimSpace(dlErr.Error())))
}
}
bundleURLs := resolveOptionalDriverBundleDownloadURLs()
if len(bundleURLs) > 0 {
for _, bundleURL := range bundleURLs {
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 20, 100, fmt.Sprintf("从驱动总包提取 %s 代理", displayName))
}
source, hash, bundleErr := downloadOptionalDriverAgentFromBundle(a, definition, bundleURL, executablePath)
if bundleErr == nil {
return source, hash, nil
}
downloadErrs = append(downloadErrs, fmt.Sprintf("%s: %s", bundleURL, strings.TrimSpace(bundleErr.Error())))
}
}
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 92, 100, "未命中预编译包,尝试开发态本地构建")
}
@@ -1176,6 +1197,112 @@ func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlT
return hash, nil
}
func downloadOptionalDriverAgentFromBundle(a *App, definition driverDefinition, bundleURL, executablePath string) (string, string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
trimmedURL := strings.TrimSpace(bundleURL)
if trimmedURL == "" {
return "", "", fmt.Errorf("驱动总包下载地址为空")
}
bundleTempPath := executablePath + ".bundle.zip.tmp"
_ = os.Remove(bundleTempPath)
_, err := downloadFileWithHash(trimmedURL, bundleTempPath, func(downloaded, total int64) {
if a == nil {
return
}
scaledDownloaded, scaledTotal := scaleProgress(downloaded, total, 20, 78)
a.emitDriverDownloadProgress(driverType, "downloading", scaledDownloaded, scaledTotal, fmt.Sprintf("下载 %s 驱动总包", displayName))
})
if err != nil {
_ = os.Remove(bundleTempPath)
return "", "", fmt.Errorf("下载驱动总包失败:%w", err)
}
defer func() { _ = os.Remove(bundleTempPath) }()
reader, err := zip.OpenReader(bundleTempPath)
if err != nil {
return "", "", fmt.Errorf("打开驱动总包失败:%w", err)
}
defer reader.Close()
entryPath := optionalDriverBundleEntryPath(driverType)
expectedBaseName := optionalDriverReleaseAssetName(driverType)
findEntry := func() *zip.File {
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if name == entryPath {
return file
}
}
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if strings.EqualFold(name, entryPath) {
return file
}
}
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if strings.EqualFold(filepath.Base(name), expectedBaseName) {
return file
}
}
return nil
}
entry := findEntry()
if entry == nil {
return "", "", fmt.Errorf("驱动总包内未找到 %s期望路径 %s", displayName, entryPath)
}
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 84, 100, fmt.Sprintf("解压 %s 驱动代理", displayName))
}
src, err := entry.Open()
if err != nil {
return "", "", fmt.Errorf("读取驱动总包条目失败:%w", err)
}
defer src.Close()
tempPath := executablePath + ".tmp"
_ = os.Remove(tempPath)
dst, err := os.Create(tempPath)
if err != nil {
return "", "", fmt.Errorf("创建驱动代理临时文件失败:%w", err)
}
if _, err := io.Copy(dst, src); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return "", "", fmt.Errorf("写入驱动代理失败:%w", err)
}
if err := dst.Sync(); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return "", "", fmt.Errorf("落盘驱动代理失败:%w", err)
}
if err := dst.Close(); err != nil {
_ = os.Remove(tempPath)
return "", "", fmt.Errorf("关闭驱动代理文件失败:%w", err)
}
if chmodErr := os.Chmod(tempPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
_ = os.Remove(tempPath)
return "", "", fmt.Errorf("设置驱动代理权限失败:%w", chmodErr)
}
if err := os.Rename(tempPath, executablePath); err != nil {
_ = os.Remove(tempPath)
return "", "", fmt.Errorf("替换驱动代理失败:%w", err)
}
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", "", fmt.Errorf("设置驱动代理权限失败:%w", chmodErr)
}
hash, err := hashFileSHA256(executablePath)
if err != nil {
return "", "", fmt.Errorf("计算驱动代理摘要失败:%w", err)
}
source := fmt.Sprintf("%s#%s", trimmedURL, filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(entry.Name), "./")))
return source, hash, nil
}
func buildOptionalDriverAgentFromSource(definition driverDefinition, executablePath string) (string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
@@ -1282,6 +1409,46 @@ func optionalDriverReleaseAssetName(driverType string) string {
return name
}
func optionalDriverBundlePlatformDir(goos string) string {
switch strings.ToLower(strings.TrimSpace(goos)) {
case "windows":
return "Windows"
case "darwin":
return "MacOS"
case "linux":
return "Linux"
default:
return "Unknown"
}
}
func optionalDriverBundleEntryPath(driverType string) string {
return filepath.ToSlash(filepath.Join(optionalDriverBundlePlatformDir(stdRuntime.GOOS), optionalDriverReleaseAssetName(driverType)))
}
func resolveOptionalDriverBundleDownloadURLs() []string {
candidates := make([]string, 0, 2)
seen := make(map[string]struct{}, 2)
appendURL := func(value string) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return
}
if _, ok := seen[trimmed]; ok {
return
}
seen[trimmed] = struct{}{}
candidates = append(candidates, trimmed)
}
currentVersion := normalizeVersion(getCurrentVersion())
if currentVersion != "" && currentVersion != "0.0.0" {
appendURL(fmt.Sprintf("https://github.com/Syngnat/GoNavi/releases/download/v%s/%s", currentVersion, optionalDriverBundleAssetName))
}
appendURL(fmt.Sprintf("https://github.com/Syngnat/GoNavi/releases/latest/download/%s", optionalDriverBundleAssetName))
return candidates
}
func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL string) []string {
driverType := normalizeDriverType(definition.Type)
candidates := make([]string, 0, 3)
@@ -1541,6 +1708,15 @@ func loadReleaseAssetSizesCached(cacheKey string, fetch func() (*githubRelease,
entry.Err = err.Error()
} else {
entry.SizeByKey = buildReleaseAssetSizeMap(release)
if indexSizes, indexErr := fetchDriverBundleAssetSizeIndex(release); indexErr == nil {
for name, size := range indexSizes {
trimmedName := strings.TrimSpace(name)
if trimmedName == "" || size <= 0 {
continue
}
entry.SizeByKey[trimmedName] = size
}
}
}
driverReleaseSizeMu.Lock()
@@ -1568,6 +1744,50 @@ func buildReleaseAssetSizeMap(release *githubRelease) map[string]int64 {
return sizes
}
func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64, error) {
if release == nil {
return nil, fmt.Errorf("release 为空")
}
indexURL := ""
for _, asset := range release.Assets {
if strings.EqualFold(strings.TrimSpace(asset.Name), optionalDriverBundleIndexAssetName) {
indexURL = strings.TrimSpace(asset.BrowserDownloadURL)
break
}
}
if indexURL == "" {
return nil, fmt.Errorf("未找到驱动总包索引资产")
}
client := &http.Client{Timeout: driverReleaseAssetSizeProbeTimeout}
req, err := http.NewRequest(http.MethodGet, indexURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "GoNavi-DriverManager")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("拉取驱动总包索引失败HTTP %d", resp.StatusCode)
}
limited := io.LimitReader(resp.Body, driverBundleIndexMaxSize)
decoder := json.NewDecoder(limited)
var index driverBundleAssetIndex
if err := decoder.Decode(&index); err != nil {
return nil, fmt.Errorf("解析驱动总包索引失败:%w", err)
}
if len(index.Assets) == 0 {
return nil, fmt.Errorf("驱动总包索引为空")
}
return index.Assets, nil
}
func fetchLatestReleaseForDriverAssets() (*githubRelease, error) {
return fetchDriverReleaseByURL(updateAPIURL)
}