mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-25 02:00:09 +08:00
🐛 fix(driver-manager): 修复驱动安装交互与 DuckDB Windows 发布链路
- 修复单驱动安装期间右侧目录操作被错误禁用的问题 - 调整 DuckDB Windows 优先下载专属 zip 并兼容带 query 的签名链接 - 补齐本地构建与 CI 发布的 duckdb-driver.zip 产物及回归测试
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
stdRuntime "runtime"
|
||||
@@ -298,6 +299,7 @@ const (
|
||||
driverReleaseLatestAPIURL = "https://api.github.com/repos/" + driverReleaseRepo + "/releases/latest"
|
||||
driverReleaseDevTag = "dev-latest"
|
||||
optionalDriverBundleAssetName = "GoNavi-DriverAgents.zip"
|
||||
duckDBWindowsDriverZipAssetName = "duckdb-driver.zip"
|
||||
optionalDriverBundleIndexAssetName = "GoNavi-DriverAgents-Index.json"
|
||||
optionalDriverBundleDownloadTimeout = 45 * time.Minute
|
||||
optionalDriverBundleCacheMaxAge = 7 * 24 * time.Hour
|
||||
@@ -371,6 +373,8 @@ var (
|
||||
errLocalDriverDirScanLimit = errors.New("local_driver_directory_scan_limit_exceeded")
|
||||
)
|
||||
|
||||
var validateOptionalDriverAgentExecutableFunc = db.ValidateOptionalDriverAgentExecutable
|
||||
|
||||
type driverVersionWarmupState struct {
|
||||
Running bool
|
||||
LastStarted time.Time
|
||||
@@ -1914,6 +1918,27 @@ func resolvePublishedDriverDownloadURLForTag(definition driverDefinition, select
|
||||
}
|
||||
|
||||
func resolvePublishedDriverReleaseAssetName(driverType string, version string, tag string) (string, bool) {
|
||||
if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
|
||||
cacheKey := "tag:" + strings.TrimSpace(tag)
|
||||
if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache(cacheKey); ok {
|
||||
if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
|
||||
return duckDBWindowsDriverZipAssetName, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached(cacheKey, func() (*githubRelease, error) {
|
||||
return fetchReleaseByTag(tag)
|
||||
})
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
|
||||
return duckDBWindowsDriverZipAssetName, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, version)
|
||||
if len(assetNames) == 0 {
|
||||
return "", false
|
||||
@@ -3006,7 +3031,7 @@ func installOptionalDriverAgentFromLocalPath(definition driverDefinition, filePa
|
||||
return installedDriverPackage{}, fmt.Errorf("导入本地驱动代理运行时依赖失败:%w", supportErr)
|
||||
}
|
||||
}
|
||||
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
|
||||
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
|
||||
return installedDriverPackage{}, validateErr
|
||||
}
|
||||
|
||||
@@ -3355,7 +3380,7 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
|
||||
|
||||
info, err := os.Stat(executablePath)
|
||||
if err == nil && !info.IsDir() {
|
||||
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
|
||||
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
|
||||
_ = os.Remove(executablePath)
|
||||
} else {
|
||||
// 用户点击“安装/重装”时应强制刷新驱动代理,避免沿用旧二进制导致修复不生效。
|
||||
@@ -3389,7 +3414,7 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
|
||||
if copyErr := copyAgentBinary(sourcePath, executablePath); copyErr != nil {
|
||||
return "", "", fmt.Errorf("复制预置 %s 驱动代理失败:%w", displayName, copyErr)
|
||||
}
|
||||
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
|
||||
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
|
||||
_ = os.Remove(executablePath)
|
||||
return "", "", validateErr
|
||||
}
|
||||
@@ -3540,6 +3565,17 @@ func shouldUseOptionalDriverBundleFallback(driverType string, restrictToExplicit
|
||||
return directURLCount == 0
|
||||
}
|
||||
|
||||
func isOptionalDriverDownloadZipURL(urlText string) bool {
|
||||
trimmedURL := strings.TrimSpace(urlText)
|
||||
if trimmedURL == "" {
|
||||
return false
|
||||
}
|
||||
if parsed, err := url.Parse(trimmedURL); err == nil && strings.TrimSpace(parsed.Path) != "" {
|
||||
return strings.EqualFold(path.Ext(parsed.Path), ".zip")
|
||||
}
|
||||
return strings.EqualFold(filepath.Ext(trimmedURL), ".zip")
|
||||
}
|
||||
|
||||
func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlText string, executablePath string) (string, error) {
|
||||
driverType := normalizeDriverType(definition.Type)
|
||||
displayName := resolveDriverDisplayName(definition)
|
||||
@@ -3547,8 +3583,46 @@ func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlT
|
||||
if trimmedURL == "" {
|
||||
return "", fmt.Errorf("下载地址为空")
|
||||
}
|
||||
if isOptionalDriverDownloadZipURL(trimmedURL) {
|
||||
tempPath := executablePath + ".download.zip"
|
||||
_ = os.Remove(tempPath)
|
||||
|
||||
if _, err := downloadFileWithHash(trimmedURL, tempPath, func(downloaded, total int64) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
scaledDownloaded, scaledTotal := scaleProgress(downloaded, total, 20, 90)
|
||||
a.emitDriverDownloadProgress(driverType, "downloading", scaledDownloaded, scaledTotal, fmt.Sprintf("下载预编译 %s 驱动包", displayName))
|
||||
}); err != nil {
|
||||
_ = os.Remove(tempPath)
|
||||
return "", fmt.Errorf("下载失败:%w", err)
|
||||
}
|
||||
|
||||
if _, err := installOptionalDriverAgentFromLocalZip(tempPath, definition, executablePath, ""); err != nil {
|
||||
_ = os.Remove(tempPath)
|
||||
_ = os.Remove(executablePath)
|
||||
for _, supportName := range optionalDriverSupportFileNames(driverType) {
|
||||
_ = os.Remove(filepath.Join(filepath.Dir(executablePath), supportName))
|
||||
}
|
||||
return "", fmt.Errorf("安装预编译驱动包失败:%w", err)
|
||||
}
|
||||
_ = os.Remove(tempPath)
|
||||
|
||||
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
|
||||
_ = os.Remove(executablePath)
|
||||
for _, supportName := range optionalDriverSupportFileNames(driverType) {
|
||||
_ = os.Remove(filepath.Join(filepath.Dir(executablePath), supportName))
|
||||
}
|
||||
return "", validateErr
|
||||
}
|
||||
hash, hashErr := hashFileSHA256(executablePath)
|
||||
if hashErr != nil {
|
||||
return "", fmt.Errorf("计算驱动代理摘要失败:%w", hashErr)
|
||||
}
|
||||
return hash, nil
|
||||
}
|
||||
if len(optionalDriverSupportFileNames(driverType)) > 0 {
|
||||
return "", fmt.Errorf("%s 当前平台需要随包提供运行时依赖(%s),不能安装单文件代理;请使用驱动总包或本地源码构建", displayName, strings.Join(optionalDriverSupportFileNames(driverType), ", "))
|
||||
return "", fmt.Errorf("%s 当前平台需要随包提供运行时依赖(%s),不能安装单文件代理;请使用驱动总包、驱动专属 zip 或本地源码构建", displayName, strings.Join(optionalDriverSupportFileNames(driverType), ", "))
|
||||
}
|
||||
tempPath := executablePath + ".tmp"
|
||||
_ = os.Remove(tempPath)
|
||||
@@ -3576,7 +3650,7 @@ func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlT
|
||||
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
|
||||
return "", fmt.Errorf("设置代理权限失败:%w", chmodErr)
|
||||
}
|
||||
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
|
||||
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
|
||||
_ = os.Remove(executablePath)
|
||||
return "", validateErr
|
||||
}
|
||||
@@ -3693,7 +3767,7 @@ func downloadOptionalDriverAgentFromBundle(a *App, definition driverDefinition,
|
||||
_ = os.Remove(executablePath)
|
||||
return "", "", supportErr
|
||||
}
|
||||
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
|
||||
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
|
||||
_ = os.Remove(executablePath)
|
||||
return "", "", validateErr
|
||||
}
|
||||
@@ -3940,6 +4014,10 @@ func shouldUseDuckDBWindowsDynamicLibrary(driverType string) bool {
|
||||
return normalizeDriverType(driverType) == "duckdb" && stdRuntime.GOOS == "windows" && stdRuntime.GOARCH == "amd64"
|
||||
}
|
||||
|
||||
func shouldPreferPublishedOptionalDriverDownloads(driverType string) bool {
|
||||
return shouldUseDuckDBWindowsDynamicLibrary(driverType)
|
||||
}
|
||||
|
||||
func shouldSkipDirectOptionalDriverDownloads(driverType string) bool {
|
||||
return shouldUseDuckDBWindowsDynamicLibrary(driverType)
|
||||
}
|
||||
@@ -4698,6 +4776,7 @@ func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloade
|
||||
func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL string, selectedVersion string) []string {
|
||||
candidates := make([]string, 0, 3)
|
||||
seen := make(map[string]struct{}, 3)
|
||||
driverType := normalizeDriverType(definition.Type)
|
||||
appendURL := func(value string) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
@@ -4710,8 +4789,20 @@ func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL
|
||||
candidates = append(candidates, trimmed)
|
||||
}
|
||||
|
||||
if shouldSkipDirectOptionalDriverDownloads(definition.Type) {
|
||||
return candidates
|
||||
restrictToExplicitArtifact := shouldRestrictToExplicitVersionArtifact(definition, selectedVersion)
|
||||
appendPublishedURLs := func() {
|
||||
if tag := currentDriverReleaseTag(); tag != "" {
|
||||
if publishedURL, ok := resolvePublishedDriverDownloadURLForTag(definition, selectedVersion, tag); ok {
|
||||
appendURL(publishedURL)
|
||||
}
|
||||
}
|
||||
if publishedURL, ok := resolveLatestPublishedDriverDownloadURL(definition); ok {
|
||||
appendURL(publishedURL)
|
||||
}
|
||||
}
|
||||
|
||||
if !restrictToExplicitArtifact && shouldPreferPublishedOptionalDriverDownloads(driverType) {
|
||||
appendPublishedURLs()
|
||||
}
|
||||
|
||||
if parsed, err := url.Parse(strings.TrimSpace(rawURL)); err == nil {
|
||||
@@ -4720,17 +4811,12 @@ func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL
|
||||
appendURL(parsed.String())
|
||||
}
|
||||
}
|
||||
if shouldRestrictToExplicitVersionArtifact(definition, selectedVersion) {
|
||||
if restrictToExplicitArtifact {
|
||||
return candidates
|
||||
}
|
||||
|
||||
if tag := currentDriverReleaseTag(); tag != "" {
|
||||
if publishedURL, ok := resolvePublishedDriverDownloadURLForTag(definition, selectedVersion, tag); ok {
|
||||
appendURL(publishedURL)
|
||||
}
|
||||
}
|
||||
if publishedURL, ok := resolveLatestPublishedDriverDownloadURL(definition); ok {
|
||||
appendURL(publishedURL)
|
||||
if !shouldPreferPublishedOptionalDriverDownloads(driverType) {
|
||||
appendPublishedURLs()
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
@@ -4755,7 +4841,7 @@ func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targe
|
||||
if statErr != nil || info.IsDir() {
|
||||
continue
|
||||
}
|
||||
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, absPath); validateErr != nil {
|
||||
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, absPath); validateErr != nil {
|
||||
continue
|
||||
}
|
||||
if !isReusableOptionalDriverAgentRevisionCurrent(driverType, absPath) {
|
||||
@@ -5342,6 +5428,24 @@ func resolveLatestPublishedDriverDownloadURL(definition driverDefinition) (strin
|
||||
if driverType == "" {
|
||||
return "", false
|
||||
}
|
||||
if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
|
||||
if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache("latest"); ok {
|
||||
if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
|
||||
return driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
|
||||
return driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
assetNames := optionalDriverReleaseAssetNames(driverType)
|
||||
if len(assetNames) == 0 {
|
||||
return "", false
|
||||
|
||||
@@ -380,9 +380,21 @@ func TestDuckDBWindowsBuildUsesDynamicLibraryTag(t *testing.T) {
|
||||
if !shouldSkipReusableAgentCandidate("duckdb", "") {
|
||||
t.Fatal("expected DuckDB Windows install to skip reusable static agent candidates")
|
||||
}
|
||||
urls := resolveOptionalDriverAgentDownloadURLs(driverDefinition{Type: "duckdb"}, "https://example.com/duckdb-driver-agent-windows-amd64.exe", "")
|
||||
if len(urls) != 0 {
|
||||
t.Fatalf("expected DuckDB Windows install to skip single-file direct downloads, got %v", urls)
|
||||
seedReleaseAssetCacheEntry(t, "latest", map[string]int64{
|
||||
duckDBWindowsDriverZipAssetName: 19 << 20,
|
||||
}, map[string]int64{
|
||||
duckDBWindowsDriverZipAssetName: 19 << 20,
|
||||
})
|
||||
legacyDirectURL := "https://example.com/duckdb-driver-agent-windows-amd64.exe"
|
||||
urls := resolveOptionalDriverAgentDownloadURLs(driverDefinition{Type: "duckdb"}, legacyDirectURL, "")
|
||||
if len(urls) < 2 {
|
||||
t.Fatalf("expected DuckDB Windows install to keep dedicated zip ahead of legacy direct candidate, got %v", urls)
|
||||
}
|
||||
if urls[0] != driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName) {
|
||||
t.Fatalf("expected DuckDB Windows dedicated zip candidate first, got %v", urls)
|
||||
}
|
||||
if urls[1] != legacyDirectURL {
|
||||
t.Fatalf("expected DuckDB Windows to keep legacy direct candidate after dedicated zip, got %v", urls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,6 +449,83 @@ func TestInstallOptionalDriverAgentFromLocalZipExtractsDuckDBDLL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadOptionalDriverAgentBinaryInstallsDuckDBDedicatedZip(t *testing.T) {
|
||||
if runtime.GOOS != "windows" || runtime.GOARCH != "amd64" {
|
||||
t.Skip("DuckDB dedicated zip flow only applies on windows/amd64")
|
||||
}
|
||||
|
||||
originalValidateFunc := validateOptionalDriverAgentExecutableFunc
|
||||
validateOptionalDriverAgentExecutableFunc = func(driverType string, executablePath string) error {
|
||||
return nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
validateOptionalDriverAgentExecutableFunc = originalValidateFunc
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
zipPath := filepath.Join(tmpDir, duckDBWindowsDriverZipAssetName)
|
||||
zipFile, err := os.Create(zipPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create zip failed: %v", err)
|
||||
}
|
||||
zw := zip.NewWriter(zipFile)
|
||||
for name, content := range map[string]string{
|
||||
"Windows/duckdb-driver-agent-windows-amd64.exe": "agent",
|
||||
"Windows/duckdb.dll": "dll",
|
||||
} {
|
||||
w, err := zw.Create(name)
|
||||
if err != nil {
|
||||
t.Fatalf("create zip entry %s failed: %v", name, err)
|
||||
}
|
||||
if _, err := w.Write([]byte(content)); err != nil {
|
||||
t.Fatalf("write zip entry %s failed: %v", name, err)
|
||||
}
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
t.Fatalf("close zip writer failed: %v", err)
|
||||
}
|
||||
if err := zipFile.Close(); err != nil {
|
||||
t.Fatalf("close zip file failed: %v", err)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, zipPath)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
target := filepath.Join(tmpDir, "install", "duckdb-driver-agent.exe")
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
t.Fatalf("create install dir failed: %v", err)
|
||||
}
|
||||
|
||||
hash, err := downloadOptionalDriverAgentBinary(nil, driverDefinition{Type: "duckdb", Name: "DuckDB"}, server.URL+"/"+duckDBWindowsDriverZipAssetName+"?source=release", target)
|
||||
if err != nil {
|
||||
t.Fatalf("download dedicated zip failed: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(hash) == "" {
|
||||
t.Fatal("expected hash for installed duckdb agent")
|
||||
}
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("expected duckdb agent to be installed: %v", err)
|
||||
}
|
||||
dllBytes, err := os.ReadFile(filepath.Join(filepath.Dir(target), "duckdb.dll"))
|
||||
if err != nil {
|
||||
t.Fatalf("expected duckdb.dll to be installed: %v", err)
|
||||
}
|
||||
if string(dllBytes) != "dll" {
|
||||
t.Fatalf("unexpected duckdb.dll content: %q", string(dllBytes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalDriverDownloadZipURLAcceptsQueryString(t *testing.T) {
|
||||
if !isOptionalDriverDownloadZipURL("https://example.com/duckdb-driver.zip?token=abc") {
|
||||
t.Fatal("expected signed zip URL to be treated as zip download")
|
||||
}
|
||||
if isOptionalDriverDownloadZipURL("https://example.com/duckdb-driver-agent.exe?token=abc") {
|
||||
t.Fatal("expected exe URL with query to remain non-zip download")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadDriverPackageRejectsUnsupportedMongoVersion(t *testing.T) {
|
||||
app := &App{}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user