Files
MyGoNavi/internal/app/methods_driver_assets.go
2026-06-23 14:04:25 +08:00

1374 lines
39 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}