Files
MyGoNavi/internal/app/methods_driver.go
Syngnat 663717d738 ♻️ refactor(driver-delivery): 重构可选驱动分发为总包+索引模式
- 工作流统一收敛驱动产物并打包单一压缩包
- 新增驱动总包索引读取与缓存合并逻辑
- 保留原单文件直链兼容并增加总包提取回退
2026-02-14 11:45:51 +08:00

1878 lines
61 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"
"os/exec"
"path/filepath"
stdRuntime "runtime"
"strings"
"sync"
"time"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/db"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type driverDefinition struct {
Type string `json:"type"`
Name string `json:"name"`
Engine string `json:"engine,omitempty"`
BuiltIn bool `json:"builtIn"`
PinnedVersion string `json:"pinnedVersion,omitempty"`
DefaultDownloadURL string `json:"defaultDownloadUrl,omitempty"`
DownloadSHA256 string `json:"downloadSha256,omitempty"`
ChecksumPolicy string `json:"checksumPolicy,omitempty"`
}
type installedDriverPackage struct {
DriverType string `json:"driverType"`
FilePath string `json:"filePath"`
FileName string `json:"fileName"`
ExecutablePath string `json:"executablePath,omitempty"`
DownloadURL string `json:"downloadUrl,omitempty"`
SHA256 string `json:"sha256,omitempty"`
DownloadedAt string `json:"downloadedAt"`
}
type driverStatusItem struct {
Type string `json:"type"`
Name string `json:"name"`
Engine string `json:"engine,omitempty"`
BuiltIn bool `json:"builtIn"`
PinnedVersion string `json:"pinnedVersion,omitempty"`
PackageSizeText string `json:"packageSizeText,omitempty"`
RuntimeAvailable bool `json:"runtimeAvailable"`
PackageInstalled bool `json:"packageInstalled"`
Connectable bool `json:"connectable"`
DefaultDownloadURL string `json:"defaultDownloadUrl,omitempty"`
InstallDir string `json:"installDir,omitempty"`
PackagePath string `json:"packagePath,omitempty"`
PackageFileName string `json:"packageFileName,omitempty"`
ExecutablePath string `json:"executablePath,omitempty"`
DownloadedAt string `json:"downloadedAt,omitempty"`
Message string `json:"message,omitempty"`
}
const driverDownloadProgressEvent = "driver:download-progress"
type driverDownloadProgressPayload struct {
DriverType string `json:"driverType"`
Status string `json:"status"`
Percent float64 `json:"percent"`
Downloaded int64 `json:"downloaded"`
Total int64 `json:"total"`
Message string `json:"message,omitempty"`
}
type pinnedDriverPackage struct {
Version string
DownloadURL string
SHA256 string
Policy string
Engine string
}
type driverManifestFile struct {
Engine string `json:"engine"`
DefaultEngine string `json:"defaultEngine"`
DefaultEngine2 string `json:"default_engine"`
Drivers map[string]driverManifestItem `json:"drivers"`
}
type driverManifestItem struct {
Version string `json:"version"`
DownloadURL string `json:"downloadUrl"`
DownloadURL2 string `json:"download_url"`
SHA256 string `json:"sha256"`
ChecksumPolicy string `json:"checksumPolicy"`
ChecksumPolicy2 string `json:"checksum_policy"`
Engine string `json:"engine"`
}
type driverManifestCacheEntry struct {
LoadedAt time.Time
Packages map[string]pinnedDriverPackage
Err string
}
type driverReleaseAssetSizeCacheEntry struct {
LoadedAt time.Time
SizeByKey map[string]int64
Err string
}
type driverBundleAssetIndex struct {
Assets map[string]int64 `json:"assets"`
}
const (
// 默认使用内置 manifest避免依赖网络与外部仓库 404。
defaultDriverManifestURLValue = "builtin://manifest"
optionalDriverBundleAssetName = "GoNavi-DriverAgents.zip"
optionalDriverBundleIndexAssetName = "GoNavi-DriverAgents-Index.json"
driverManifestCacheTTL = 5 * time.Minute
driverReleaseAssetSizeCacheTTL = 30 * time.Minute
driverReleaseAssetSizeErrorCacheTTL = 30 * time.Second
driverReleaseAssetSizeProbeTimeout = 4 * time.Second
driverBundleIndexMaxSize = 1 << 20
driverManifestMaxSize = 2 << 20
driverChecksumPolicyStrict = "strict"
driverChecksumPolicyWarn = "warn"
driverChecksumPolicyOff = "off"
driverEngineGo = "go"
driverEngineExternal = "external"
)
const builtinDriverManifestJSON = `{
"engine": "go",
"drivers": {
"mysql": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off" },
"mariadb": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/mariadb" },
"diros": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/diros" },
"sphinx": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sphinx" },
"sqlserver": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sqlserver" },
"sqlite": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sqlite" },
"duckdb": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/duckdb" },
"dameng": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/dameng" },
"kingbase": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/kingbase" },
"highgo": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/highgo" },
"vastbase": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/vastbase" },
"mongodb": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/mongodb" },
"tdengine": { "engine": "go", "version": "go-embedded", "checksumPolicy": "off", "downloadUrl": "builtin://activate/tdengine" }
}
}`
var (
driverManifestCacheMu sync.RWMutex
driverManifestCache = make(map[string]driverManifestCacheEntry)
driverReleaseSizeMu sync.RWMutex
driverReleaseSizeMap = make(map[string]driverReleaseAssetSizeCacheEntry)
)
var pinnedDriverPackageMap = map[string]pinnedDriverPackage{
"postgres": {
Version: "go-embedded",
Policy: driverChecksumPolicyOff,
Engine: driverEngineGo,
},
}
func (a *App) SelectDriverDownloadDirectory(currentDir string) connection.QueryResult {
defaultDir := strings.TrimSpace(currentDir)
if defaultDir == "" {
defaultDir = defaultDriverDownloadDirectory()
} else if !filepath.IsAbs(defaultDir) {
if abs, err := filepath.Abs(defaultDir); err == nil {
defaultDir = abs
}
}
selection, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
Title: "选择驱动下载目录",
DefaultDirectory: defaultDir,
CanCreateDirectories: true,
})
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if strings.TrimSpace(selection) == "" {
return connection.QueryResult{Success: false, Message: "Cancelled"}
}
resolved, err := resolveDriverDownloadDirectory(selection)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Data: map[string]interface{}{
"path": resolved,
"defaultPath": defaultDriverDownloadDirectory(),
"isDefaultPath": false,
},
}
}
func (a *App) SelectDriverPackageFile(currentPath string) connection.QueryResult {
defaultDir := strings.TrimSpace(currentPath)
if defaultDir == "" {
defaultDir = defaultDriverDownloadDirectory()
}
if filepath.Ext(defaultDir) != "" {
defaultDir = filepath.Dir(defaultDir)
}
if !filepath.IsAbs(defaultDir) {
if abs, err := filepath.Abs(defaultDir); err == nil {
defaultDir = abs
}
}
selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "选择驱动包文件",
DefaultDirectory: defaultDir,
Filters: []runtime.FileFilter{
{DisplayName: "所有文件", Pattern: "*"},
},
})
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if strings.TrimSpace(selection) == "" {
return connection.QueryResult{Success: false, Message: "Cancelled"}
}
if abs, err := filepath.Abs(selection); err == nil {
selection = abs
}
return connection.QueryResult{Success: true, Data: map[string]interface{}{"path": selection}}
}
func (a *App) ResolveDriverDownloadDirectory(directory string) connection.QueryResult {
resolved, err := resolveDriverDownloadDirectory(directory)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: map[string]interface{}{"path": resolved}}
}
func (a *App) ConfigureDriverRuntimeDirectory(directory string) connection.QueryResult {
resolved, err := resolveDriverDownloadDirectory(directory)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
db.SetExternalDriverDownloadDirectory(resolved)
return connection.QueryResult{
Success: true,
Data: map[string]interface{}{
"path": resolved,
"defaultPath": defaultDriverDownloadDirectory(),
"isDefaultPath": strings.TrimSpace(directory) == "",
},
Message: "驱动运行时目录已生效",
}
}
func (a *App) ResolveDriverRepositoryURL(repositoryURL string) connection.QueryResult {
resolved, err := resolveDriverRepositoryURL(repositoryURL)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: map[string]interface{}{"url": resolved}}
}
func (a *App) ResolveDriverPackageDownloadURL(driverType string, repositoryURL string) connection.QueryResult {
effectivePackages, manifestErr := resolveEffectiveDriverPackages(repositoryURL)
definition, ok := resolveDriverDefinitionWithPackages(driverType, effectivePackages)
if !ok {
return connection.QueryResult{Success: false, Message: "不支持的驱动类型"}
}
engine := effectiveDriverEngine(definition)
if definition.BuiltIn {
return connection.QueryResult{Success: false, Message: "内置驱动无需下载扩展包"}
}
if err := ensureOptionalDriverBuildAvailable(definition); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if engine == driverEngineGo && !definition.BuiltIn {
urlText := strings.TrimSpace(definition.DefaultDownloadURL)
if urlText == "" {
urlText = fmt.Sprintf("builtin://activate/%s", definition.Type)
}
data := map[string]interface{}{
"url": urlText,
"driverType": definition.Type,
"driverName": definition.Name,
"engine": engine,
"manifestError": errorMessage(manifestErr),
}
if strings.TrimSpace(definition.DownloadSHA256) != "" {
data["sha256"] = strings.TrimSpace(definition.DownloadSHA256)
}
return connection.QueryResult{Success: true, Data: data}
}
return connection.QueryResult{Success: false, Message: "当前仅支持纯 Go 可选驱动的安装启用"}
}
func (a *App) GetDriverStatusList(downloadDir string, manifestURL string) connection.QueryResult {
resolvedDir, err := resolveDriverDownloadDirectory(downloadDir)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
db.SetExternalDriverDownloadDirectory(resolvedDir)
effectivePackages, manifestErr := resolveEffectiveDriverPackages(manifestURL)
definitions := allDriverDefinitionsWithPackages(effectivePackages)
packageSizeBytesMap := preloadOptionalDriverPackageSizes(definitions)
items := make([]driverStatusItem, 0, len(definitions))
for _, definition := range definitions {
engine := effectiveDriverEngine(definition)
runtimeAvailable, runtimeReason := db.DriverRuntimeSupportStatus(definition.Type)
pkg, packageMetaExists := readInstalledDriverPackage(resolvedDir, definition.Type)
packageInstalled := definition.BuiltIn || packageMetaExists
if runtimeAvailable && db.IsOptionalGoDriver(definition.Type) {
packageInstalled = true
}
item := driverStatusItem{
Type: definition.Type,
Name: definition.Name,
Engine: engine,
BuiltIn: definition.BuiltIn,
PinnedVersion: definition.PinnedVersion,
PackageSizeText: resolveDriverPackageSizeText(definition, pkg, packageMetaExists, packageSizeBytesMap),
RuntimeAvailable: runtimeAvailable,
PackageInstalled: packageInstalled,
Connectable: runtimeAvailable,
DefaultDownloadURL: definition.DefaultDownloadURL,
InstallDir: driverInstallDir(resolvedDir, definition.Type),
}
if packageMetaExists {
item.PackagePath = pkg.FilePath
item.PackageFileName = pkg.FileName
item.DownloadedAt = pkg.DownloadedAt
item.ExecutablePath = pkg.ExecutablePath
}
switch {
case definition.BuiltIn:
item.Message = "内置驱动,可直接连接"
case runtimeAvailable:
item.Message = "纯 Go 驱动已启用,可直接连接"
case packageInstalled && strings.TrimSpace(runtimeReason) != "":
item.Message = runtimeReason
case packageInstalled:
item.Message = "驱动已安装,待生效"
case strings.TrimSpace(runtimeReason) != "":
item.Message = runtimeReason
default:
if strings.TrimSpace(definition.PinnedVersion) != "" {
item.Message = fmt.Sprintf("未启用(版本:%s", strings.TrimSpace(definition.PinnedVersion))
} else {
item.Message = "未启用"
}
}
items = append(items, item)
}
return connection.QueryResult{
Success: true,
Data: map[string]interface{}{
"downloadDir": resolvedDir,
"drivers": items,
"manifestURL": resolveManifestURLForView(manifestURL),
"manifestError": errorMessage(manifestErr),
},
}
}
func (a *App) InstallLocalDriverPackage(driverType string, filePath string, downloadDir string) connection.QueryResult {
definition, ok := resolveDriverDefinition(driverType)
if !ok {
return connection.QueryResult{Success: false, Message: "不支持的驱动类型"}
}
if definition.BuiltIn {
return connection.QueryResult{Success: false, Message: "内置驱动无需安装扩展包"}
}
if err := ensureOptionalDriverBuildAvailable(definition); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
engine := effectiveDriverEngine(definition)
if !(engine == driverEngineGo && !definition.BuiltIn) {
return connection.QueryResult{Success: false, Message: "当前仅支持纯 Go 可选驱动的安装启用"}
}
resolvedDir, err := resolveDriverDownloadDirectory(downloadDir)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
db.SetExternalDriverDownloadDirectory(resolvedDir)
hash := ""
if pathText := strings.TrimSpace(filePath); pathText != "" {
if fileHash, hashErr := hashFileSHA256(pathText); hashErr == nil {
hash = fileHash
}
}
a.emitDriverDownloadProgress(definition.Type, "start", 0, 0, "开始安装")
meta := installedDriverPackage{
DriverType: definition.Type,
FilePath: "",
FileName: "embedded-go-driver",
DownloadURL: "local://activate",
SHA256: hash,
DownloadedAt: time.Now().Format(time.RFC3339),
}
if err := writeInstalledDriverPackage(resolvedDir, definition.Type, meta); err != nil {
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, err.Error())
return connection.QueryResult{Success: false, Message: err.Error()}
}
a.emitDriverDownloadProgress(definition.Type, "done", 1, 1, "安装完成(纯 Go 驱动已启用)")
return connection.QueryResult{Success: true, Message: "驱动安装成功", Data: map[string]interface{}{
"driverType": definition.Type,
"driverName": definition.Name,
"engine": engine,
}}
}
func (a *App) DownloadDriverPackage(driverType string, downloadURL string, downloadDir string) connection.QueryResult {
definition, ok := resolveDriverDefinition(driverType)
if !ok {
return connection.QueryResult{Success: false, Message: "不支持的驱动类型"}
}
engine := effectiveDriverEngine(definition)
if definition.BuiltIn {
return connection.QueryResult{Success: false, Message: "内置驱动无需下载扩展包"}
}
if err := ensureOptionalDriverBuildAvailable(definition); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if !(engine == driverEngineGo && !definition.BuiltIn) {
return connection.QueryResult{Success: false, Message: "当前仅支持纯 Go 可选驱动的安装启用"}
}
urlText := strings.TrimSpace(downloadURL)
if urlText == "" {
urlText = strings.TrimSpace(definition.DefaultDownloadURL)
}
if urlText == "" {
urlText = fmt.Sprintf("builtin://activate/%s", definition.Type)
}
resolvedDir, err := resolveDriverDownloadDirectory(downloadDir)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
db.SetExternalDriverDownloadDirectory(resolvedDir)
if db.IsOptionalGoDriver(definition.Type) {
displayName := strings.TrimSpace(definition.Name)
if displayName == "" {
displayName = strings.TrimSpace(definition.Type)
}
a.emitDriverDownloadProgress(definition.Type, "start", 0, 100, fmt.Sprintf("开始安装 %s 驱动代理", displayName))
meta, installErr := installOptionalDriverAgentPackage(a, definition, resolvedDir, urlText)
if installErr != nil {
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, installErr.Error())
return connection.QueryResult{Success: false, Message: installErr.Error()}
}
a.emitDriverDownloadProgress(definition.Type, "downloading", 95, 100, "写入驱动元数据")
if writeErr := writeInstalledDriverPackage(resolvedDir, definition.Type, meta); writeErr != nil {
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, writeErr.Error())
return connection.QueryResult{Success: false, Message: writeErr.Error()}
}
a.emitDriverDownloadProgress(definition.Type, "done", 100, 100, fmt.Sprintf("%s 驱动代理安装完成", displayName))
return connection.QueryResult{Success: true, Message: "驱动安装成功", Data: map[string]interface{}{
"driverType": definition.Type,
"driverName": definition.Name,
"engine": engine,
}}
}
a.emitDriverDownloadProgress(definition.Type, "start", 0, 0, "开始安装")
meta := installedDriverPackage{
DriverType: definition.Type,
FilePath: "",
FileName: "embedded-go-driver",
DownloadURL: urlText,
SHA256: "",
DownloadedAt: time.Now().Format(time.RFC3339),
}
if err := writeInstalledDriverPackage(resolvedDir, definition.Type, meta); err != nil {
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, err.Error())
return connection.QueryResult{Success: false, Message: err.Error()}
}
a.emitDriverDownloadProgress(definition.Type, "done", 1, 1, "安装完成(纯 Go 驱动已启用)")
return connection.QueryResult{Success: true, Message: "驱动安装成功", Data: map[string]interface{}{
"driverType": definition.Type,
"driverName": definition.Name,
"engine": engine,
}}
}
func (a *App) RemoveDriverPackage(driverType string, downloadDir string) connection.QueryResult {
definition, ok := resolveDriverDefinition(driverType)
if !ok {
return connection.QueryResult{Success: false, Message: "不支持的驱动类型"}
}
if definition.BuiltIn {
return connection.QueryResult{Success: false, Message: "内置驱动不可移除"}
}
resolvedDir, err := resolveDriverDownloadDirectory(downloadDir)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
db.SetExternalDriverDownloadDirectory(resolvedDir)
driverDir := driverInstallDir(resolvedDir, definition.Type)
if err := os.RemoveAll(driverDir); err != nil {
return connection.QueryResult{Success: false, Message: fmt.Sprintf("移除驱动包失败:%v", err)}
}
return connection.QueryResult{Success: true, Message: "驱动包已移除", Data: map[string]interface{}{
"driverType": definition.Type,
"driverName": definition.Name,
}}
}
func (a *App) emitDriverDownloadProgress(driverType string, status string, downloaded, total int64, message string) {
if a.ctx == nil {
return
}
payload := driverDownloadProgressPayload{
DriverType: normalizeDriverType(driverType),
Status: strings.TrimSpace(status),
Percent: 0,
Downloaded: downloaded,
Total: total,
Message: strings.TrimSpace(message),
}
if payload.DriverType == "" {
payload.DriverType = "unknown"
}
if payload.Status == "" {
payload.Status = "downloading"
}
if total > 0 {
payload.Percent = (float64(downloaded) / float64(total)) * 100
if payload.Percent < 0 {
payload.Percent = 0
}
if payload.Percent > 100 {
payload.Percent = 100
}
}
if payload.Status == "done" && payload.Percent < 100 {
payload.Percent = 100
}
runtime.EventsEmit(a.ctx, driverDownloadProgressEvent, payload)
}
func defaultDriverDownloadDirectory() string {
root, err := db.ResolveExternalDriverRoot("")
if err == nil && strings.TrimSpace(root) != "" {
return root
}
return filepath.Join(os.TempDir(), "gonavi-drivers")
}
func resolveDriverDownloadDirectory(directory string) (string, error) {
return db.ResolveExternalDriverRoot(directory)
}
func normalizeDriverType(driverType string) string {
normalized := strings.ToLower(strings.TrimSpace(driverType))
switch normalized {
case "doris":
return "diros"
case "postgresql":
return "postgres"
default:
return normalized
}
}
func normalizeDriverEngine(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case driverEngineGo:
return driverEngineGo
case "jdbc":
return driverEngineExternal
case driverEngineExternal, "exec", "binary":
return driverEngineExternal
default:
return ""
}
}
func normalizeDriverChecksumPolicy(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case driverChecksumPolicyStrict:
return driverChecksumPolicyStrict
case driverChecksumPolicyOff:
return driverChecksumPolicyOff
case driverChecksumPolicyWarn:
return driverChecksumPolicyWarn
default:
return driverChecksumPolicyWarn
}
}
func effectiveDriverEngine(definition driverDefinition) string {
if definition.BuiltIn {
return driverEngineGo
}
engine := normalizeDriverEngine(definition.Engine)
if engine == "" {
return driverEngineExternal
}
return engine
}
func resolveDriverDefinition(driverType string) (driverDefinition, bool) {
return resolveDriverDefinitionWithPackages(driverType, nil)
}
func resolveDriverDefinitionWithPackages(driverType string, packages map[string]pinnedDriverPackage) (driverDefinition, bool) {
normalized := normalizeDriverType(driverType)
for _, definition := range allDriverDefinitionsWithPackages(packages) {
if normalizeDriverType(definition.Type) == normalized {
return definition, true
}
}
return driverDefinition{}, false
}
func allDriverDefinitionsWithPackages(packages map[string]pinnedDriverPackage) []driverDefinition {
return []driverDefinition{
{Type: "mysql", Name: "MySQL", Engine: driverEngineGo, BuiltIn: true},
{Type: "oracle", Name: "Oracle", Engine: driverEngineGo, BuiltIn: true},
{Type: "redis", Name: "Redis", Engine: driverEngineGo, BuiltIn: true},
{Type: "postgres", Name: "PostgreSQL", Engine: driverEngineGo, BuiltIn: true},
// 其他数据源需要先在驱动管理中“安装启用”。
buildOptionalGoDriverDefinition("mariadb", "MariaDB", packages),
buildOptionalGoDriverDefinition("diros", "Diros", packages),
buildOptionalGoDriverDefinition("sphinx", "Sphinx", packages),
buildOptionalGoDriverDefinition("sqlserver", "SQL Server", packages),
buildOptionalGoDriverDefinition("sqlite", "SQLite", packages),
buildOptionalGoDriverDefinition("duckdb", "DuckDB", packages),
buildOptionalGoDriverDefinition("dameng", "Dameng", packages),
buildOptionalGoDriverDefinition("kingbase", "Kingbase", packages),
buildOptionalGoDriverDefinition("highgo", "HighGo", packages),
buildOptionalGoDriverDefinition("vastbase", "Vastbase", packages),
buildOptionalGoDriverDefinition("mongodb", "MongoDB", packages),
buildOptionalGoDriverDefinition("tdengine", "TDengine", packages),
}
}
func buildOptionalGoDriverDefinition(driverType string, driverName string, packages map[string]pinnedDriverPackage) driverDefinition {
spec := resolvedPinnedPackage(driverType, packages)
return driverDefinition{
Type: normalizeDriverType(driverType),
Name: driverName,
Engine: driverEngineGo,
BuiltIn: false,
PinnedVersion: strings.TrimSpace(spec.Version),
DefaultDownloadURL: strings.TrimSpace(spec.DownloadURL),
DownloadSHA256: strings.TrimSpace(spec.SHA256),
ChecksumPolicy: normalizeDriverChecksumPolicy(spec.Policy),
}
}
func ensureOptionalDriverBuildAvailable(definition driverDefinition) error {
driverType := normalizeDriverType(definition.Type)
if !db.IsOptionalGoDriver(driverType) {
return nil
}
if db.IsOptionalGoDriverBuildIncluded(driverType) {
return nil
}
driverName := strings.TrimSpace(definition.Name)
if driverName == "" {
driverName = strings.TrimSpace(definition.Type)
}
return fmt.Errorf("%s 当前发行包为精简构建,未内置该驱动;如需使用请安装 Full 版", driverName)
}
func driverPinnedPackage(driverType string) pinnedDriverPackage {
spec, ok := pinnedDriverPackageMap[normalizeDriverType(driverType)]
if !ok {
return pinnedDriverPackage{}
}
spec.Version = strings.TrimSpace(spec.Version)
spec.DownloadURL = strings.TrimSpace(spec.DownloadURL)
spec.SHA256 = strings.TrimSpace(spec.SHA256)
spec.Policy = normalizeDriverChecksumPolicy(spec.Policy)
spec.Engine = normalizeDriverEngine(spec.Engine)
return spec
}
func resolvedPinnedPackage(driverType string, packages map[string]pinnedDriverPackage) pinnedDriverPackage {
normalizedType := normalizeDriverType(driverType)
spec := driverPinnedPackage(normalizedType)
if packages != nil {
override, ok := packages[normalizedType]
if ok {
if strings.TrimSpace(override.Version) != "" {
spec.Version = strings.TrimSpace(override.Version)
}
if strings.TrimSpace(override.DownloadURL) != "" {
spec.DownloadURL = strings.TrimSpace(override.DownloadURL)
}
if strings.TrimSpace(override.SHA256) != "" {
spec.SHA256 = strings.TrimSpace(override.SHA256)
}
if strings.TrimSpace(override.Policy) != "" {
spec.Policy = normalizeDriverChecksumPolicy(override.Policy)
}
if strings.TrimSpace(override.Engine) != "" {
spec.Engine = normalizeDriverEngine(override.Engine)
}
}
}
if normalizedType == "postgres" {
spec.Engine = driverEngineGo
if strings.TrimSpace(spec.Version) == "" {
spec.Version = "go-embedded"
}
if strings.TrimSpace(spec.Policy) == "" {
spec.Policy = driverChecksumPolicyOff
}
}
return spec
}
func copyPinnedPackageMap(source map[string]pinnedDriverPackage) map[string]pinnedDriverPackage {
if len(source) == 0 {
return map[string]pinnedDriverPackage{}
}
result := make(map[string]pinnedDriverPackage, len(source))
for key, value := range source {
result[key] = pinnedDriverPackage{
Version: strings.TrimSpace(value.Version),
DownloadURL: strings.TrimSpace(value.DownloadURL),
SHA256: strings.TrimSpace(value.SHA256),
Policy: normalizeDriverChecksumPolicy(value.Policy),
Engine: normalizeDriverEngine(value.Engine),
}
}
return result
}
func resolveEffectiveDriverPackages(manifestURL string) (map[string]pinnedDriverPackage, error) {
effective := copyPinnedPackageMap(pinnedDriverPackageMap)
manifestPackages, err := resolveManifestDriverPackages(manifestURL)
if err != nil {
return effective, err
}
for driverType, item := range manifestPackages {
normalizedType := normalizeDriverType(driverType)
base := effective[normalizedType]
if strings.TrimSpace(item.Version) != "" {
base.Version = strings.TrimSpace(item.Version)
}
if strings.TrimSpace(item.DownloadURL) != "" {
base.DownloadURL = strings.TrimSpace(item.DownloadURL)
}
if strings.TrimSpace(item.SHA256) != "" {
base.SHA256 = strings.TrimSpace(item.SHA256)
}
if strings.TrimSpace(item.Policy) != "" {
base.Policy = normalizeDriverChecksumPolicy(item.Policy)
}
if strings.TrimSpace(item.Engine) != "" {
base.Engine = normalizeDriverEngine(item.Engine)
}
effective[normalizedType] = base
}
return effective, nil
}
func resolveDriverRepositoryURL(repositoryURL string) (string, error) {
urlText := strings.TrimSpace(repositoryURL)
if urlText == "" {
return defaultDriverManifestURLValue, nil
}
parsed, err := url.Parse(urlText)
if err == nil && parsed.Scheme != "" {
switch strings.ToLower(parsed.Scheme) {
case "http", "https":
return parsed.String(), nil
case "file":
if parsed.Path == "" {
return "", fmt.Errorf("无效的文件清单地址")
}
return urlText, nil
case "builtin":
if isBuiltinManifestURL(parsed) {
return defaultDriverManifestURLValue, nil
}
return "", fmt.Errorf("不支持的内置清单地址:%s", parsed.String())
default:
return "", fmt.Errorf("不支持的清单地址协议:%s", parsed.Scheme)
}
}
absPath, absErr := filepath.Abs(urlText)
if absErr != nil {
return "", absErr
}
return absPath, nil
}
func resolveManifestURLForView(manifestURL string) string {
resolved, err := resolveDriverRepositoryURL(manifestURL)
if err != nil {
return strings.TrimSpace(manifestURL)
}
return resolved
}
func resolveManifestDriverPackages(manifestURL string) (map[string]pinnedDriverPackage, error) {
resolvedURL, err := resolveDriverRepositoryURL(manifestURL)
if err != nil {
return nil, err
}
driverManifestCacheMu.RLock()
cached, ok := driverManifestCache[resolvedURL]
driverManifestCacheMu.RUnlock()
if ok && time.Since(cached.LoadedAt) < driverManifestCacheTTL {
if strings.TrimSpace(cached.Err) != "" {
return nil, errors.New(cached.Err)
}
return copyPinnedPackageMap(cached.Packages), nil
}
packages, loadErr := loadManifestPackages(resolvedURL)
entry := driverManifestCacheEntry{
LoadedAt: time.Now(),
Packages: copyPinnedPackageMap(packages),
}
if loadErr != nil {
entry.Err = loadErr.Error()
}
driverManifestCacheMu.Lock()
driverManifestCache[resolvedURL] = entry
driverManifestCacheMu.Unlock()
if loadErr != nil {
return nil, loadErr
}
return packages, nil
}
func loadManifestPackages(resolvedURL string) (map[string]pinnedDriverPackage, error) {
content, err := loadManifestContent(resolvedURL)
if err != nil {
return nil, err
}
var manifest driverManifestFile
if err := json.Unmarshal(content, &manifest); err != nil {
return nil, fmt.Errorf("解析驱动清单失败:%w", err)
}
defaultEngine := normalizeDriverEngine(manifest.Engine)
if defaultEngine == "" {
defaultEngine = normalizeDriverEngine(manifest.DefaultEngine)
}
if defaultEngine == "" {
defaultEngine = normalizeDriverEngine(manifest.DefaultEngine2)
}
result := make(map[string]pinnedDriverPackage)
for driverType, item := range manifest.Drivers {
normalizedType := normalizeDriverType(driverType)
if normalizedType == "" {
continue
}
downloadURL := strings.TrimSpace(item.DownloadURL)
if downloadURL == "" {
downloadURL = strings.TrimSpace(item.DownloadURL2)
}
policy := strings.TrimSpace(item.ChecksumPolicy)
if policy == "" {
policy = strings.TrimSpace(item.ChecksumPolicy2)
}
engine := normalizeDriverEngine(item.Engine)
if engine == "" {
engine = defaultEngine
}
result[normalizedType] = pinnedDriverPackage{
Version: strings.TrimSpace(item.Version),
DownloadURL: downloadURL,
SHA256: strings.TrimSpace(item.SHA256),
Policy: normalizeDriverChecksumPolicy(policy),
Engine: engine,
}
}
return result, nil
}
func loadManifestContent(resolvedURL string) ([]byte, error) {
trimmed := strings.TrimSpace(resolvedURL)
if trimmed == "" {
return nil, fmt.Errorf("驱动清单地址为空")
}
parsed, err := url.Parse(trimmed)
if err == nil {
scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
switch scheme {
case "http", "https":
client := &http.Client{Timeout: 12 * time.Second}
req, reqErr := http.NewRequest(http.MethodGet, parsed.String(), nil)
if reqErr != nil {
return nil, reqErr
}
req.Header.Set("User-Agent", "GoNavi-DriverManifest")
resp, doErr := client.Do(req)
if doErr != nil {
return nil, doErr
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("拉取驱动清单失败HTTP %d", resp.StatusCode)
}
limited := io.LimitReader(resp.Body, driverManifestMaxSize+1)
body, readErr := io.ReadAll(limited)
if readErr != nil {
return nil, readErr
}
if int64(len(body)) > driverManifestMaxSize {
return nil, fmt.Errorf("驱动清单超过大小限制")
}
return body, nil
case "file":
pathText := strings.TrimSpace(parsed.Path)
if pathText == "" {
return nil, fmt.Errorf("无效的本地驱动清单地址")
}
body, readErr := os.ReadFile(pathText)
if readErr != nil {
return nil, readErr
}
if int64(len(body)) > driverManifestMaxSize {
return nil, fmt.Errorf("驱动清单超过大小限制")
}
return body, nil
case "builtin":
if isBuiltinManifestURL(parsed) {
return []byte(builtinDriverManifestJSON), nil
}
return nil, fmt.Errorf("不支持的内置清单地址:%s", parsed.String())
}
}
body, readErr := os.ReadFile(trimmed)
if readErr != nil {
return nil, readErr
}
if int64(len(body)) > driverManifestMaxSize {
return nil, fmt.Errorf("驱动清单超过大小限制")
}
return body, nil
}
func isBuiltinManifestURL(parsed *url.URL) bool {
if parsed == nil {
return false
}
if strings.ToLower(strings.TrimSpace(parsed.Scheme)) != "builtin" {
return false
}
if strings.ToLower(strings.TrimSpace(parsed.Host)) != "manifest" {
return false
}
pathText := strings.TrimSpace(parsed.Path)
return pathText == "" || pathText == "/"
}
func errorMessage(err error) string {
if err == nil {
return ""
}
return strings.TrimSpace(err.Error())
}
func driverInstallDir(downloadDir string, driverType string) string {
root, err := resolveDriverDownloadDirectory(downloadDir)
if err != nil {
root = defaultDriverDownloadDirectory()
}
return filepath.Join(root, normalizeDriverType(driverType))
}
func installedDriverMetaPath(downloadDir string, driverType string) string {
return filepath.Join(driverInstallDir(downloadDir, driverType), "installed.json")
}
func readInstalledDriverPackage(downloadDir string, driverType string) (installedDriverPackage, bool) {
metaPath := installedDriverMetaPath(downloadDir, driverType)
content, err := os.ReadFile(metaPath)
if err != nil {
return installedDriverPackage{}, false
}
var meta installedDriverPackage
if err := json.Unmarshal(content, &meta); err != nil {
return installedDriverPackage{}, false
}
meta.DriverType = normalizeDriverType(meta.DriverType)
if strings.TrimSpace(meta.DriverType) == "" {
meta.DriverType = normalizeDriverType(driverType)
}
return meta, true
}
func writeInstalledDriverPackage(downloadDir string, driverType string, meta installedDriverPackage) error {
driverDir := driverInstallDir(downloadDir, driverType)
if err := os.MkdirAll(driverDir, 0o755); err != nil {
return fmt.Errorf("创建驱动目录失败:%w", err)
}
meta.DriverType = normalizeDriverType(driverType)
if meta.DownloadedAt == "" {
meta.DownloadedAt = time.Now().Format(time.RFC3339)
}
payload, err := json.MarshalIndent(meta, "", " ")
if err != nil {
return fmt.Errorf("写入驱动元数据失败:%w", err)
}
if err := os.WriteFile(installedDriverMetaPath(downloadDir, driverType), payload, 0o644); err != nil {
return fmt.Errorf("写入驱动元数据失败:%w", err)
}
return nil
}
func hashFileSHA256(filePath string) (string, error) {
pathText := strings.TrimSpace(filePath)
if pathText == "" {
return "", fmt.Errorf("文件路径为空")
}
file, err := os.Open(pathText)
if err != nil {
return "", err
}
defer file.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return "", err
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}
func installOptionalDriverAgentPackage(a *App, definition driverDefinition, resolvedDir string, downloadURL string) (installedDriverPackage, error) {
driverType := normalizeDriverType(definition.Type)
executablePath, err := db.ResolveOptionalDriverAgentExecutablePath(resolvedDir, driverType)
if err != nil {
return installedDriverPackage{}, err
}
downloadSource, hash, err := ensureOptionalDriverAgentBinary(a, definition, executablePath, downloadURL)
if err != nil {
return installedDriverPackage{}, err
}
if strings.TrimSpace(hash) == "" {
hash, err = hashFileSHA256(executablePath)
if err != nil {
return installedDriverPackage{}, fmt.Errorf("计算 %s 驱动代理摘要失败:%w", resolveDriverDisplayName(definition), err)
}
}
if strings.TrimSpace(downloadSource) == "" {
downloadSource = strings.TrimSpace(downloadURL)
}
return installedDriverPackage{
DriverType: driverType,
FilePath: executablePath,
FileName: filepath.Base(executablePath),
ExecutablePath: executablePath,
DownloadURL: strings.TrimSpace(downloadSource),
SHA256: hash,
DownloadedAt: time.Now().Format(time.RFC3339),
}, nil
}
func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, executablePath string, downloadURL string) (string, string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
info, err := os.Stat(executablePath)
if err == nil && !info.IsDir() {
hash, hashErr := hashFileSHA256(executablePath)
if hashErr != nil {
return "", "", fmt.Errorf("读取已安装 %s 驱动代理摘要失败:%w", displayName, hashErr)
}
return fmt.Sprintf("local://existing/%s-driver-agent", driverType), hash, nil
}
if err == nil && info.IsDir() {
return "", "", fmt.Errorf("%s 驱动代理路径被目录占用:%s", displayName, executablePath)
}
if mkErr := os.MkdirAll(filepath.Dir(executablePath), 0o755); mkErr != nil {
return "", "", fmt.Errorf("创建 %s 驱动目录失败:%w", displayName, mkErr)
}
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 10, 100, "检查本地驱动代理缓存")
}
if sourcePath, ok := findExistingOptionalDriverAgentCandidate(definition, executablePath); ok {
if copyErr := copyAgentBinary(sourcePath, executablePath); copyErr != nil {
return "", "", fmt.Errorf("复制预置 %s 驱动代理失败:%w", displayName, copyErr)
}
hash, hashErr := hashFileSHA256(executablePath)
if hashErr != nil {
return "", "", fmt.Errorf("计算预置 %s 驱动代理摘要失败:%w", displayName, hashErr)
}
return "file://" + sourcePath, hash, nil
}
downloadURLs := resolveOptionalDriverAgentDownloadURLs(definition, downloadURL)
var downloadErrs []string
if len(downloadURLs) > 0 {
for _, candidateURL := range downloadURLs {
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 20, 100, fmt.Sprintf("下载预编译 %s 驱动代理", displayName))
}
hash, dlErr := downloadOptionalDriverAgentBinary(a, definition, candidateURL, executablePath)
if dlErr == nil {
return candidateURL, hash, nil
}
downloadErrs = append(downloadErrs, fmt.Sprintf("%s: %s", candidateURL, strings.TrimSpace(dlErr.Error())))
}
}
bundleURLs := resolveOptionalDriverBundleDownloadURLs()
if len(bundleURLs) > 0 {
for _, bundleURL := range bundleURLs {
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 20, 100, fmt.Sprintf("从驱动总包提取 %s 代理", displayName))
}
source, hash, bundleErr := downloadOptionalDriverAgentFromBundle(a, definition, bundleURL, executablePath)
if bundleErr == nil {
return source, hash, nil
}
downloadErrs = append(downloadErrs, fmt.Sprintf("%s: %s", bundleURL, strings.TrimSpace(bundleErr.Error())))
}
}
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 92, 100, "未命中预编译包,尝试开发态本地构建")
}
hash, buildErr := buildOptionalDriverAgentFromSource(definition, executablePath)
if buildErr == nil {
return fmt.Sprintf("local://go-build/%s-driver-agent", driverType), hash, nil
}
var parts []string
if len(downloadErrs) > 0 {
parts = append(parts, "预编译包下载失败:"+strings.Join(downloadErrs, ""))
}
parts = append(parts, "本地构建失败:"+strings.TrimSpace(buildErr.Error()))
return "", "", errors.New(strings.Join(parts, ""))
}
func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlText string, executablePath string) (string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
trimmedURL := strings.TrimSpace(urlText)
if trimmedURL == "" {
return "", fmt.Errorf("下载地址为空")
}
tempPath := executablePath + ".tmp"
_ = os.Remove(tempPath)
hash, 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))
})
if err != nil {
_ = os.Remove(tempPath)
return "", fmt.Errorf("下载失败:%w", err)
}
if chmodErr := os.Chmod(tempPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
_ = os.Remove(tempPath)
return "", fmt.Errorf("设置代理权限失败:%w", chmodErr)
}
if renameErr := os.Rename(tempPath, executablePath); renameErr != nil {
_ = os.Remove(tempPath)
return "", fmt.Errorf("落地代理文件失败:%w", renameErr)
}
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", fmt.Errorf("设置代理权限失败:%w", chmodErr)
}
return hash, nil
}
func downloadOptionalDriverAgentFromBundle(a *App, definition driverDefinition, bundleURL, executablePath string) (string, string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
trimmedURL := strings.TrimSpace(bundleURL)
if trimmedURL == "" {
return "", "", fmt.Errorf("驱动总包下载地址为空")
}
bundleTempPath := executablePath + ".bundle.zip.tmp"
_ = os.Remove(bundleTempPath)
_, err := downloadFileWithHash(trimmedURL, bundleTempPath, func(downloaded, total int64) {
if a == nil {
return
}
scaledDownloaded, scaledTotal := scaleProgress(downloaded, total, 20, 78)
a.emitDriverDownloadProgress(driverType, "downloading", scaledDownloaded, scaledTotal, fmt.Sprintf("下载 %s 驱动总包", displayName))
})
if err != nil {
_ = os.Remove(bundleTempPath)
return "", "", fmt.Errorf("下载驱动总包失败:%w", err)
}
defer func() { _ = os.Remove(bundleTempPath) }()
reader, err := zip.OpenReader(bundleTempPath)
if err != nil {
return "", "", fmt.Errorf("打开驱动总包失败:%w", err)
}
defer reader.Close()
entryPath := optionalDriverBundleEntryPath(driverType)
expectedBaseName := optionalDriverReleaseAssetName(driverType)
findEntry := func() *zip.File {
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if name == entryPath {
return file
}
}
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if strings.EqualFold(name, entryPath) {
return file
}
}
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if strings.EqualFold(filepath.Base(name), expectedBaseName) {
return file
}
}
return nil
}
entry := findEntry()
if entry == nil {
return "", "", fmt.Errorf("驱动总包内未找到 %s期望路径 %s", displayName, entryPath)
}
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 84, 100, fmt.Sprintf("解压 %s 驱动代理", displayName))
}
src, err := entry.Open()
if err != nil {
return "", "", fmt.Errorf("读取驱动总包条目失败:%w", err)
}
defer src.Close()
tempPath := executablePath + ".tmp"
_ = os.Remove(tempPath)
dst, err := os.Create(tempPath)
if err != nil {
return "", "", fmt.Errorf("创建驱动代理临时文件失败:%w", err)
}
if _, err := io.Copy(dst, src); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return "", "", fmt.Errorf("写入驱动代理失败:%w", err)
}
if err := dst.Sync(); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return "", "", fmt.Errorf("落盘驱动代理失败:%w", err)
}
if err := dst.Close(); err != nil {
_ = os.Remove(tempPath)
return "", "", fmt.Errorf("关闭驱动代理文件失败:%w", err)
}
if chmodErr := os.Chmod(tempPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
_ = os.Remove(tempPath)
return "", "", fmt.Errorf("设置驱动代理权限失败:%w", chmodErr)
}
if err := os.Rename(tempPath, executablePath); err != nil {
_ = os.Remove(tempPath)
return "", "", fmt.Errorf("替换驱动代理失败:%w", err)
}
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", "", fmt.Errorf("设置驱动代理权限失败:%w", chmodErr)
}
hash, err := hashFileSHA256(executablePath)
if err != nil {
return "", "", fmt.Errorf("计算驱动代理摘要失败:%w", err)
}
source := fmt.Sprintf("%s#%s", trimmedURL, filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(entry.Name), "./")))
return source, hash, nil
}
func buildOptionalDriverAgentFromSource(definition driverDefinition, executablePath string) (string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
goPath, lookErr := exec.LookPath("go")
if lookErr != nil {
return "", fmt.Errorf("当前环境未安装 Go且未找到可用的 %s 预编译代理包", displayName)
}
tagName, tagErr := optionalDriverBuildTag(driverType)
if tagErr != nil {
return "", tagErr
}
projectRoot, rootErr := locateProjectRootForAgentBuild()
if rootErr != nil {
return "", rootErr
}
cmd := exec.Command(goPath, "build", "-tags", tagName, "-trimpath", "-ldflags", "-s -w", "-o", executablePath, "./cmd/optional-driver-agent")
cmd.Dir = projectRoot
output, buildErr := cmd.CombinedOutput()
if buildErr != nil {
return "", fmt.Errorf("构建 %s 驱动代理失败:%v输出%s", displayName, buildErr, strings.TrimSpace(string(output)))
}
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", fmt.Errorf("设置 %s 驱动代理权限失败:%w", displayName, chmodErr)
}
hash, hashErr := hashFileSHA256(executablePath)
if hashErr != nil {
return "", fmt.Errorf("计算 %s 驱动代理摘要失败:%w", displayName, hashErr)
}
return hash, nil
}
func optionalDriverBuildTag(driverType string) (string, error) {
switch normalizeDriverType(driverType) {
case "mysql":
return "gonavi_mysql_driver", nil
case "mariadb":
return "gonavi_mariadb_driver", nil
case "diros":
return "gonavi_diros_driver", nil
case "sphinx":
return "gonavi_sphinx_driver", nil
case "sqlserver":
return "gonavi_sqlserver_driver", nil
case "sqlite":
return "gonavi_sqlite_driver", nil
case "duckdb":
return "gonavi_duckdb_driver", nil
case "dameng":
return "gonavi_dameng_driver", nil
case "kingbase":
return "gonavi_kingbase_driver", nil
case "highgo":
return "gonavi_highgo_driver", nil
case "vastbase":
return "gonavi_vastbase_driver", nil
case "mongodb":
return "gonavi_mongodb_driver", nil
case "tdengine":
return "gonavi_tdengine_driver", nil
default:
return "", fmt.Errorf("未配置驱动构建标签:%s", driverType)
}
}
func locateProjectRootForAgentBuild() (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("获取当前目录失败:%w", err)
}
dir := wd
for {
if fileExists(filepath.Join(dir, "go.mod")) && fileExists(filepath.Join(dir, "cmd", "optional-driver-agent", "main.go")) {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return "", fmt.Errorf("未找到通用驱动代理源码,无法自动构建;请使用已发布版本")
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func optionalDriverExecutableBaseName(driverType string) string {
name := fmt.Sprintf("%s-driver-agent", normalizeDriverType(driverType))
if stdRuntime.GOOS == "windows" {
return name + ".exe"
}
return name
}
func optionalDriverReleaseAssetName(driverType string) string {
name := fmt.Sprintf("%s-driver-agent-%s-%s", normalizeDriverType(driverType), stdRuntime.GOOS, stdRuntime.GOARCH)
if stdRuntime.GOOS == "windows" {
return name + ".exe"
}
return name
}
func optionalDriverBundlePlatformDir(goos string) string {
switch strings.ToLower(strings.TrimSpace(goos)) {
case "windows":
return "Windows"
case "darwin":
return "MacOS"
case "linux":
return "Linux"
default:
return "Unknown"
}
}
func optionalDriverBundleEntryPath(driverType string) string {
return filepath.ToSlash(filepath.Join(optionalDriverBundlePlatformDir(stdRuntime.GOOS), optionalDriverReleaseAssetName(driverType)))
}
func resolveOptionalDriverBundleDownloadURLs() []string {
candidates := make([]string, 0, 2)
seen := make(map[string]struct{}, 2)
appendURL := func(value string) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return
}
if _, ok := seen[trimmed]; ok {
return
}
seen[trimmed] = struct{}{}
candidates = append(candidates, trimmed)
}
currentVersion := normalizeVersion(getCurrentVersion())
if currentVersion != "" && currentVersion != "0.0.0" {
appendURL(fmt.Sprintf("https://github.com/Syngnat/GoNavi/releases/download/v%s/%s", currentVersion, optionalDriverBundleAssetName))
}
appendURL(fmt.Sprintf("https://github.com/Syngnat/GoNavi/releases/latest/download/%s", optionalDriverBundleAssetName))
return candidates
}
func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL string) []string {
driverType := normalizeDriverType(definition.Type)
candidates := make([]string, 0, 3)
seen := make(map[string]struct{}, 3)
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 parsed, err := url.Parse(strings.TrimSpace(rawURL)); err == nil {
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
case "http", "https":
appendURL(parsed.String())
}
}
assetName := optionalDriverReleaseAssetName(driverType)
currentVersion := normalizeVersion(getCurrentVersion())
if currentVersion != "" && currentVersion != "0.0.0" {
appendURL(fmt.Sprintf("https://github.com/Syngnat/GoNavi/releases/download/v%s/%s", currentVersion, assetName))
}
appendURL(fmt.Sprintf("https://github.com/Syngnat/GoNavi/releases/latest/download/%s", assetName))
return candidates
}
func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targetPath string) (string, bool) {
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() {
return absPath, true
}
}
return "", false
}
func resolveOptionalDriverAgentCandidatePaths(definition driverDefinition) []string {
driverType := normalizeDriverType(definition.Type)
name := optionalDriverExecutableBaseName(driverType)
assetName := optionalDriverReleaseAssetName(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)
appendPath(filepath.Join(exeDir, name))
appendPath(filepath.Join(exeDir, assetName))
appendPath(filepath.Join(exeDir, "drivers", driverType, name))
appendPath(filepath.Join(exeDir, "drivers", driverType, assetName))
resourcesDir := filepath.Clean(filepath.Join(exeDir, "..", "Resources"))
appendPath(filepath.Join(resourcesDir, "drivers", driverType, name))
appendPath(filepath.Join(resourcesDir, "drivers", driverType, assetName))
}
if wd, err := os.Getwd(); err == nil && strings.TrimSpace(wd) != "" {
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 "未知"
}
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 := os.Rename(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 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
}
currentVersion := normalizeVersion(getCurrentVersion())
tag := ""
if currentVersion != "" && currentVersion != "0.0.0" {
tag = "v" + currentVersion
}
fillFromSizes := func(sizeByAsset map[string]int64, driverTypes []string) []string {
missing := make([]string, 0, len(driverTypes))
for _, driverType := range driverTypes {
assetName := optionalDriverReleaseAssetName(driverType)
sizeBytes := sizeByAsset[assetName]
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, error) {
key := strings.TrimSpace(cacheKey)
if key == "" {
return nil, fmt.Errorf("缓存 key 为空")
}
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, errors.New(strings.TrimSpace(cached.Err))
}
return cached.SizeByKey, nil
}
}
release, err := fetch()
entry := driverReleaseAssetSizeCacheEntry{
LoadedAt: time.Now(),
SizeByKey: map[string]int64{},
}
if err != nil {
entry.Err = err.Error()
} else {
entry.SizeByKey = buildReleaseAssetSizeMap(release)
if indexSizes, indexErr := fetchDriverBundleAssetSizeIndex(release); indexErr == nil {
for name, size := range indexSizes {
trimmedName := strings.TrimSpace(name)
if trimmedName == "" || size <= 0 {
continue
}
entry.SizeByKey[trimmedName] = size
}
}
}
driverReleaseSizeMu.Lock()
driverReleaseSizeMap[key] = entry
driverReleaseSizeMu.Unlock()
if err != nil {
return nil, err
}
return entry.SizeByKey, nil
}
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 fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64, error) {
if release == nil {
return nil, fmt.Errorf("release 为空")
}
indexURL := ""
for _, asset := range release.Assets {
if strings.EqualFold(strings.TrimSpace(asset.Name), optionalDriverBundleIndexAssetName) {
indexURL = strings.TrimSpace(asset.BrowserDownloadURL)
break
}
}
if indexURL == "" {
return nil, fmt.Errorf("未找到驱动总包索引资产")
}
client := &http.Client{Timeout: driverReleaseAssetSizeProbeTimeout}
req, err := http.NewRequest(http.MethodGet, indexURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "GoNavi-DriverManager")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("拉取驱动总包索引失败HTTP %d", resp.StatusCode)
}
limited := io.LimitReader(resp.Body, driverBundleIndexMaxSize)
decoder := json.NewDecoder(limited)
var index driverBundleAssetIndex
if err := decoder.Decode(&index); err != nil {
return nil, fmt.Errorf("解析驱动总包索引失败:%w", err)
}
if len(index.Assets) == 0 {
return nil, fmt.Errorf("驱动总包索引为空")
}
return index.Assets, nil
}
func fetchLatestReleaseForDriverAssets() (*githubRelease, error) {
return fetchDriverReleaseByURL(updateAPIURL)
}
func fetchReleaseByTag(tag string) (*githubRelease, error) {
tagName := strings.TrimSpace(tag)
if tagName == "" {
return nil, fmt.Errorf("Tag 为空")
}
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/releases/tags/%s", updateRepo, url.PathEscape(tagName))
return fetchDriverReleaseByURL(apiURL)
}
func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) {
urlText := strings.TrimSpace(apiURL)
if urlText == "" {
return nil, fmt.Errorf("API 地址为空")
}
client := &http.Client{Timeout: 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, fmt.Errorf("拉取 Release 信息失败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) string {
if definition.BuiltIn {
return "内置"
}
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 "待发布"
}
return "-"
}
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)
}