mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 10:29:52 +08:00
🐛 fix(driver-manager): 修复驱动安装交互与 DuckDB Windows 发布链路
- 修复单驱动安装期间右侧目录操作被错误禁用的问题 - 调整 DuckDB Windows 优先下载专属 zip 并兼容带 query 的签名链接 - 补齐本地构建与 CI 发布的 duckdb-driver.zip 产物及回归测试
This commit is contained in:
22
.github/workflows/dev-build.yml
vendored
22
.github/workflows/dev-build.yml
vendored
@@ -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 \
|
||||
|
||||
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1 +1 @@
|
||||
0295a42fd931778d85157816d79d29e5
|
||||
d0464f9da25e9356e61652e638c99ffe
|
||||
204
frontend/src/components/DriverManagerModal.test.tsx
Normal file
204
frontend/src/components/DriverManagerModal.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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()}
|
||||
>
|
||||
导入驱动目录
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user