🐛 fix(driver-manager): 修复驱动安装交互与 DuckDB Windows 发布链路

- 修复单驱动安装期间右侧目录操作被错误禁用的问题
- 调整 DuckDB Windows 优先下载专属 zip 并兼容带 query 的签名链接
- 补齐本地构建与 CI 发布的 duckdb-driver.zip 产物及回归测试
This commit is contained in:
Syngnat
2026-06-05 07:15:16 +08:00
parent a718c41d5d
commit 2438899ff5
8 changed files with 514 additions and 29 deletions

View File

@@ -434,6 +434,28 @@ jobs:
./cmd/optional-driver-agent
fi
bash ./tools/compress-driver-artifact.sh "${OUTPUT_PATH}" "$TARGET_PLATFORM" "${{ matrix.os_name }}/${OUTPUT}"
if [ "$DRIVER" = "duckdb" ] && [ -n "$DUCKDB_LIB_DIR" ]; then
DUCKDB_ZIP_PATH="${OUTDIR}/duckdb-driver.zip" \
DUCKDB_AGENT_PATH="${OUTPUT_PATH}" \
DUCKDB_DLL_PATH="${OUTDIR}/duckdb.dll" \
python3 - <<'PY'
import os
import zipfile
zip_path = os.environ["DUCKDB_ZIP_PATH"]
entries = [
("Windows/duckdb-driver-agent-windows-amd64.exe", os.environ["DUCKDB_AGENT_PATH"]),
("Windows/duckdb.dll", os.environ["DUCKDB_DLL_PATH"]),
]
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for arcname, src in entries:
if not os.path.isfile(src):
raise FileNotFoundError(src)
zf.write(src, arcname)
PY
echo "📦 已生成 DuckDB Windows 专属驱动包: ${DUCKDB_ZIP_PATH}"
fi
done
bash ./tools/verify-driver-agent-revisions.sh \

View File

@@ -426,6 +426,28 @@ jobs:
./cmd/optional-driver-agent
fi
bash ./tools/compress-driver-artifact.sh "${OUTPUT_PATH}" "$TARGET_PLATFORM" "${{ matrix.os_name }}/${OUTPUT}"
if [ "$DRIVER" = "duckdb" ] && [ -n "$DUCKDB_LIB_DIR" ]; then
DUCKDB_ZIP_PATH="${OUTDIR}/duckdb-driver.zip" \
DUCKDB_AGENT_PATH="${OUTPUT_PATH}" \
DUCKDB_DLL_PATH="${OUTDIR}/duckdb.dll" \
python3 - <<'PY'
import os
import zipfile
zip_path = os.environ["DUCKDB_ZIP_PATH"]
entries = [
("Windows/duckdb-driver-agent-windows-amd64.exe", os.environ["DUCKDB_AGENT_PATH"]),
("Windows/duckdb.dll", os.environ["DUCKDB_DLL_PATH"]),
]
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for arcname, src in entries:
if not os.path.isfile(src):
raise FileNotFoundError(src)
zf.write(src, arcname)
PY
echo "📦 已生成 DuckDB Windows 专属驱动包: ${DUCKDB_ZIP_PATH}"
fi
done
bash ./tools/verify-driver-agent-revisions.sh \
@@ -707,6 +729,7 @@ jobs:
REQUIRED_FILES+=("drivers/Windows/${driver}-driver-agent-windows-arm64.exe")
else
REQUIRED_FILES+=("drivers/Windows/duckdb.dll")
REQUIRED_FILES+=("drivers/Windows/duckdb-driver.zip")
fi
for file in "${REQUIRED_FILES[@]}"; do

View File

@@ -145,6 +145,40 @@ PY
fi
}
zip_duckdb_windows_package() {
local bundle_stage_dir="$1"
local zip_path="$2"
rm -f "$zip_path"
if command -v python3 >/dev/null 2>&1; then
BUNDLE_STAGE_DIR="$bundle_stage_dir" BUNDLE_ZIP_PATH="$zip_path" python3 - <<'PY'
import os
import zipfile
stage_dir = os.environ["BUNDLE_STAGE_DIR"]
zip_path = os.environ["BUNDLE_ZIP_PATH"]
entries = [
("Windows/duckdb-driver-agent-windows-amd64.exe", os.path.join(stage_dir, "Windows", "duckdb-driver-agent-windows-amd64.exe")),
("Windows/duckdb.dll", os.path.join(stage_dir, "Windows", "duckdb.dll")),
]
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for arcname, src in entries:
if not os.path.isfile(src):
raise FileNotFoundError(src)
zf.write(src, arcname)
PY
elif command -v zip >/dev/null 2>&1; then
(
cd "$bundle_stage_dir"
zip -qry "$zip_path" "Windows/duckdb-driver-agent-windows-amd64.exe" "Windows/duckdb.dll"
)
else
echo "❌ 未找到 python3 或 zip无法生成 DuckDB Windows 专属驱动包。"
exit 1
fi
}
prepare_duckdb_windows_library() {
local cache_root="$1"
local lib_dir="$cache_root/duckdb-windows-${DUCKDB_WINDOWS_LIBRARY_VERSION}"
@@ -202,6 +236,7 @@ driver_csv=""
target_platform=""
out_root="dist/driver-agents"
bundle_name="GoNavi-DriverAgents.zip"
duckdb_windows_zip_name="duckdb-driver.zip"
strict_mode="false"
upx_mode="${GONAVI_DRIVER_AGENT_UPX:-auto}"
@@ -403,6 +438,14 @@ fi
zip_bundle "$bundle_zip_path" "$bundle_stage_dir"
duckdb_asset_path="$out_root_abs/windows-amd64/duckdb-driver-agent-windows-amd64.exe"
duckdb_dll_path="$out_root_abs/windows-amd64/$DUCKDB_WINDOWS_SUPPORT_DLL"
if [[ -f "$duckdb_asset_path" && -f "$duckdb_dll_path" ]]; then
duckdb_zip_path="$out_root_abs/windows-amd64/$duckdb_windows_zip_name"
zip_duckdb_windows_package "$bundle_stage_dir" "$duckdb_zip_path"
built_assets+=("Windows/$duckdb_windows_zip_name")
fi
echo ""
echo "✅ 构建完成"
echo " 单文件输出根目录:$out_root_abs"

View File

@@ -1 +1 @@
0295a42fd931778d85157816d79d29e5
d0464f9da25e9356e61652e638c99ffe

View File

@@ -0,0 +1,204 @@
import React from 'react';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import DriverManagerModal from './DriverManagerModal';
const storeState = vi.hoisted(() => ({
theme: 'light',
appearance: {
enabled: true,
opacity: 1,
blur: 0,
uiVersion: 'legacy',
},
}));
const backendApp = vi.hoisted(() => ({
CheckDriverNetworkStatus: vi.fn(),
DownloadDriverPackage: vi.fn(),
GetDriverVersionList: vi.fn(),
GetDriverVersionPackageSize: vi.fn(),
GetDriverStatusList: vi.fn(),
InstallLocalDriverPackage: vi.fn(),
OpenDriverDownloadDirectory: vi.fn(),
RemoveDriverPackage: vi.fn(),
SelectDriverPackageDirectory: vi.fn(),
SelectDriverPackageFile: vi.fn(),
}));
const runtimeApi = vi.hoisted(() => ({
EventsOn: vi.fn(() => vi.fn()),
}));
const messageApi = vi.hoisted(() => ({
error: vi.fn(),
success: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
}));
vi.mock('../store', () => ({
useStore: (selector: (state: typeof storeState) => any) => selector(storeState),
}));
vi.mock('../../wailsjs/go/app/App', () => backendApp);
vi.mock('../../wailsjs/runtime/runtime', () => runtimeApi);
vi.mock('@ant-design/icons', () => {
const Icon = () => <span />;
return {
DeleteOutlined: Icon,
DownloadOutlined: Icon,
FileSearchOutlined: Icon,
FolderOpenOutlined: Icon,
InfoCircleFilled: Icon,
ReloadOutlined: Icon,
};
});
vi.mock('antd', () => {
const Button: any = ({ children, disabled, loading, onClick, ...rest }: any) => (
<button type="button" disabled={disabled || loading} onClick={onClick} {...rest}>
{children}
</button>
);
const Input: any = ({ value, onChange, placeholder }: any) => (
<input value={value} onChange={onChange} placeholder={placeholder} />
);
Input.Search = ({ value, onChange, placeholder }: any) => (
<input value={value} onChange={onChange} placeholder={placeholder} />
);
const Select = () => null;
const Progress = () => <div data-progress="true" />;
const Tag = ({ children }: any) => <span>{children}</span>;
const Switch = ({ checked, onChange, disabled }: any) => (
<button type="button" disabled={disabled} data-switch-checked={String(checked)} onClick={() => onChange?.(!checked)}>
switch
</button>
);
const Space = ({ children }: any) => <div>{children}</div>;
const Text = ({ children }: any) => <span>{children}</span>;
const Paragraph = ({ children }: any) => <div>{children}</div>;
const Typography = { Paragraph, Text };
const Alert = ({ children, message, description }: any) => <div>{children}{message}{description}</div>;
const Empty: any = ({ description }: any) => <div>{description}</div>;
Empty.PRESENTED_IMAGE_SIMPLE = null;
const Collapse = ({ items }: any) => (
<div>{items?.map((item: any) => <div key={item.key}>{item.label}{item.children}</div>)}</div>
);
const Modal: any = ({ children, open, footer, title }: any) => (open ? (
<section data-modal-title={title}>
{children}
<div>{footer}</div>
</section>
) : null);
Modal.confirm = vi.fn();
return {
Alert,
Button,
Collapse,
Empty,
Input,
Modal,
Progress,
Select,
Space,
Switch,
Tag,
Typography,
message: messageApi,
};
});
const flushPromises = async () => {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
};
const textContent = (node: any): string =>
(node.children || [])
.map((item: any) => (typeof item === 'string' ? item : textContent(item)))
.join('');
const findButton = (renderer: ReactTestRenderer, text: string) =>
renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text))[0];
describe('DriverManagerModal toolbar actions', () => {
beforeEach(() => {
vi.clearAllMocks();
backendApp.GetDriverStatusList.mockResolvedValue({
success: true,
data: {
downloadDir: 'D:/drivers',
drivers: [
{
type: 'duckdb',
name: 'DuckDB',
builtIn: false,
pinnedVersion: '2.5.6',
runtimeAvailable: false,
packageInstalled: false,
connectable: false,
defaultDownloadUrl: 'builtin://activate/duckdb',
message: '未启用',
},
],
},
});
backendApp.CheckDriverNetworkStatus.mockResolvedValue({
success: true,
data: {
reachable: true,
summary: 'ok',
recommendedProxy: false,
proxyConfigured: false,
checks: [],
},
});
backendApp.GetDriverVersionList.mockResolvedValue({
success: true,
data: {
versions: [{ version: '2.5.6', downloadUrl: 'builtin://activate/duckdb', recommended: true }],
},
});
backendApp.DownloadDriverPackage.mockImplementation(() => new Promise(() => {}));
backendApp.OpenDriverDownloadDirectory.mockResolvedValue({ success: true });
backendApp.SelectDriverPackageDirectory.mockResolvedValue({ success: true, data: { path: 'D:/drivers/import' } });
});
it('keeps directory tools enabled while a single driver install is running', async () => {
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<DriverManagerModal open onClose={vi.fn()} />);
});
await flushPromises();
const installButton = findButton(renderer!, '安装启用');
const openDirButtonBefore = findButton(renderer!, '打开驱动目录');
const importDirButtonBefore = findButton(renderer!, '导入驱动目录');
const installAllButtonBefore = findButton(renderer!, '安装所有驱动');
expect(openDirButtonBefore.props.disabled).toBeFalsy();
expect(importDirButtonBefore.props.disabled).toBeFalsy();
expect(installAllButtonBefore.props.disabled).toBeFalsy();
await act(async () => {
installButton.props.onClick();
await Promise.resolve();
});
const openDirButtonAfter = findButton(renderer!, '打开驱动目录');
const importDirButtonAfter = findButton(renderer!, '导入驱动目录');
const installAllButtonAfter = findButton(renderer!, '安装所有驱动');
expect(openDirButtonAfter.props.disabled).toBeFalsy();
expect(importDirButtonAfter.props.disabled).toBeFalsy();
expect(installAllButtonAfter.props.disabled).toBe(true);
});
});

View File

@@ -234,7 +234,8 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
const [versionSizeLoadingMap, setVersionSizeLoadingMap] = useState<Record<string, boolean>>({});
const downloadDirRef = useRef(downloadDir);
const progressMapRef = useRef<Record<string, DriverProgressState>>({});
const batchBusy = batchDirectoryImporting || batchAction !== '' || actionState.kind !== '';
const batchBusy = batchDirectoryImporting || batchAction !== '';
const installMutatingBusy = batchBusy || actionState.kind !== '';
useEffect(() => {
downloadDirRef.current = downloadDir;
@@ -1436,7 +1437,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
</Button>
<Button key="close" type="primary" onClick={onClose}>
{batchBusy ? '后台运行' : '关闭'}
{installMutatingBusy ? '后台运行' : '关闭'}
</Button>
</Space>
)}
@@ -1584,12 +1585,12 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
<Switch
checked={forceOverwriteInstalled}
onChange={(checked) => setForceOverwriteInstalled(checked)}
disabled={batchBusy}
disabled={batchDirectoryImporting}
/>
<Button
type="primary"
icon={<DownloadOutlined />}
disabled={batchBusy || installableRows.length === 0}
disabled={installMutatingBusy || installableRows.length === 0}
loading={batchAction === 'install-all'}
onClick={() => void installAllDrivers()}
>
@@ -1598,7 +1599,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
<Button
type="primary"
icon={<DownloadOutlined />}
disabled={batchBusy || reinstallableRows.length === 0}
disabled={installMutatingBusy || reinstallableRows.length === 0}
loading={batchAction === 'reinstall-updates'}
onClick={() => void reinstallNeededDrivers()}
>
@@ -1607,7 +1608,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
<Button
danger
icon={<DeleteOutlined />}
disabled={batchBusy || removableRows.length === 0}
disabled={installMutatingBusy || removableRows.length === 0}
loading={batchAction === 'remove-all'}
onClick={() => void removeAllDrivers()}
>
@@ -1615,7 +1616,6 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
</Button>
<Button
icon={<FolderOpenOutlined />}
disabled={batchBusy}
onClick={() => void openDriverDirectory()}
>
@@ -1623,7 +1623,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
<Button
icon={<FolderOpenOutlined />}
loading={batchDirectoryImporting}
disabled={batchBusy && !batchDirectoryImporting}
disabled={batchDirectoryImporting}
onClick={() => void installDriversFromDirectory()}
>

View File

@@ -15,6 +15,7 @@ import (
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
stdRuntime "runtime"
@@ -298,6 +299,7 @@ const (
driverReleaseLatestAPIURL = "https://api.github.com/repos/" + driverReleaseRepo + "/releases/latest"
driverReleaseDevTag = "dev-latest"
optionalDriverBundleAssetName = "GoNavi-DriverAgents.zip"
duckDBWindowsDriverZipAssetName = "duckdb-driver.zip"
optionalDriverBundleIndexAssetName = "GoNavi-DriverAgents-Index.json"
optionalDriverBundleDownloadTimeout = 45 * time.Minute
optionalDriverBundleCacheMaxAge = 7 * 24 * time.Hour
@@ -371,6 +373,8 @@ var (
errLocalDriverDirScanLimit = errors.New("local_driver_directory_scan_limit_exceeded")
)
var validateOptionalDriverAgentExecutableFunc = db.ValidateOptionalDriverAgentExecutable
type driverVersionWarmupState struct {
Running bool
LastStarted time.Time
@@ -1914,6 +1918,27 @@ func resolvePublishedDriverDownloadURLForTag(definition driverDefinition, select
}
func resolvePublishedDriverReleaseAssetName(driverType string, version string, tag string) (string, bool) {
if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
cacheKey := "tag:" + strings.TrimSpace(tag)
if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache(cacheKey); ok {
if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
return duckDBWindowsDriverZipAssetName, true
}
return "", false
}
sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached(cacheKey, func() (*githubRelease, error) {
return fetchReleaseByTag(tag)
})
if err != nil {
return "", false
}
if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
return duckDBWindowsDriverZipAssetName, true
}
return "", false
}
assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, version)
if len(assetNames) == 0 {
return "", false
@@ -3006,7 +3031,7 @@ func installOptionalDriverAgentFromLocalPath(definition driverDefinition, filePa
return installedDriverPackage{}, fmt.Errorf("导入本地驱动代理运行时依赖失败:%w", supportErr)
}
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
return installedDriverPackage{}, validateErr
}
@@ -3355,7 +3380,7 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
info, err := os.Stat(executablePath)
if err == nil && !info.IsDir() {
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
_ = os.Remove(executablePath)
} else {
// 用户点击“安装/重装”时应强制刷新驱动代理,避免沿用旧二进制导致修复不生效。
@@ -3389,7 +3414,7 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut
if copyErr := copyAgentBinary(sourcePath, executablePath); copyErr != nil {
return "", "", fmt.Errorf("复制预置 %s 驱动代理失败:%w", displayName, copyErr)
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
_ = os.Remove(executablePath)
return "", "", validateErr
}
@@ -3540,6 +3565,17 @@ func shouldUseOptionalDriverBundleFallback(driverType string, restrictToExplicit
return directURLCount == 0
}
func isOptionalDriverDownloadZipURL(urlText string) bool {
trimmedURL := strings.TrimSpace(urlText)
if trimmedURL == "" {
return false
}
if parsed, err := url.Parse(trimmedURL); err == nil && strings.TrimSpace(parsed.Path) != "" {
return strings.EqualFold(path.Ext(parsed.Path), ".zip")
}
return strings.EqualFold(filepath.Ext(trimmedURL), ".zip")
}
func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlText string, executablePath string) (string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
@@ -3547,8 +3583,46 @@ func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlT
if trimmedURL == "" {
return "", fmt.Errorf("下载地址为空")
}
if isOptionalDriverDownloadZipURL(trimmedURL) {
tempPath := executablePath + ".download.zip"
_ = os.Remove(tempPath)
if _, err := downloadFileWithHash(trimmedURL, tempPath, func(downloaded, total int64) {
if a == nil {
return
}
scaledDownloaded, scaledTotal := scaleProgress(downloaded, total, 20, 90)
a.emitDriverDownloadProgress(driverType, "downloading", scaledDownloaded, scaledTotal, fmt.Sprintf("下载预编译 %s 驱动包", displayName))
}); err != nil {
_ = os.Remove(tempPath)
return "", fmt.Errorf("下载失败:%w", err)
}
if _, err := installOptionalDriverAgentFromLocalZip(tempPath, definition, executablePath, ""); err != nil {
_ = os.Remove(tempPath)
_ = os.Remove(executablePath)
for _, supportName := range optionalDriverSupportFileNames(driverType) {
_ = os.Remove(filepath.Join(filepath.Dir(executablePath), supportName))
}
return "", fmt.Errorf("安装预编译驱动包失败:%w", err)
}
_ = os.Remove(tempPath)
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
_ = os.Remove(executablePath)
for _, supportName := range optionalDriverSupportFileNames(driverType) {
_ = os.Remove(filepath.Join(filepath.Dir(executablePath), supportName))
}
return "", validateErr
}
hash, hashErr := hashFileSHA256(executablePath)
if hashErr != nil {
return "", fmt.Errorf("计算驱动代理摘要失败:%w", hashErr)
}
return hash, nil
}
if len(optionalDriverSupportFileNames(driverType)) > 0 {
return "", fmt.Errorf("%s 当前平台需要随包提供运行时依赖(%s不能安装单文件代理请使用驱动总包或本地源码构建", displayName, strings.Join(optionalDriverSupportFileNames(driverType), ", "))
return "", fmt.Errorf("%s 当前平台需要随包提供运行时依赖(%s不能安装单文件代理请使用驱动总包、驱动专属 zip 或本地源码构建", displayName, strings.Join(optionalDriverSupportFileNames(driverType), ", "))
}
tempPath := executablePath + ".tmp"
_ = os.Remove(tempPath)
@@ -3576,7 +3650,7 @@ func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlT
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", fmt.Errorf("设置代理权限失败:%w", chmodErr)
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
_ = os.Remove(executablePath)
return "", validateErr
}
@@ -3693,7 +3767,7 @@ func downloadOptionalDriverAgentFromBundle(a *App, definition driverDefinition,
_ = os.Remove(executablePath)
return "", "", supportErr
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil {
_ = os.Remove(executablePath)
return "", "", validateErr
}
@@ -3940,6 +4014,10 @@ func shouldUseDuckDBWindowsDynamicLibrary(driverType string) bool {
return normalizeDriverType(driverType) == "duckdb" && stdRuntime.GOOS == "windows" && stdRuntime.GOARCH == "amd64"
}
func shouldPreferPublishedOptionalDriverDownloads(driverType string) bool {
return shouldUseDuckDBWindowsDynamicLibrary(driverType)
}
func shouldSkipDirectOptionalDriverDownloads(driverType string) bool {
return shouldUseDuckDBWindowsDynamicLibrary(driverType)
}
@@ -4698,6 +4776,7 @@ func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloade
func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL string, selectedVersion string) []string {
candidates := make([]string, 0, 3)
seen := make(map[string]struct{}, 3)
driverType := normalizeDriverType(definition.Type)
appendURL := func(value string) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
@@ -4710,8 +4789,20 @@ func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL
candidates = append(candidates, trimmed)
}
if shouldSkipDirectOptionalDriverDownloads(definition.Type) {
return candidates
restrictToExplicitArtifact := shouldRestrictToExplicitVersionArtifact(definition, selectedVersion)
appendPublishedURLs := func() {
if tag := currentDriverReleaseTag(); tag != "" {
if publishedURL, ok := resolvePublishedDriverDownloadURLForTag(definition, selectedVersion, tag); ok {
appendURL(publishedURL)
}
}
if publishedURL, ok := resolveLatestPublishedDriverDownloadURL(definition); ok {
appendURL(publishedURL)
}
}
if !restrictToExplicitArtifact && shouldPreferPublishedOptionalDriverDownloads(driverType) {
appendPublishedURLs()
}
if parsed, err := url.Parse(strings.TrimSpace(rawURL)); err == nil {
@@ -4720,17 +4811,12 @@ func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL
appendURL(parsed.String())
}
}
if shouldRestrictToExplicitVersionArtifact(definition, selectedVersion) {
if restrictToExplicitArtifact {
return candidates
}
if tag := currentDriverReleaseTag(); tag != "" {
if publishedURL, ok := resolvePublishedDriverDownloadURLForTag(definition, selectedVersion, tag); ok {
appendURL(publishedURL)
}
}
if publishedURL, ok := resolveLatestPublishedDriverDownloadURL(definition); ok {
appendURL(publishedURL)
if !shouldPreferPublishedOptionalDriverDownloads(driverType) {
appendPublishedURLs()
}
return candidates
}
@@ -4755,7 +4841,7 @@ func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targe
if statErr != nil || info.IsDir() {
continue
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, absPath); validateErr != nil {
if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, absPath); validateErr != nil {
continue
}
if !isReusableOptionalDriverAgentRevisionCurrent(driverType, absPath) {
@@ -5342,6 +5428,24 @@ func resolveLatestPublishedDriverDownloadURL(definition driverDefinition) (strin
if driverType == "" {
return "", false
}
if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache("latest"); ok {
if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
return driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName), true
}
return "", false
}
sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets)
if err != nil {
return "", false
}
if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 {
return driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName), true
}
return "", false
}
assetNames := optionalDriverReleaseAssetNames(driverType)
if len(assetNames) == 0 {
return "", false

View File

@@ -380,9 +380,21 @@ func TestDuckDBWindowsBuildUsesDynamicLibraryTag(t *testing.T) {
if !shouldSkipReusableAgentCandidate("duckdb", "") {
t.Fatal("expected DuckDB Windows install to skip reusable static agent candidates")
}
urls := resolveOptionalDriverAgentDownloadURLs(driverDefinition{Type: "duckdb"}, "https://example.com/duckdb-driver-agent-windows-amd64.exe", "")
if len(urls) != 0 {
t.Fatalf("expected DuckDB Windows install to skip single-file direct downloads, got %v", urls)
seedReleaseAssetCacheEntry(t, "latest", map[string]int64{
duckDBWindowsDriverZipAssetName: 19 << 20,
}, map[string]int64{
duckDBWindowsDriverZipAssetName: 19 << 20,
})
legacyDirectURL := "https://example.com/duckdb-driver-agent-windows-amd64.exe"
urls := resolveOptionalDriverAgentDownloadURLs(driverDefinition{Type: "duckdb"}, legacyDirectURL, "")
if len(urls) < 2 {
t.Fatalf("expected DuckDB Windows install to keep dedicated zip ahead of legacy direct candidate, got %v", urls)
}
if urls[0] != driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName) {
t.Fatalf("expected DuckDB Windows dedicated zip candidate first, got %v", urls)
}
if urls[1] != legacyDirectURL {
t.Fatalf("expected DuckDB Windows to keep legacy direct candidate after dedicated zip, got %v", urls)
}
}
@@ -437,6 +449,83 @@ func TestInstallOptionalDriverAgentFromLocalZipExtractsDuckDBDLL(t *testing.T) {
}
}
func TestDownloadOptionalDriverAgentBinaryInstallsDuckDBDedicatedZip(t *testing.T) {
if runtime.GOOS != "windows" || runtime.GOARCH != "amd64" {
t.Skip("DuckDB dedicated zip flow only applies on windows/amd64")
}
originalValidateFunc := validateOptionalDriverAgentExecutableFunc
validateOptionalDriverAgentExecutableFunc = func(driverType string, executablePath string) error {
return nil
}
t.Cleanup(func() {
validateOptionalDriverAgentExecutableFunc = originalValidateFunc
})
tmpDir := t.TempDir()
zipPath := filepath.Join(tmpDir, duckDBWindowsDriverZipAssetName)
zipFile, err := os.Create(zipPath)
if err != nil {
t.Fatalf("create zip failed: %v", err)
}
zw := zip.NewWriter(zipFile)
for name, content := range map[string]string{
"Windows/duckdb-driver-agent-windows-amd64.exe": "agent",
"Windows/duckdb.dll": "dll",
} {
w, err := zw.Create(name)
if err != nil {
t.Fatalf("create zip entry %s failed: %v", name, err)
}
if _, err := w.Write([]byte(content)); err != nil {
t.Fatalf("write zip entry %s failed: %v", name, err)
}
}
if err := zw.Close(); err != nil {
t.Fatalf("close zip writer failed: %v", err)
}
if err := zipFile.Close(); err != nil {
t.Fatalf("close zip file failed: %v", err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, zipPath)
}))
defer server.Close()
target := filepath.Join(tmpDir, "install", "duckdb-driver-agent.exe")
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
t.Fatalf("create install dir failed: %v", err)
}
hash, err := downloadOptionalDriverAgentBinary(nil, driverDefinition{Type: "duckdb", Name: "DuckDB"}, server.URL+"/"+duckDBWindowsDriverZipAssetName+"?source=release", target)
if err != nil {
t.Fatalf("download dedicated zip failed: %v", err)
}
if strings.TrimSpace(hash) == "" {
t.Fatal("expected hash for installed duckdb agent")
}
if _, err := os.Stat(target); err != nil {
t.Fatalf("expected duckdb agent to be installed: %v", err)
}
dllBytes, err := os.ReadFile(filepath.Join(filepath.Dir(target), "duckdb.dll"))
if err != nil {
t.Fatalf("expected duckdb.dll to be installed: %v", err)
}
if string(dllBytes) != "dll" {
t.Fatalf("unexpected duckdb.dll content: %q", string(dllBytes))
}
}
func TestOptionalDriverDownloadZipURLAcceptsQueryString(t *testing.T) {
if !isOptionalDriverDownloadZipURL("https://example.com/duckdb-driver.zip?token=abc") {
t.Fatal("expected signed zip URL to be treated as zip download")
}
if isOptionalDriverDownloadZipURL("https://example.com/duckdb-driver-agent.exe?token=abc") {
t.Fatal("expected exe URL with query to remain non-zip download")
}
}
func TestDownloadDriverPackageRejectsUnsupportedMongoVersion(t *testing.T) {
app := &App{}