mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-12 11:49:40 +08:00
1878 lines
61 KiB
Go
1878 lines
61 KiB
Go
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)
|
||
}
|