mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-25 16:04:02 +08:00
1374 lines
39 KiB
Go
1374 lines
39 KiB
Go
package app
|
||
|
||
import (
|
||
"archive/zip"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
stdRuntime "runtime"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"GoNavi-Wails/internal/db"
|
||
"GoNavi-Wails/internal/logger"
|
||
)
|
||
|
||
func optionalDriverPublicTypeName(driverType string) string {
|
||
switch normalizeDriverType(driverType) {
|
||
case "diros":
|
||
return "doris"
|
||
default:
|
||
return normalizeDriverType(driverType)
|
||
}
|
||
}
|
||
|
||
func optionalDriverExecutableBaseNameForType(typeName string) string {
|
||
base := strings.TrimSpace(typeName)
|
||
if base == "" {
|
||
base = "unknown"
|
||
}
|
||
name := fmt.Sprintf("%s-driver-agent", base)
|
||
if stdRuntime.GOOS == "windows" {
|
||
return name + ".exe"
|
||
}
|
||
return name
|
||
}
|
||
|
||
func optionalDriverReleaseAssetNameForType(typeName string, goos string, goarch string) string {
|
||
base := strings.TrimSpace(typeName)
|
||
if base == "" {
|
||
base = "unknown"
|
||
}
|
||
name := fmt.Sprintf("%s-driver-agent-%s-%s", base, goos, goarch)
|
||
if strings.EqualFold(goos, "windows") {
|
||
return name + ".exe"
|
||
}
|
||
return name
|
||
}
|
||
|
||
func optionalDriverNameStemCandidates(driverType string, selectedVersion string) []string {
|
||
candidates := make([]string, 0, 3)
|
||
seen := make(map[string]struct{}, 3)
|
||
appendStem := func(stem string) {
|
||
trimmed := strings.TrimSpace(stem)
|
||
if trimmed == "" {
|
||
return
|
||
}
|
||
if _, ok := seen[trimmed]; ok {
|
||
return
|
||
}
|
||
seen[trimmed] = struct{}{}
|
||
candidates = append(candidates, trimmed)
|
||
}
|
||
|
||
base := fmt.Sprintf("%s-driver-agent", optionalDriverPublicTypeName(driverType))
|
||
if normalizeDriverType(driverType) == "mongodb" {
|
||
switch resolveMongoDriverMajorFromVersion(selectedVersion) {
|
||
case 1:
|
||
appendStem(base + "-v1")
|
||
case 2:
|
||
appendStem(base + "-v2")
|
||
appendStem(base)
|
||
default:
|
||
appendStem(base)
|
||
}
|
||
return candidates
|
||
}
|
||
|
||
appendStem(base)
|
||
return candidates
|
||
}
|
||
|
||
func optionalDriverExecutableBaseNamesForVersion(driverType string, selectedVersion string) []string {
|
||
names := make([]string, 0, 2)
|
||
seen := make(map[string]struct{}, 2)
|
||
appendName := func(stem string) {
|
||
name := strings.TrimSpace(stem)
|
||
if strings.TrimSpace(name) == "" {
|
||
return
|
||
}
|
||
if stdRuntime.GOOS == "windows" {
|
||
name += ".exe"
|
||
}
|
||
if _, ok := seen[name]; ok {
|
||
return
|
||
}
|
||
seen[name] = struct{}{}
|
||
names = append(names, name)
|
||
}
|
||
|
||
for _, stem := range optionalDriverNameStemCandidates(driverType, selectedVersion) {
|
||
appendName(stem)
|
||
}
|
||
return names
|
||
}
|
||
|
||
func optionalDriverExecutableBaseNames(driverType string) []string {
|
||
return optionalDriverExecutableBaseNamesForVersion(driverType, "")
|
||
}
|
||
|
||
func optionalDriverReleaseAssetNamesForVersion(driverType string, selectedVersion string) []string {
|
||
names := make([]string, 0, 2)
|
||
seen := make(map[string]struct{}, 2)
|
||
appendName := func(stem string) {
|
||
trimmedStem := strings.TrimSpace(stem)
|
||
if trimmedStem == "" {
|
||
return
|
||
}
|
||
name := fmt.Sprintf("%s-%s-%s", trimmedStem, stdRuntime.GOOS, stdRuntime.GOARCH)
|
||
if strings.EqualFold(stdRuntime.GOOS, "windows") {
|
||
name += ".exe"
|
||
}
|
||
if strings.TrimSpace(name) == "" {
|
||
return
|
||
}
|
||
if _, ok := seen[name]; ok {
|
||
return
|
||
}
|
||
seen[name] = struct{}{}
|
||
names = append(names, name)
|
||
}
|
||
|
||
for _, stem := range optionalDriverNameStemCandidates(driverType, selectedVersion) {
|
||
appendName(stem)
|
||
}
|
||
return names
|
||
}
|
||
|
||
func optionalDriverReleaseAssetNames(driverType string) []string {
|
||
return optionalDriverReleaseAssetNamesForVersion(driverType, "")
|
||
}
|
||
|
||
func optionalDriverExecutableBaseName(driverType string) string {
|
||
names := optionalDriverExecutableBaseNames(driverType)
|
||
if len(names) == 0 {
|
||
return optionalDriverExecutableBaseNameForType("")
|
||
}
|
||
return names[0]
|
||
}
|
||
|
||
func optionalDriverReleaseAssetName(driverType string) string {
|
||
names := optionalDriverReleaseAssetNames(driverType)
|
||
if len(names) == 0 {
|
||
return optionalDriverReleaseAssetNameForType("", stdRuntime.GOOS, stdRuntime.GOARCH)
|
||
}
|
||
return names[0]
|
||
}
|
||
|
||
func optionalDriverReleaseAssetNameForVersion(driverType string, selectedVersion string) string {
|
||
names := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion)
|
||
if len(names) == 0 {
|
||
return optionalDriverReleaseAssetNameForType("", stdRuntime.GOOS, stdRuntime.GOARCH)
|
||
}
|
||
return names[0]
|
||
}
|
||
|
||
func currentDriverReleaseTag() string {
|
||
currentVersion := normalizeVersion(getCurrentVersion())
|
||
if currentVersion == "" || currentVersion == "0.0.0" {
|
||
return ""
|
||
}
|
||
if isDevelopmentDriverReleaseVersion(currentVersion) {
|
||
return driverReleaseDevTag
|
||
}
|
||
return "v" + currentVersion
|
||
}
|
||
|
||
func isDevelopmentDriverReleaseVersion(version string) bool {
|
||
normalized := strings.ToLower(strings.TrimSpace(normalizeVersion(version)))
|
||
if normalized == "" || normalized == "0.0.0" {
|
||
return false
|
||
}
|
||
if strings.HasPrefix(normalized, "dev-") {
|
||
return true
|
||
}
|
||
for _, marker := range []string{"-dev", "-test", "-local", "-snapshot"} {
|
||
if strings.Contains(normalized, marker) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func driverReleaseDownloadURL(tag string, assetName string) string {
|
||
tagName := strings.TrimSpace(tag)
|
||
asset := strings.TrimSpace(assetName)
|
||
if tagName == "" || asset == "" {
|
||
return ""
|
||
}
|
||
return fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", driverReleaseRepo, url.PathEscape(tagName), url.PathEscape(asset))
|
||
}
|
||
|
||
func driverReleaseLatestDownloadURL(assetName string) string {
|
||
asset := strings.TrimSpace(assetName)
|
||
if asset == "" {
|
||
return ""
|
||
}
|
||
return fmt.Sprintf("https://github.com/%s/releases/latest/download/%s", driverReleaseRepo, url.PathEscape(asset))
|
||
}
|
||
|
||
func findReleaseAssetByName(release *githubRelease, assetNames []string) (githubAsset, bool) {
|
||
if release == nil || len(release.Assets) == 0 || len(assetNames) == 0 {
|
||
return githubAsset{}, false
|
||
}
|
||
for _, expected := range assetNames {
|
||
trimmed := strings.TrimSpace(expected)
|
||
if trimmed == "" {
|
||
continue
|
||
}
|
||
for _, asset := range release.Assets {
|
||
if strings.EqualFold(strings.TrimSpace(asset.Name), trimmed) {
|
||
return asset, true
|
||
}
|
||
}
|
||
}
|
||
return githubAsset{}, false
|
||
}
|
||
|
||
func driverReleaseAssetAPIURL(asset githubAsset) string {
|
||
urlText := strings.TrimSpace(asset.URL)
|
||
if urlText != "" {
|
||
name := strings.TrimSpace(asset.Name)
|
||
if name == "" {
|
||
return urlText
|
||
}
|
||
parsed, err := url.Parse(urlText)
|
||
if err != nil {
|
||
return urlText
|
||
}
|
||
parsed.Fragment = name
|
||
return parsed.String()
|
||
}
|
||
urlText = strings.TrimSpace(asset.BrowserDownloadURL)
|
||
if urlText == "" {
|
||
return ""
|
||
}
|
||
return urlText
|
||
}
|
||
|
||
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 optionalDriverBundleEntryPathsForVersion(driverType string, selectedVersion string) []string {
|
||
platformDir := optionalDriverBundlePlatformDir(stdRuntime.GOOS)
|
||
assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion)
|
||
result := make([]string, 0, len(assetNames))
|
||
seen := make(map[string]struct{}, len(assetNames))
|
||
for _, assetName := range assetNames {
|
||
entry := filepath.ToSlash(filepath.Join(platformDir, assetName))
|
||
if _, ok := seen[entry]; ok {
|
||
continue
|
||
}
|
||
seen[entry] = struct{}{}
|
||
result = append(result, entry)
|
||
}
|
||
return result
|
||
}
|
||
|
||
func optionalDriverBundleEntryPaths(driverType string) []string {
|
||
return optionalDriverBundleEntryPathsForVersion(driverType, "")
|
||
}
|
||
|
||
func optionalDriverBundleEntryPathForVersion(driverType string, selectedVersion string) string {
|
||
paths := optionalDriverBundleEntryPathsForVersion(driverType, selectedVersion)
|
||
if len(paths) == 0 {
|
||
return filepath.ToSlash(filepath.Join(optionalDriverBundlePlatformDir(stdRuntime.GOOS), optionalDriverReleaseAssetNameForVersion(driverType, selectedVersion)))
|
||
}
|
||
return paths[0]
|
||
}
|
||
|
||
func optionalDriverBundleEntryPath(driverType string) string {
|
||
return optionalDriverBundleEntryPathForVersion(driverType, "")
|
||
}
|
||
|
||
func resolveOptionalDriverAssetSize(sizeByAsset map[string]int64, driverType string) int64 {
|
||
if len(sizeByAsset) == 0 {
|
||
return 0
|
||
}
|
||
for _, assetName := range optionalDriverReleaseAssetNames(driverType) {
|
||
sizeBytes := sizeByAsset[assetName]
|
||
if sizeBytes > 0 {
|
||
return sizeBytes
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func resolveOptionalDriverAssetSizeForVersion(sizeByAsset map[string]int64, driverType string, version string) int64 {
|
||
if len(sizeByAsset) == 0 {
|
||
return 0
|
||
}
|
||
for _, assetName := range optionalDriverReleaseAssetNamesForVersion(driverType, version) {
|
||
sizeBytes := sizeByAsset[assetName]
|
||
if sizeBytes > 0 {
|
||
return sizeBytes
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
if tag := currentDriverReleaseTag(); tag != "" {
|
||
if release, err := fetchReleaseByTag(tag); err == nil {
|
||
if asset, ok := findReleaseAssetByName(release, []string{optionalDriverBundleAssetName}); ok {
|
||
appendURL(driverReleaseAssetAPIURL(asset))
|
||
}
|
||
}
|
||
appendURL(driverReleaseDownloadURL(tag, optionalDriverBundleAssetName))
|
||
}
|
||
if release, err := fetchLatestReleaseForDriverAssets(); err == nil {
|
||
if asset, ok := findReleaseAssetByName(release, []string{optionalDriverBundleAssetName}); ok {
|
||
appendURL(driverReleaseAssetAPIURL(asset))
|
||
}
|
||
}
|
||
appendURL(driverReleaseLatestDownloadURL(optionalDriverBundleAssetName))
|
||
return candidates
|
||
}
|
||
|
||
func optionalDriverBundleCacheDir() (string, error) {
|
||
cacheDir := filepath.Join(os.TempDir(), "gonavi-driver-bundle-cache")
|
||
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||
return "", err
|
||
}
|
||
return cacheDir, nil
|
||
}
|
||
|
||
func optionalDriverBundleCachePath(bundleURL string) (string, error) {
|
||
cacheDir, err := optionalDriverBundleCacheDir()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
sum := sha256.Sum256([]byte(strings.TrimSpace(bundleURL)))
|
||
return filepath.Join(cacheDir, hex.EncodeToString(sum[:])+".zip"), nil
|
||
}
|
||
|
||
func cleanupOptionalDriverBundleCache(keepPaths ...string) {
|
||
cacheDir, err := optionalDriverBundleCacheDir()
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
keep := make(map[string]struct{}, len(keepPaths)+4)
|
||
for _, path := range keepPaths {
|
||
if strings.TrimSpace(path) != "" {
|
||
keep[filepath.Clean(path)] = struct{}{}
|
||
}
|
||
}
|
||
optionalDriverBundleDownloadMu.Lock()
|
||
for _, state := range optionalDriverBundleDownloads {
|
||
if state != nil && strings.TrimSpace(state.path) != "" {
|
||
keep[filepath.Clean(state.path)] = struct{}{}
|
||
}
|
||
}
|
||
optionalDriverBundleDownloadMu.Unlock()
|
||
|
||
type cacheFile struct {
|
||
path string
|
||
modTime time.Time
|
||
}
|
||
cacheFiles := make([]cacheFile, 0)
|
||
now := time.Now()
|
||
entries, err := os.ReadDir(cacheDir)
|
||
if err != nil {
|
||
return
|
||
}
|
||
for _, entry := range entries {
|
||
if entry.IsDir() {
|
||
continue
|
||
}
|
||
path := filepath.Join(cacheDir, entry.Name())
|
||
cleanPath := filepath.Clean(path)
|
||
if _, ok := keep[cleanPath]; ok {
|
||
continue
|
||
}
|
||
info, statErr := entry.Info()
|
||
if statErr != nil {
|
||
continue
|
||
}
|
||
name := strings.ToLower(strings.TrimSpace(entry.Name()))
|
||
if strings.HasSuffix(name, ".tmp") {
|
||
if now.Sub(info.ModTime()) > 24*time.Hour {
|
||
_ = os.Remove(path)
|
||
}
|
||
continue
|
||
}
|
||
if !strings.HasSuffix(name, ".zip") {
|
||
continue
|
||
}
|
||
if now.Sub(info.ModTime()) > optionalDriverBundleCacheMaxAge {
|
||
_ = os.Remove(path)
|
||
continue
|
||
}
|
||
cacheFiles = append(cacheFiles, cacheFile{path: path, modTime: info.ModTime()})
|
||
}
|
||
if len(cacheFiles) <= optionalDriverBundleCacheMaxFiles {
|
||
return
|
||
}
|
||
sort.Slice(cacheFiles, func(i, j int) bool {
|
||
return cacheFiles[i].modTime.After(cacheFiles[j].modTime)
|
||
})
|
||
for _, item := range cacheFiles[optionalDriverBundleCacheMaxFiles:] {
|
||
_ = os.Remove(item.path)
|
||
}
|
||
}
|
||
|
||
func downloadOptionalDriverBundleToCache(bundleURL string, onProgress func(downloaded, total int64)) (string, error) {
|
||
cachePath, err := optionalDriverBundleCachePath(bundleURL)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
tempPath := cachePath + fmt.Sprintf(".%d.tmp", time.Now().UnixNano())
|
||
_ = os.Remove(tempPath)
|
||
if _, err := downloadFileWithHashWithTimeout(bundleURL, tempPath, onProgress, optionalDriverBundleDownloadTimeout); err != nil {
|
||
_ = os.Remove(tempPath)
|
||
return "", err
|
||
}
|
||
if err := os.Remove(cachePath); err != nil && !os.IsNotExist(err) {
|
||
_ = os.Remove(tempPath)
|
||
return "", err
|
||
}
|
||
if err := os.Rename(tempPath, cachePath); err != nil {
|
||
_ = os.Remove(tempPath)
|
||
return "", err
|
||
}
|
||
reader, err := zip.OpenReader(cachePath)
|
||
if err != nil {
|
||
_ = os.Remove(cachePath)
|
||
return "", fmt.Errorf("open driver bundle failed: %w", err)
|
||
}
|
||
if err := reader.Close(); err != nil {
|
||
_ = os.Remove(cachePath)
|
||
return "", fmt.Errorf("close driver bundle failed: %w", err)
|
||
}
|
||
cleanupOptionalDriverBundleCache(cachePath)
|
||
return cachePath, nil
|
||
}
|
||
|
||
func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloaded, total int64), onWaiting func()) (string, error) {
|
||
trimmedURL := strings.TrimSpace(bundleURL)
|
||
if trimmedURL == "" {
|
||
return "", newLocalizedDriverBackendError("driver_manager.backend.error.bundle_url_empty", nil, nil)
|
||
}
|
||
|
||
for {
|
||
optionalDriverBundleDownloadMu.Lock()
|
||
state, ok := optionalDriverBundleDownloads[trimmedURL]
|
||
if ok {
|
||
if state.finished {
|
||
path := strings.TrimSpace(state.path)
|
||
err := state.err
|
||
if err == nil && path != "" && fileExists(path) {
|
||
optionalDriverBundleDownloadMu.Unlock()
|
||
return path, nil
|
||
}
|
||
delete(optionalDriverBundleDownloads, trimmedURL)
|
||
optionalDriverBundleDownloadMu.Unlock()
|
||
continue
|
||
}
|
||
done := state.done
|
||
optionalDriverBundleDownloadMu.Unlock()
|
||
if onWaiting != nil {
|
||
onWaiting()
|
||
}
|
||
<-done
|
||
optionalDriverBundleDownloadMu.Lock()
|
||
path := strings.TrimSpace(state.path)
|
||
err := state.err
|
||
if err == nil && path != "" && fileExists(path) {
|
||
optionalDriverBundleDownloadMu.Unlock()
|
||
return path, nil
|
||
}
|
||
if current, exists := optionalDriverBundleDownloads[trimmedURL]; exists && current == state {
|
||
delete(optionalDriverBundleDownloads, trimmedURL)
|
||
}
|
||
optionalDriverBundleDownloadMu.Unlock()
|
||
if err == nil {
|
||
err = fmt.Errorf("driver bundle cache file is unavailable")
|
||
}
|
||
return "", err
|
||
}
|
||
|
||
state = &optionalDriverBundleDownloadState{done: make(chan struct{})}
|
||
optionalDriverBundleDownloads[trimmedURL] = state
|
||
optionalDriverBundleDownloadMu.Unlock()
|
||
|
||
path, err := downloadOptionalDriverBundleToCache(trimmedURL, onProgress)
|
||
optionalDriverBundleDownloadMu.Lock()
|
||
state.path = path
|
||
state.err = err
|
||
state.finished = true
|
||
if err != nil {
|
||
delete(optionalDriverBundleDownloads, trimmedURL)
|
||
}
|
||
close(state.done)
|
||
optionalDriverBundleDownloadMu.Unlock()
|
||
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return path, nil
|
||
}
|
||
}
|
||
|
||
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 == "" {
|
||
return
|
||
}
|
||
if _, ok := seen[trimmed]; ok {
|
||
return
|
||
}
|
||
seen[trimmed] = struct{}{}
|
||
candidates = append(candidates, trimmed)
|
||
}
|
||
|
||
restrictToExplicitArtifact := shouldRestrictToExplicitVersionArtifact(definition, selectedVersion)
|
||
appendPublishedURLs := func() {
|
||
if tag := currentDriverReleaseTag(); tag != "" {
|
||
if publishedURL, ok := resolvePublishedDriverDownloadURLForTag(definition, selectedVersion, tag); ok {
|
||
appendURL(publishedURL)
|
||
}
|
||
}
|
||
if publishedURL, ok := resolveLatestPublishedDriverDownloadURLForVersion(definition, selectedVersion); ok {
|
||
appendURL(publishedURL)
|
||
}
|
||
}
|
||
|
||
if !restrictToExplicitArtifact && shouldPreferPublishedOptionalDriverDownloads(driverType) {
|
||
appendPublishedURLs()
|
||
}
|
||
|
||
if parsed, err := url.Parse(strings.TrimSpace(rawURL)); err == nil {
|
||
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
|
||
case "http", "https":
|
||
appendURL(parsed.String())
|
||
}
|
||
}
|
||
if restrictToExplicitArtifact {
|
||
return candidates
|
||
}
|
||
|
||
if !shouldPreferPublishedOptionalDriverDownloads(driverType) {
|
||
appendPublishedURLs()
|
||
}
|
||
return candidates
|
||
}
|
||
|
||
func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targetPath string) (string, bool) {
|
||
driverType := normalizeDriverType(definition.Type)
|
||
targetAbs, _ := filepath.Abs(targetPath)
|
||
candidates := resolveOptionalDriverAgentCandidatePaths(definition)
|
||
for _, candidate := range candidates {
|
||
candidate = strings.TrimSpace(candidate)
|
||
if candidate == "" {
|
||
continue
|
||
}
|
||
absPath, err := filepath.Abs(candidate)
|
||
if err != nil || absPath == "" {
|
||
continue
|
||
}
|
||
if targetAbs != "" && absPath == targetAbs {
|
||
continue
|
||
}
|
||
info, statErr := os.Stat(absPath)
|
||
if statErr != nil || info.IsDir() {
|
||
continue
|
||
}
|
||
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, absPath); validateErr != nil {
|
||
continue
|
||
}
|
||
if !isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType, absPath) {
|
||
continue
|
||
}
|
||
return absPath, true
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
func isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType string, executablePath string) bool {
|
||
expected := strings.TrimSpace(db.OptionalDriverAgentRevision(driverType))
|
||
if expected == "" {
|
||
return true
|
||
}
|
||
actual, current, err := optionalDriverAgentRevisionCurrent(driverType, executablePath)
|
||
displayName := resolveDriverDisplayName(driverDefinition{Type: driverType})
|
||
if err != nil {
|
||
logger.Warnf("可复用 %s 驱动代理候选版本元数据不可用,仍允许安装:path=%s err=%v;建议在驱动管理中重装", displayName, executablePath, err)
|
||
return true
|
||
}
|
||
if !current {
|
||
actualLabel := strings.TrimSpace(actual)
|
||
if actualLabel == "" {
|
||
actualLabel = "空"
|
||
}
|
||
logger.Warnf("可复用 %s 驱动代理候选 revision 不匹配,仍允许安装:path=%s actual=%s expected=%s;建议在驱动管理中重装", displayName, executablePath, actualLabel, expected)
|
||
return true
|
||
}
|
||
return true
|
||
}
|
||
|
||
func resolveOptionalDriverAgentCandidatePaths(definition driverDefinition) []string {
|
||
driverType := normalizeDriverType(definition.Type)
|
||
names := optionalDriverExecutableBaseNames(driverType)
|
||
assetNames := optionalDriverReleaseAssetNames(driverType)
|
||
pathTypeNames := make([]string, 0, 2)
|
||
seenPathType := make(map[string]struct{}, 2)
|
||
appendPathType := func(typeName string) {
|
||
trimmed := strings.TrimSpace(typeName)
|
||
if trimmed == "" {
|
||
return
|
||
}
|
||
if _, ok := seenPathType[trimmed]; ok {
|
||
return
|
||
}
|
||
seenPathType[trimmed] = struct{}{}
|
||
pathTypeNames = append(pathTypeNames, trimmed)
|
||
}
|
||
appendPathType(optionalDriverPublicTypeName(driverType))
|
||
|
||
candidates := make([]string, 0, 12)
|
||
appendPath := func(pathText string) {
|
||
trimmed := strings.TrimSpace(pathText)
|
||
if trimmed != "" {
|
||
candidates = append(candidates, trimmed)
|
||
}
|
||
}
|
||
|
||
if exePath, err := os.Executable(); err == nil && strings.TrimSpace(exePath) != "" {
|
||
resolved := exePath
|
||
if evalPath, evalErr := filepath.EvalSymlinks(exePath); evalErr == nil && strings.TrimSpace(evalPath) != "" {
|
||
resolved = evalPath
|
||
}
|
||
exeDir := filepath.Dir(resolved)
|
||
for _, name := range names {
|
||
appendPath(filepath.Join(exeDir, name))
|
||
}
|
||
for _, assetName := range assetNames {
|
||
appendPath(filepath.Join(exeDir, assetName))
|
||
}
|
||
for _, typeName := range pathTypeNames {
|
||
for _, name := range names {
|
||
appendPath(filepath.Join(exeDir, "drivers", typeName, name))
|
||
}
|
||
for _, assetName := range assetNames {
|
||
appendPath(filepath.Join(exeDir, "drivers", typeName, assetName))
|
||
}
|
||
}
|
||
|
||
resourcesDir := filepath.Clean(filepath.Join(exeDir, "..", "Resources"))
|
||
for _, typeName := range pathTypeNames {
|
||
for _, name := range names {
|
||
appendPath(filepath.Join(resourcesDir, "drivers", typeName, name))
|
||
}
|
||
for _, assetName := range assetNames {
|
||
appendPath(filepath.Join(resourcesDir, "drivers", typeName, assetName))
|
||
}
|
||
}
|
||
}
|
||
if wd, err := os.Getwd(); err == nil && strings.TrimSpace(wd) != "" {
|
||
for _, assetName := range assetNames {
|
||
appendPath(filepath.Join(wd, "dist", assetName))
|
||
appendPath(filepath.Join(wd, assetName))
|
||
}
|
||
}
|
||
|
||
unique := make([]string, 0, len(candidates))
|
||
seen := make(map[string]struct{}, len(candidates))
|
||
for _, item := range candidates {
|
||
if _, ok := seen[item]; ok {
|
||
continue
|
||
}
|
||
seen[item] = struct{}{}
|
||
unique = append(unique, item)
|
||
}
|
||
return unique
|
||
}
|
||
|
||
func resolveDriverDisplayName(definition driverDefinition) string {
|
||
if strings.TrimSpace(definition.Name) != "" {
|
||
return strings.TrimSpace(definition.Name)
|
||
}
|
||
if strings.TrimSpace(definition.Type) != "" {
|
||
return strings.TrimSpace(definition.Type)
|
||
}
|
||
return defaultAppText("driver_manager.backend.driver_fallback_name", nil)
|
||
}
|
||
|
||
func activateOptionalDriverAgentBinary(driverType string, installPath string, runtimePath string) error {
|
||
source := strings.TrimSpace(installPath)
|
||
target := strings.TrimSpace(runtimePath)
|
||
if source == "" || target == "" {
|
||
return fmt.Errorf("agent path is empty")
|
||
}
|
||
if source == target {
|
||
return nil
|
||
}
|
||
|
||
absSource := source
|
||
absTarget := target
|
||
if value, err := filepath.Abs(source); err == nil && strings.TrimSpace(value) != "" {
|
||
absSource = value
|
||
}
|
||
if value, err := filepath.Abs(target); err == nil && strings.TrimSpace(value) != "" {
|
||
absTarget = value
|
||
}
|
||
if strings.EqualFold(absSource, absTarget) {
|
||
return nil
|
||
}
|
||
if err := copyAgentBinary(source, target); err != nil {
|
||
return err
|
||
}
|
||
return copyOptionalDriverSupportFilesFromDirectory(driverType, filepath.Dir(source), filepath.Dir(target))
|
||
}
|
||
|
||
func copyAgentBinary(sourcePath, targetPath string) error {
|
||
src, err := os.Open(sourcePath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer src.Close()
|
||
|
||
tempPath := targetPath + ".tmp"
|
||
_ = os.Remove(tempPath)
|
||
dst, err := os.Create(tempPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if _, err := io.Copy(dst, src); err != nil {
|
||
dst.Close()
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
if err := dst.Sync(); err != nil {
|
||
dst.Close()
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
if err := dst.Close(); err != nil {
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
if chmodErr := os.Chmod(tempPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
|
||
_ = os.Remove(tempPath)
|
||
return chmodErr
|
||
}
|
||
if err := renameTempFileOverTarget(tempPath, targetPath); err != nil {
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
if chmodErr := os.Chmod(targetPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
|
||
return chmodErr
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func extractZipFileToPath(file *zip.File, targetPath string) error {
|
||
if file == nil {
|
||
return newLocalizedDriverBackendError("driver_manager.backend.error.zip_entry_empty", nil, nil)
|
||
}
|
||
src, err := file.Open()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer src.Close()
|
||
tempPath := targetPath + ".tmp"
|
||
_ = os.Remove(tempPath)
|
||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||
return err
|
||
}
|
||
dst, err := os.Create(tempPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if _, err := io.Copy(dst, src); err != nil {
|
||
dst.Close()
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
if err := dst.Sync(); err != nil {
|
||
dst.Close()
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
if err := dst.Close(); err != nil {
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
if err := renameTempFileOverTarget(tempPath, targetPath); err != nil {
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func copyOptionalDriverSupportFile(sourcePath, targetPath string) error {
|
||
src, err := os.Open(sourcePath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer src.Close()
|
||
|
||
tempPath := targetPath + ".tmp"
|
||
_ = os.Remove(tempPath)
|
||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||
return err
|
||
}
|
||
dst, err := os.Create(tempPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if _, err := io.Copy(dst, src); err != nil {
|
||
dst.Close()
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
if err := dst.Sync(); err != nil {
|
||
dst.Close()
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
if err := dst.Close(); err != nil {
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
if err := renameTempFileOverTarget(tempPath, targetPath); err != nil {
|
||
_ = os.Remove(tempPath)
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func renameTempFileOverTarget(tempPath, targetPath string) error {
|
||
if err := os.Rename(tempPath, targetPath); err == nil {
|
||
return nil
|
||
} else {
|
||
firstErr := err
|
||
if removeErr := os.Remove(targetPath); removeErr != nil && !os.IsNotExist(removeErr) {
|
||
return firstErr
|
||
}
|
||
if retryErr := os.Rename(tempPath, targetPath); retryErr != nil {
|
||
return retryErr
|
||
}
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func copyOptionalDriverSupportFilesFromDirectory(driverType string, sourceDir string, targetDir string) error {
|
||
names := optionalDriverSupportFileNames(driverType)
|
||
if len(names) == 0 {
|
||
return nil
|
||
}
|
||
sourceRoot := strings.TrimSpace(sourceDir)
|
||
targetRoot := strings.TrimSpace(targetDir)
|
||
if sourceRoot == "" || targetRoot == "" {
|
||
return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_directory_empty", nil, nil)
|
||
}
|
||
for _, name := range names {
|
||
sourcePath := filepath.Join(sourceRoot, name)
|
||
targetPath := filepath.Join(targetRoot, name)
|
||
if err := copyOptionalDriverSupportFile(sourcePath, targetPath); err != nil {
|
||
return newLocalizedDriverBackendError("driver_manager.backend.error.copy_runtime_dependency_entry_failed", map[string]any{"name": name}, err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func findOptionalDriverSupportFileInZip(files []*zip.File, agentEntryName string, supportName string) *zip.File {
|
||
normalizedAgent := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(agentEntryName), "./"))
|
||
agentDir := filepath.ToSlash(filepath.Dir(normalizedAgent))
|
||
if agentDir == "." {
|
||
agentDir = ""
|
||
}
|
||
candidatePaths := []string{}
|
||
if agentDir != "" {
|
||
candidatePaths = append(candidatePaths, filepath.ToSlash(filepath.Join(agentDir, supportName)))
|
||
}
|
||
candidatePaths = append(candidatePaths, supportName)
|
||
|
||
for _, candidate := range candidatePaths {
|
||
for _, file := range files {
|
||
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
|
||
if name == candidate {
|
||
return file
|
||
}
|
||
}
|
||
for _, file := range files {
|
||
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
|
||
if strings.EqualFold(name, candidate) {
|
||
return file
|
||
}
|
||
}
|
||
}
|
||
for _, file := range files {
|
||
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
|
||
if strings.EqualFold(filepath.Base(name), supportName) {
|
||
return file
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func extractOptionalDriverSupportFilesFromZip(files []*zip.File, driverType string, agentEntryName string, targetDir string) error {
|
||
names := optionalDriverSupportFileNames(driverType)
|
||
if len(names) == 0 {
|
||
return nil
|
||
}
|
||
targetRoot := strings.TrimSpace(targetDir)
|
||
if targetRoot == "" {
|
||
return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_target_directory_empty", nil, nil)
|
||
}
|
||
for _, name := range names {
|
||
entry := findOptionalDriverSupportFileInZip(files, agentEntryName, name)
|
||
if entry == nil {
|
||
return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_entry_missing", map[string]any{"name": name}, nil)
|
||
}
|
||
if err := extractZipFileToPath(entry, filepath.Join(targetRoot, name)); err != nil {
|
||
return newLocalizedDriverBackendError("driver_manager.backend.error.extract_runtime_dependency_failed", map[string]any{"name": name}, err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func scaleProgress(downloaded, total, start, end int64) (int64, int64) {
|
||
if end <= start {
|
||
return end, 100
|
||
}
|
||
if total <= 0 {
|
||
return start, 100
|
||
}
|
||
if downloaded < 0 {
|
||
downloaded = 0
|
||
}
|
||
if downloaded > total {
|
||
downloaded = total
|
||
}
|
||
span := end - start
|
||
return start + ((downloaded * span) / total), 100
|
||
}
|
||
|
||
func preloadOptionalDriverPackageSizes(definitions []driverDefinition) map[string]int64 {
|
||
result := make(map[string]int64)
|
||
if len(definitions) == 0 {
|
||
return result
|
||
}
|
||
|
||
needed := make([]string, 0, len(definitions))
|
||
for _, definition := range definitions {
|
||
normalizedType := normalizeDriverType(definition.Type)
|
||
if normalizedType == "" || definition.BuiltIn {
|
||
continue
|
||
}
|
||
if !db.IsOptionalGoDriver(normalizedType) {
|
||
continue
|
||
}
|
||
if !db.IsOptionalGoDriverBuildIncluded(normalizedType) {
|
||
continue
|
||
}
|
||
needed = append(needed, normalizedType)
|
||
}
|
||
if len(needed) == 0 {
|
||
return result
|
||
}
|
||
|
||
tag := currentDriverReleaseTag()
|
||
|
||
fillFromSizes := func(sizeByAsset map[string]int64, driverTypes []string) []string {
|
||
missing := make([]string, 0, len(driverTypes))
|
||
for _, driverType := range driverTypes {
|
||
sizeBytes := resolveOptionalDriverAssetSize(sizeByAsset, driverType)
|
||
if sizeBytes > 0 {
|
||
result[driverType] = sizeBytes
|
||
continue
|
||
}
|
||
missing = append(missing, driverType)
|
||
}
|
||
return missing
|
||
}
|
||
|
||
pending := needed
|
||
if tag != "" {
|
||
if sizeByAsset, _, err := loadReleaseAssetSizesCached("tag:"+tag, func() (*githubRelease, error) {
|
||
return fetchReleaseByTag(tag)
|
||
}); err == nil {
|
||
pending = fillFromSizes(sizeByAsset, pending)
|
||
}
|
||
}
|
||
if len(pending) == 0 {
|
||
return result
|
||
}
|
||
if sizeByAsset, _, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets); err == nil {
|
||
_ = fillFromSizes(sizeByAsset, pending)
|
||
}
|
||
return result
|
||
}
|
||
|
||
func loadReleaseAssetSizesCached(cacheKey string, fetch func() (*githubRelease, error)) (map[string]int64, map[string]bool, error) {
|
||
key := strings.TrimSpace(cacheKey)
|
||
if key == "" {
|
||
return nil, nil, newLocalizedDriverBackendError("driver_manager.backend.error.cache_key_empty", nil, nil)
|
||
}
|
||
|
||
driverReleaseSizeMu.RLock()
|
||
cached, ok := driverReleaseSizeMap[key]
|
||
driverReleaseSizeMu.RUnlock()
|
||
if ok {
|
||
ttl := driverReleaseAssetSizeCacheTTL
|
||
if strings.TrimSpace(cached.Err) != "" {
|
||
ttl = driverReleaseAssetSizeErrorCacheTTL
|
||
}
|
||
if time.Since(cached.LoadedAt) < ttl {
|
||
if strings.TrimSpace(cached.Err) != "" {
|
||
return nil, nil, errors.New(strings.TrimSpace(cached.Err))
|
||
}
|
||
return cached.SizeByKey, cached.PublishedAssets, nil
|
||
}
|
||
}
|
||
|
||
release, err := fetch()
|
||
entry := driverReleaseAssetSizeCacheEntry{
|
||
LoadedAt: time.Now(),
|
||
SizeByKey: map[string]int64{},
|
||
PublishedAssets: map[string]bool{},
|
||
}
|
||
if err != nil {
|
||
entry.Err = err.Error()
|
||
} else {
|
||
entry.SizeByKey = buildReleaseAssetSizeMap(release)
|
||
entry.PublishedAssets = buildReleaseAssetNameMap(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()
|
||
driverReleaseSizeMap[key] = entry
|
||
driverReleaseSizeMu.Unlock()
|
||
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
return entry.SizeByKey, entry.PublishedAssets, nil
|
||
}
|
||
|
||
func readReleaseAssetSizesFromCache(cacheKey string) (map[string]int64, map[string]bool, bool) {
|
||
key := strings.TrimSpace(cacheKey)
|
||
if key == "" {
|
||
return nil, nil, false
|
||
}
|
||
|
||
driverReleaseSizeMu.RLock()
|
||
cached, ok := driverReleaseSizeMap[key]
|
||
driverReleaseSizeMu.RUnlock()
|
||
if !ok {
|
||
return nil, nil, false
|
||
}
|
||
|
||
ttl := driverReleaseAssetSizeCacheTTL
|
||
if strings.TrimSpace(cached.Err) != "" {
|
||
ttl = driverReleaseAssetSizeErrorCacheTTL
|
||
}
|
||
if time.Since(cached.LoadedAt) >= ttl {
|
||
return nil, nil, false
|
||
}
|
||
if strings.TrimSpace(cached.Err) != "" {
|
||
return nil, nil, false
|
||
}
|
||
return cached.SizeByKey, cached.PublishedAssets, true
|
||
}
|
||
|
||
func buildReleaseAssetSizeMap(release *githubRelease) map[string]int64 {
|
||
sizes := make(map[string]int64)
|
||
if release == nil {
|
||
return sizes
|
||
}
|
||
for _, asset := range release.Assets {
|
||
name := strings.TrimSpace(asset.Name)
|
||
if name == "" || asset.Size <= 0 {
|
||
continue
|
||
}
|
||
sizes[name] = asset.Size
|
||
}
|
||
return sizes
|
||
}
|
||
|
||
func buildReleaseAssetNameMap(release *githubRelease) map[string]bool {
|
||
names := make(map[string]bool)
|
||
if release == nil {
|
||
return names
|
||
}
|
||
for _, asset := range release.Assets {
|
||
name := strings.TrimSpace(asset.Name)
|
||
if name == "" {
|
||
continue
|
||
}
|
||
names[name] = true
|
||
}
|
||
return names
|
||
}
|
||
|
||
func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64, error) {
|
||
if release == nil {
|
||
return nil, newLocalizedDriverBackendError("driver_manager.backend.error.release_empty", nil, nil)
|
||
}
|
||
indexURL := ""
|
||
for _, asset := range release.Assets {
|
||
if strings.EqualFold(strings.TrimSpace(asset.Name), optionalDriverBundleIndexAssetName) {
|
||
indexURL = strings.TrimSpace(asset.BrowserDownloadURL)
|
||
break
|
||
}
|
||
}
|
||
if indexURL == "" {
|
||
return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_asset_missing", nil, nil)
|
||
}
|
||
|
||
client := newHTTPClientWithGlobalProxy(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, newLocalizedDriverBackendError(
|
||
"driver_manager.backend.error.bundle_index_fetch_failed",
|
||
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, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_parse_failed", nil, err)
|
||
}
|
||
if len(index.Assets) == 0 {
|
||
return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_empty", nil, nil)
|
||
}
|
||
return index.Assets, nil
|
||
}
|
||
|
||
func fetchLatestReleaseForDriverAssets() (*githubRelease, error) {
|
||
return fetchDriverReleaseByURL(driverReleaseLatestAPIURL)
|
||
}
|
||
|
||
func resolveLatestPublishedDriverDownloadURL(definition driverDefinition) (string, bool) {
|
||
return resolveLatestPublishedDriverDownloadURLForVersion(definition, "")
|
||
}
|
||
|
||
func resolveLatestPublishedDriverDownloadURLForVersion(definition driverDefinition, selectedVersion string) (string, bool) {
|
||
driverType := normalizeDriverType(definition.Type)
|
||
if driverType == "" {
|
||
return "", false
|
||
}
|
||
if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
|
||
if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache("latest"); ok {
|
||
if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
|
||
if release, err := fetchLatestReleaseForDriverAssets(); err == nil {
|
||
if asset, found := findReleaseAssetByName(release, []string{duckDBWindowsDriverZipAssetName}); found {
|
||
return driverReleaseAssetAPIURL(asset), true
|
||
}
|
||
}
|
||
return driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName), true
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets)
|
||
if err != nil {
|
||
return "", false
|
||
}
|
||
if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
|
||
if release, relErr := fetchLatestReleaseForDriverAssets(); relErr == nil {
|
||
if asset, found := findReleaseAssetByName(release, []string{duckDBWindowsDriverZipAssetName}); found {
|
||
return driverReleaseAssetAPIURL(asset), true
|
||
}
|
||
}
|
||
return driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName), true
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion)
|
||
if len(assetNames) == 0 {
|
||
return "", false
|
||
}
|
||
|
||
if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache("latest"); ok {
|
||
for _, assetName := range assetNames {
|
||
if publishedAssets[assetName] && sizeByAsset[assetName] > 0 {
|
||
if release, err := fetchLatestReleaseForDriverAssets(); err == nil {
|
||
if asset, found := findReleaseAssetByName(release, []string{assetName}); found {
|
||
return driverReleaseAssetAPIURL(asset), true
|
||
}
|
||
}
|
||
return driverReleaseLatestDownloadURL(assetName), true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets)
|
||
if err != nil {
|
||
return "", false
|
||
}
|
||
for _, assetName := range assetNames {
|
||
if publishedAssets[assetName] && sizeByAsset[assetName] > 0 {
|
||
if release, relErr := fetchLatestReleaseForDriverAssets(); relErr == nil {
|
||
if asset, found := findReleaseAssetByName(release, []string{assetName}); found {
|
||
return driverReleaseAssetAPIURL(asset), true
|
||
}
|
||
}
|
||
return driverReleaseLatestDownloadURL(assetName), true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
func fetchReleaseByTag(tag string) (*githubRelease, error) {
|
||
tagName := strings.TrimSpace(tag)
|
||
if tagName == "" {
|
||
return nil, newLocalizedDriverBackendError("driver_manager.backend.error.tag_empty", nil, nil)
|
||
}
|
||
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/releases/tags/%s", driverReleaseRepo, url.PathEscape(tagName))
|
||
return fetchDriverReleaseByURL(apiURL)
|
||
}
|
||
|
||
func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) {
|
||
urlText := strings.TrimSpace(apiURL)
|
||
if urlText == "" {
|
||
return nil, newLocalizedDriverBackendError("driver_manager.backend.error.api_url_empty", nil, nil)
|
||
}
|
||
|
||
client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout)
|
||
req, err := http.NewRequest(http.MethodGet, urlText, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.Header.Set("User-Agent", "GoNavi-DriverManager")
|
||
req.Header.Set("Accept", "application/vnd.github+json")
|
||
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, newLocalizedDriverBackendError(
|
||
"driver_manager.backend.error.release_info_fetch_failed",
|
||
nil,
|
||
fmt.Errorf("HTTP %d", resp.StatusCode),
|
||
)
|
||
}
|
||
|
||
var release githubRelease
|
||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||
return nil, err
|
||
}
|
||
return &release, nil
|
||
}
|
||
|
||
func resolveDriverPackageSizeText(definition driverDefinition, pkg installedDriverPackage, packageMetaExists bool, packageSizeBytesMap map[string]int64, text func(string, map[string]any) string) string {
|
||
if definition.BuiltIn {
|
||
return driverManagerLocalizedText(text, "driver_manager.package_size.built_in", nil, "Built-in")
|
||
}
|
||
|
||
normalizedType := normalizeDriverType(definition.Type)
|
||
if packageMetaExists {
|
||
sizeBytes := readInstalledPackageSizeBytes(pkg)
|
||
if sizeBytes > 0 {
|
||
return formatSizeMB(sizeBytes)
|
||
}
|
||
}
|
||
if sizeBytes, ok := packageSizeBytesMap[normalizedType]; ok && sizeBytes > 0 {
|
||
return formatSizeMB(sizeBytes)
|
||
}
|
||
|
||
if !db.IsOptionalGoDriverBuildIncluded(normalizedType) {
|
||
return driverManagerLocalizedText(text, "driver_manager.package_size.pending_release", nil, "Pending release")
|
||
}
|
||
return "-"
|
||
}
|
||
|
||
func driverManagerLocalizedText(text func(string, map[string]any) string, key string, params map[string]any, fallback string) string {
|
||
if text == nil {
|
||
return fallback
|
||
}
|
||
localized := text(key, params)
|
||
if localized == "" {
|
||
return fallback
|
||
}
|
||
return localized
|
||
}
|
||
|
||
func readInstalledPackageSizeBytes(pkg installedDriverPackage) int64 {
|
||
pathText := strings.TrimSpace(pkg.ExecutablePath)
|
||
if pathText == "" {
|
||
pathText = strings.TrimSpace(pkg.FilePath)
|
||
}
|
||
if pathText == "" {
|
||
return 0
|
||
}
|
||
info, err := os.Stat(pathText)
|
||
if err != nil || info.IsDir() {
|
||
return 0
|
||
}
|
||
return info.Size()
|
||
}
|
||
|
||
func formatSizeMB(sizeBytes int64) string {
|
||
if sizeBytes <= 0 {
|
||
return "-"
|
||
}
|
||
sizeMB := float64(sizeBytes) / (1024 * 1024)
|
||
return fmt.Sprintf("%.2f MB", sizeMB)
|
||
}
|