fix(jdbc): include managed drivers in offline bundles

This commit is contained in:
zipg
2026-08-15 00:27:34 +08:00
committed by GitHub
parent 86bb88ab66
commit 8737114d28
14 changed files with 942 additions and 15 deletions
@@ -0,0 +1,111 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const BUILDER = join(REPO_ROOT, "agents/scripts/build_offline_jdbc_payload.mjs");
const ZIP_BUILDER = join(REPO_ROOT, "agents/scripts/build_offline_zip.sh");
const RELEASE_VERIFIER = join(REPO_ROOT, "agents/scripts/verify_offline_jdbc_release.mjs");
const ASSET_MANIFEST = join(REPO_ROOT, "apps/desktop/src/lib/database/managedJdbcAssets.json");
const PLATFORMS = ["macos-aarch64", "macos-x64", "linux-x64", "linux-aarch64", "windows-x64", "windows-aarch64"];
function flattenBundles(manifest) {
return Object.values(manifest.drivers).flatMap((driver) => Object.values(driver.bundles || {}));
}
function run(command, args, options = {}) {
const result = spawnSync(command, args, { encoding: "utf8", ...options });
assert.equal(result.status, 0, result.stderr || result.stdout);
return result;
}
test("builds and verifies all platform ZIPs from the managed JDBC asset manifest", () => {
const root = mkdtempSync(join(tmpdir(), "dbx-offline-jdbc-builder-test-"));
try {
const releaseDir = join(root, "release");
const pluginZip = join(root, "plugin.zip");
const jarRoot = join(root, "jar");
const artifact = join(root, "driver.jar");
const resolver = join(root, "resolver.mjs");
const testManifestPath = join(root, "managed-jdbc-assets.json");
mkdirSync(releaseDir, { recursive: true });
mkdirSync(join(jarRoot, "META-INF"), { recursive: true });
writeFileSync(pluginZip, "plugin");
writeFileSync(join(jarRoot, "META-INF/LICENSE"), "Apache License 2.0\n");
writeFileSync(join(jarRoot, "META-INF/NOTICE"), "Test notice\n");
run("zip", ["-qr", artifact, "."], { cwd: jarRoot });
const digest = createHash("sha256").update(readFileSync(artifact)).digest("hex");
const productionManifest = JSON.parse(readFileSync(ASSET_MANIFEST, "utf8"));
const testManifest = structuredClone(productionManifest);
const resolverBundles = {};
for (const bundle of flattenBundles(testManifest)) {
bundle.artifact.file_name = "driver.jar";
bundle.artifact.size = statSync(artifact).size;
bundle.artifact.sha256 = digest;
bundle.redistribution.license_entries = ["META-INF/LICENSE"];
bundle.redistribution.notice_entries = ["META-INF/NOTICE"];
resolverBundles[bundle.coordinate] = {
coordinate: bundle.coordinate,
scope: "runtime",
repositories: [testManifest.repository],
artifacts: [
{
groupId: bundle.artifact.group_id,
artifactId: bundle.artifact.artifact_id,
version: bundle.artifact.version,
classifier: bundle.artifact.classifier,
extension: bundle.artifact.extension,
file: artifact,
size: bundle.artifact.size,
sha256: digest,
},
],
};
}
writeFileSync(testManifestPath, `${JSON.stringify(testManifest, null, 2)}\n`);
writeFileSync(
resolver,
`#!/usr/bin/env node
const bundles = ${JSON.stringify(resolverBundles)};
const coordinate = process.argv[process.argv.indexOf("--coordinate") + 1];
if (!bundles[coordinate]) process.exit(2);
process.stdout.write(JSON.stringify(bundles[coordinate]));
`,
);
chmodSync(resolver, 0o755);
run(process.execPath, [BUILDER, releaseDir, pluginZip, resolver, testManifestPath], {
env: { ...process.env, DBX_OFFLINE_JDBC_MAVEN_CACHE: join(root, "cache") },
});
const payloadDir = join(releaseDir, "offline-jdbc/jdbc");
const offlineManifest = JSON.parse(readFileSync(join(payloadDir, "offline-manifest.json"), "utf8"));
assert.equal(offlineManifest.format_version, 1);
assert.equal(offlineManifest.plugin_entry, "jdbc/plugin.zip");
assert.equal(offlineManifest.third_party_entry, "jdbc/third-party-notices.json");
assert.deepEqual(
offlineManifest.bundles.map((bundle) => bundle.coordinate),
flattenBundles(productionManifest).map((bundle) => bundle.coordinate),
);
for (const bundle of offlineManifest.bundles) {
const bundleManifest = JSON.parse(readFileSync(join(payloadDir, "maven", bundle.id, "manifest.json"), "utf8"));
assert.equal(bundleManifest.coordinate, bundle.coordinate);
assert.equal(bundleManifest.artifacts.length, 1);
assert.equal(bundleManifest.artifacts[0].sha256, digest);
}
writeFileSync(join(releaseDir, "agent-registry.json"), "{}\n");
for (const platform of PLATFORMS) writeFileSync(join(releaseDir, `dbx-jre-21-${platform}.tar.zst`), platform);
run("bash", [ZIP_BUILDER, releaseDir]);
run(process.execPath, [RELEASE_VERIFIER, releaseDir, testManifestPath]);
for (const platform of PLATFORMS) assert.equal(existsSync(join(releaseDir, `dbx-agents-offline-${platform}.zip`)), true);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
+18
View File
@@ -969,6 +969,11 @@ jobs:
with:
token: ${{ steps.release-token.outputs.token }}
fetch-depth: 0
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- uses: gradle/actions/setup-gradle@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
@@ -1172,9 +1177,22 @@ jobs:
echo "=== agent-registry.json ==="
cat release/agent-registry.json
- name: Build managed JDBC offline payload
shell: bash
run: |
./plugins/jdbc/package.sh
JDBC_VERSION="$(sed -nE "s/^version[[:space:]]*=[[:space:]]*'([^']+)'.*/\1/p" plugins/jdbc/build.gradle | head -n 1)"
node agents/scripts/build_offline_jdbc_payload.mjs \
release \
"plugins/jdbc/dist/dbx-jdbc-plugin-${JDBC_VERSION}.zip" \
"plugins/jdbc/dist/dbx-jdbc-plugin-${JDBC_VERSION}/bin/dbx-maven-resolver"
- name: Build offline ZIP bundles
run: bash agents/scripts/build_offline_zip.sh release
- name: Verify managed JDBC assets in all offline ZIPs
run: node agents/scripts/verify_offline_jdbc_release.mjs release
- name: Build single-driver packages
run: |
python3 agents/scripts/build_driver_zips.py release --cleanup-sources
+50
View File
@@ -526,6 +526,46 @@ jobs:
- name: JDBC plugin package check
run: ./plugins/jdbc/package.sh
offline-jdbc-release:
needs: changes
if: needs.changes.outputs.offline_jdbc == 'true'
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"
cache: gradle
- name: Cache approved Maven assets
uses: actions/cache@v4
with:
path: ~/.cache/dbx-offline-jdbc-maven
key: offline-jdbc-${{ hashFiles('apps/desktop/src/lib/database/managedJdbcAssets.json') }}
- name: Build real managed JDBC payload
env:
DBX_OFFLINE_JDBC_MAVEN_CACHE: ~/.cache/dbx-offline-jdbc-maven
run: |
./plugins/jdbc/package.sh
JDBC_VERSION="$(sed -nE "s/^version[[:space:]]*=[[:space:]]*'([^']+)'.*/\1/p" plugins/jdbc/build.gradle | head -n 1)"
node agents/scripts/build_offline_jdbc_payload.mjs \
release \
"plugins/jdbc/dist/dbx-jdbc-plugin-${JDBC_VERSION}.zip" \
"plugins/jdbc/dist/dbx-jdbc-plugin-${JDBC_VERSION}/bin/dbx-maven-resolver"
- name: Build and unpack all six platform ZIPs
run: |
printf '{}\n' > release/agent-registry.json
for platform in macos-aarch64 macos-x64 linux-x64 linux-aarch64 windows-x64 windows-aarch64; do
printf '%s\n' "$platform" > "release/dbx-jre-21-${platform}.tar.zst"
done
bash agents/scripts/build_offline_zip.sh release
node agents/scripts/verify_offline_jdbc_release.mjs release
nix-packaging:
needs: changes
if: needs.changes.outputs.nix == 'true'
@@ -548,6 +588,7 @@ jobs:
rust: ${{ steps.filter.outputs.rust }}
rust_full: ${{ steps.rust-mode.outputs.full }}
jdbc: ${{ steps.filter.outputs.jdbc }}
offline_jdbc: ${{ steps.filter.outputs.offline_jdbc }}
agents: ${{ steps.filter.outputs.agents }}
duckdb_windows: ${{ steps.filter.outputs.duckdb_windows }}
nix: ${{ steps.filter.outputs.nix }}
@@ -595,6 +636,15 @@ jobs:
- '.github/workflows/ci.yml'
jdbc:
- 'plugins/jdbc/**'
offline_jdbc:
- 'plugins/jdbc/**'
- 'agents/scripts/build_offline_jdbc_payload.mjs'
- 'agents/scripts/build_offline_zip.sh'
- 'agents/scripts/verify_offline_jdbc_release.mjs'
- 'apps/desktop/src/lib/database/managedJdbcAssets.json'
- '.github/scripts/offline-jdbc-payload.test.mjs'
- '.github/workflows/agents-release.yml'
- '.github/workflows/ci.yml'
agents:
- 'agents/**'
- 'crates/dbx-core/Cargo.toml'
@@ -0,0 +1,195 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const DEFAULT_ASSET_MANIFEST = join(REPO_ROOT, "apps/desktop/src/lib/database/managedJdbcAssets.json");
function fail(message) {
throw new Error(message);
}
async function sha256(path) {
const hash = createHash("sha256");
for await (const chunk of createReadStream(path)) hash.update(chunk);
return hash.digest("hex");
}
function loadAssetManifest(path) {
const manifest = JSON.parse(readFileSync(path, "utf8"));
if (manifest.format_version !== 1) fail(`Unsupported managed JDBC asset manifest version: ${manifest.format_version}`);
if (!manifest.repository || !manifest.drivers || typeof manifest.drivers !== "object") fail("Invalid managed JDBC asset manifest");
const bundles = Object.values(manifest.drivers).flatMap((driver) => Object.values(driver.bundles || {}));
if (bundles.length === 0) fail("Managed JDBC asset manifest contains no bundles");
const ids = new Set();
const coordinates = new Set();
for (const bundle of bundles) {
if (!bundle.id || !bundle.coordinate || !bundle.artifact || !bundle.redistribution) fail("Managed JDBC bundle is incomplete");
if (ids.has(bundle.id)) fail(`Duplicate managed JDBC bundle id: ${bundle.id}`);
if (coordinates.has(bundle.coordinate)) fail(`Duplicate managed JDBC coordinate: ${bundle.coordinate}`);
ids.add(bundle.id);
coordinates.add(bundle.coordinate);
}
return { manifest, bundles };
}
function resolveBundle(resolver, coordinate, localRepository, repositories) {
const javaToolOptions = [
process.env.JAVA_TOOL_OPTIONS,
"-Djava.net.preferIPv4Stack=true",
"-Daether.connector.connectTimeout=30000",
"-Daether.connector.requestTimeout=300000",
]
.filter(Boolean)
.join(" ");
const args = ["resolve", "--coordinate", coordinate, "--local-repo", localRepository];
for (const repository of repositories) args.push("--repo", repository);
const result = spawnSync(resolver, args, {
encoding: "utf8",
maxBuffer: 128 * 1024 * 1024,
env: { ...process.env, JAVA_TOOL_OPTIONS: javaToolOptions },
});
if (result.error) fail(`Failed to start JDBC Maven resolver for ${coordinate}: ${result.error.message}`);
if (result.status !== 0) fail(`Failed to resolve ${coordinate}: ${(result.stderr || result.stdout).trim()}`);
try {
return JSON.parse(result.stdout);
} catch (error) {
fail(`Failed to parse Maven resolver output for ${coordinate}: ${error.message}`);
}
}
function artifactIdentity(artifact) {
return [artifact.groupId, artifact.artifactId, artifact.version, artifact.classifier || "", artifact.extension].join(":");
}
function expectedArtifactIdentity(artifact) {
return [artifact.group_id, artifact.artifact_id, artifact.version, artifact.classifier || "", artifact.extension].join(":");
}
function jarEntries(path) {
const result = spawnSync("unzip", ["-Z1", path], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
if (result.error) fail(`Failed to inspect JDBC JAR ${path}: ${result.error.message}`);
if (result.status !== 0) fail(`Failed to inspect JDBC JAR ${path}: ${(result.stderr || result.stdout).trim()}`);
return new Set(result.stdout.split(/\r?\n/).filter(Boolean));
}
async function validateAndCopyBundle(bundle, resolved, mavenDir) {
const resolvedArtifacts = (resolved.artifacts || []).filter((artifact) => String(artifact.extension).toLowerCase() === "jar");
const identities = new Set(resolvedArtifacts.map(artifactIdentity));
if (identities.size !== resolvedArtifacts.length) fail(`Maven resolver returned duplicate JAR artifacts for ${bundle.coordinate}`);
if (resolvedArtifacts.length !== 1) {
fail(`Resolved dependency set changed for ${bundle.coordinate}: expected 1 approved shaded JAR, received ${resolvedArtifacts.length}`);
}
const resolvedArtifact = resolvedArtifacts[0];
const approved = bundle.artifact;
if (artifactIdentity(resolvedArtifact) !== expectedArtifactIdentity(approved)) {
fail(`Resolved artifact identity does not match the approved asset for ${bundle.coordinate}`);
}
if (!existsSync(resolvedArtifact.file)) fail(`Resolved artifact does not exist: ${resolvedArtifact.file}`);
if (basename(resolvedArtifact.file) !== approved.file_name) fail(`Resolved artifact file name does not match the approved asset for ${bundle.coordinate}`);
const actualSize = statSync(resolvedArtifact.file).size;
const actualDigest = await sha256(resolvedArtifact.file);
if (Number(resolvedArtifact.size) !== actualSize || String(resolvedArtifact.sha256).toLowerCase() !== actualDigest) {
fail(`Maven resolver metadata mismatch: ${resolvedArtifact.file}`);
}
if (approved.size !== actualSize || approved.sha256.toLowerCase() !== actualDigest) {
fail(`Resolved artifact checksum is not approved for redistribution: ${bundle.coordinate}`);
}
const entries = jarEntries(resolvedArtifact.file);
for (const entry of [...bundle.redistribution.license_entries, ...bundle.redistribution.notice_entries]) {
if (!entries.has(entry)) fail(`Approved license/notice entry is missing from ${bundle.coordinate}: ${entry}`);
}
const bundleDir = join(mavenDir, bundle.id);
const jarsDir = join(bundleDir, "jars");
mkdirSync(jarsDir, { recursive: true });
copyFileSync(resolvedArtifact.file, join(jarsDir, approved.file_name));
const artifact = {
group_id: approved.group_id,
artifact_id: approved.artifact_id,
version: approved.version,
classifier: approved.classifier || "",
extension: approved.extension,
file_name: approved.file_name,
path: "",
size: actualSize,
sha256: actualDigest,
};
const bundleManifest = {
id: bundle.id,
coordinate: bundle.coordinate,
scope: resolved.scope || "runtime",
repositories: resolved.repositories?.length ? resolved.repositories : [],
installed_at: "",
path: "",
artifacts: [artifact],
};
writeFileSync(join(bundleDir, "manifest.json"), `${JSON.stringify(bundleManifest, null, 2)}\n`);
return artifact;
}
async function main() {
const [releaseArg, pluginZipArg, resolverArg, assetManifestArg] = process.argv.slice(2);
if (!releaseArg || !pluginZipArg || !resolverArg) {
fail("Usage: build_offline_jdbc_payload.mjs <release-dir> <jdbc-plugin-zip> <maven-resolver> [asset-manifest]");
}
const releaseDir = resolve(releaseArg);
const pluginZip = resolve(pluginZipArg);
const resolver = resolve(resolverArg);
const assetManifestPath = resolve(assetManifestArg || DEFAULT_ASSET_MANIFEST);
if (!existsSync(pluginZip)) fail(`JDBC plugin ZIP not found: ${pluginZip}`);
if (!existsSync(resolver)) fail(`JDBC Maven resolver not found: ${resolver}`);
if (!existsSync(assetManifestPath)) fail(`Managed JDBC asset manifest not found: ${assetManifestPath}`);
const { manifest: assetManifest, bundles } = loadAssetManifest(assetManifestPath);
const payloadDir = join(releaseDir, "offline-jdbc", "jdbc");
const mavenDir = join(payloadDir, "maven");
rmSync(payloadDir, { recursive: true, force: true });
mkdirSync(mavenDir, { recursive: true });
copyFileSync(pluginZip, join(payloadDir, "plugin.zip"));
const externalCache = process.env.DBX_OFFLINE_JDBC_MAVEN_CACHE?.trim();
const resolverRepository = process.env.DBX_OFFLINE_JDBC_MAVEN_REPOSITORY?.trim() || assetManifest.repository;
const workDir = externalCache ? null : mkdtempSync(join(tmpdir(), "dbx-offline-jdbc-"));
const localRepository = externalCache ? resolve(externalCache) : join(workDir, "maven-cache");
mkdirSync(localRepository, { recursive: true });
const manifestBundles = [];
const notices = [];
try {
for (const bundle of bundles) {
console.log(`Resolving offline JDBC bundle ${bundle.coordinate}`);
const resolved = resolveBundle(resolver, bundle.coordinate, localRepository, [resolverRepository]);
const artifact = await validateAndCopyBundle(bundle, resolved, mavenDir);
manifestBundles.push({ id: bundle.id, coordinate: bundle.coordinate });
notices.push({ id: bundle.id, coordinate: bundle.coordinate, artifact, redistribution: bundle.redistribution });
console.log(`Prepared ${bundle.coordinate} (approved SHA-256 ${artifact.sha256})`);
}
} finally {
if (workDir) rmSync(workDir, { recursive: true, force: true });
}
writeFileSync(
join(payloadDir, "third-party-notices.json"),
`${JSON.stringify({ format_version: 1, bundles: notices }, null, 2)}\n`,
);
writeFileSync(
join(payloadDir, "offline-manifest.json"),
`${JSON.stringify({ format_version: 1, plugin_entry: "jdbc/plugin.zip", third_party_entry: "jdbc/third-party-notices.json", bundles: manifestBundles }, null, 2)}\n`,
);
console.log(`Prepared offline JDBC payload in ${payloadDir}`);
}
main().catch((error) => {
console.error(`ERROR: ${error?.stack || error?.message || String(error)}`);
process.exitCode = 1;
});
+9 -2
View File
@@ -3,7 +3,8 @@ set -euo pipefail
# Build offline ZIP bundles for each platform.
# Usage: ./scripts/build_offline_zip.sh <release-dir>
# The release-dir must contain agent-registry.json, raw driver artifacts, and dbx-jre-*.tar.zst
# The release-dir must contain agent-registry.json, raw driver artifacts, and dbx-jre-*.tar.zst.
# If offline-jdbc/jdbc is present, it is included as the managed JDBC payload.
RELEASE_DIR="$(cd "${1:?Usage: build_offline_zip.sh <release-dir>}" && pwd)"
@@ -30,6 +31,10 @@ for platform in "${PLATFORMS[@]}"; do
cp "$RELEASE_DIR/agent-registry.json" "$WORK/"
if [ -d "$RELEASE_DIR/offline-jdbc/jdbc" ]; then
cp -R "$RELEASE_DIR/offline-jdbc/jdbc" "$WORK/"
fi
# Copy all JRE archives for this platform
JRE_COUNT=0
for jre_file in "$RELEASE_DIR"/dbx-jre-*-"$platform".tar.zst; do
@@ -56,7 +61,9 @@ for platform in "${PLATFORMS[@]}"; do
done
ZIP_NAME="dbx-agents-offline-${platform}.zip"
(cd "$WORK" && zip -r "$RELEASE_DIR/$ZIP_NAME" agent-registry.json jre/ drivers/)
ZIP_ENTRIES=(agent-registry.json jre/ drivers/)
[ -d "$WORK/jdbc" ] && ZIP_ENTRIES+=(jdbc/)
(cd "$WORK" && zip -r "$RELEASE_DIR/$ZIP_NAME" "${ZIP_ENTRIES[@]}")
SIZE=$(du -h "$RELEASE_DIR/$ZIP_NAME" | cut -f1)
echo "Created $ZIP_NAME ($SIZE)"
done
@@ -0,0 +1,101 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { createReadStream, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const DEFAULT_ASSET_MANIFEST = join(REPO_ROOT, "apps/desktop/src/lib/database/managedJdbcAssets.json");
const PLATFORMS = ["macos-aarch64", "macos-x64", "linux-x64", "linux-aarch64", "windows-x64", "windows-aarch64"];
function fail(message) {
throw new Error(message);
}
async function sha256(path) {
const hash = createHash("sha256");
for await (const chunk of createReadStream(path)) hash.update(chunk);
return hash.digest("hex");
}
function filesBelow(root) {
const files = [];
function visit(directory) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) visit(path);
else if (entry.isFile()) files.push(relative(root, path).replaceAll("\\", "/"));
}
}
visit(root);
return files.sort();
}
function flattenBundles(manifest) {
return Object.values(manifest.drivers).flatMap((driver) => Object.values(driver.bundles || {}));
}
async function validatePayload(payloadDir, assetManifestPath) {
const assets = JSON.parse(readFileSync(assetManifestPath, "utf8"));
const approved = flattenBundles(assets);
const offline = JSON.parse(readFileSync(join(payloadDir, "offline-manifest.json"), "utf8"));
const notices = JSON.parse(readFileSync(join(payloadDir, "third-party-notices.json"), "utf8"));
if (offline.third_party_entry !== "jdbc/third-party-notices.json") fail("Offline JDBC third-party notice entry is missing");
if (JSON.stringify(offline.bundles) !== JSON.stringify(approved.map(({ id, coordinate }) => ({ id, coordinate })))) {
fail("Offline JDBC bundle list differs from the managed JDBC asset manifest");
}
if (notices.bundles.length !== approved.length) fail("Offline JDBC third-party notice list is incomplete");
for (const bundle of approved) {
const manifest = JSON.parse(readFileSync(join(payloadDir, "maven", bundle.id, "manifest.json"), "utf8"));
if (manifest.coordinate !== bundle.coordinate || manifest.artifacts.length !== 1) fail(`Invalid offline JDBC bundle: ${bundle.coordinate}`);
const artifact = manifest.artifacts[0];
const jar = join(payloadDir, "maven", bundle.id, "jars", artifact.file_name);
if (artifact.size !== statSync(jar).size || artifact.sha256 !== (await sha256(jar))) fail(`Invalid bundled JDBC artifact: ${bundle.coordinate}`);
if (artifact.sha256 !== bundle.artifact.sha256 || artifact.size !== bundle.artifact.size) fail(`Unapproved bundled JDBC artifact: ${bundle.coordinate}`);
const notice = notices.bundles.find((candidate) => candidate.id === bundle.id);
if (!notice || JSON.stringify(notice.redistribution) !== JSON.stringify(bundle.redistribution)) fail(`Missing redistribution metadata: ${bundle.coordinate}`);
}
}
async function main() {
const [releaseArg, assetManifestArg] = process.argv.slice(2);
if (!releaseArg) fail("Usage: verify_offline_jdbc_release.mjs <release-dir> [asset-manifest]");
const releaseDir = resolve(releaseArg);
const payloadDir = join(releaseDir, "offline-jdbc", "jdbc");
const assetManifestPath = resolve(assetManifestArg || DEFAULT_ASSET_MANIFEST);
if (!existsSync(payloadDir)) fail(`Offline JDBC payload not found: ${payloadDir}`);
await validatePayload(payloadDir, assetManifestPath);
const expectedFiles = filesBelow(payloadDir);
const expectedHashes = new Map();
for (const file of expectedFiles) expectedHashes.set(file, await sha256(join(payloadDir, file)));
for (const platform of PLATFORMS) {
const zip = join(releaseDir, `dbx-agents-offline-${platform}.zip`);
if (!existsSync(zip)) fail(`Offline ZIP not found: ${zip}`);
const extractionRoot = mkdtempSync(join(tmpdir(), `dbx-offline-verify-${platform}-`));
try {
const result = spawnSync("unzip", ["-q", zip, "-d", extractionRoot], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
if (result.error) fail(`Failed to unpack ${zip}: ${result.error.message}`);
if (result.status !== 0) fail(`Failed to unpack ${zip}: ${(result.stderr || result.stdout).trim()}`);
const extractedPayload = join(extractionRoot, "jdbc");
const actualFiles = filesBelow(extractedPayload);
if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) fail(`Incomplete jdbc/ directory in ${zip}`);
for (const file of actualFiles) {
if ((await sha256(join(extractedPayload, file))) !== expectedHashes.get(file)) fail(`JDBC payload checksum mismatch in ${zip}: ${file}`);
}
} finally {
rmSync(extractionRoot, { recursive: true, force: true });
}
console.log(`Verified complete jdbc/ payload in dbx-agents-offline-${platform}.zip`);
}
}
main().catch((error) => {
console.error(`ERROR: ${error?.stack || error?.message || String(error)}`);
process.exitCode = 1;
});
@@ -628,7 +628,7 @@ async function importOfflineZip() {
resetAgentInstallProgress();
try {
const count = await api.importAgentsFromZip(selected, activeAgentOperationId.value);
await refreshAgents();
await Promise.all([refreshAgents(), loadJdbcDrivers(), loadJdbcPluginStatus()]);
toast(t("driverStore.offlineImportSuccess", { count }));
} catch (e: any) {
toast(t("driverStore.offlineImportFailed", { error: backendError(e) }));
@@ -660,7 +660,7 @@ async function importDriverFile(driver: AgentDriverInfo) {
resetAgentInstallProgress();
try {
const count = await api.importAgentsFromZip(selected, activeAgentOperationId.value);
await refreshAgents();
await Promise.all([refreshAgents(), loadJdbcDrivers(), loadJdbcPluginStatus()]);
toast(t("driverStore.offlineImportSuccess", { count }));
} finally {
activeAgentOperationId.value = null;
@@ -24,6 +24,26 @@ describe("managedJdbcDrivers", () => {
expect(managedJdbcDriverRows([], pluginStatus()).map(({ db_type }) => db_type)).toEqual(["prestosql", "phoenix"]);
});
it("marks both managed drivers installed when the offline payload contains every bundle", () => {
const coordinates = [PRESTOSQL_JDBC_DRIVER_COORDINATE, ...PHOENIX_MANAGED_MAVEN_COORDINATES];
const bundles = coordinates.map(
(coordinate): JdbcMavenBundleInfo => ({
id: coordinate.replace(/[^A-Za-z0-9.-]/g, "_"),
coordinate,
scope: "runtime",
repositories: ["https://repo.maven.apache.org/maven2/"],
installed_at: "2026-08-14T00:00:00Z",
path: `/plugins/jdbc/drivers/maven/${coordinate}`,
artifacts: [],
}),
);
expect(managedJdbcDriverRows(bundles, pluginStatus()).map(({ db_type, installed }) => ({ db_type, installed }))).toEqual([
{ db_type: "prestosql", installed: true },
{ db_type: "phoenix", installed: true },
]);
});
it("installs PrestoSQL through the generic Maven resolver", async () => {
const installed: JdbcMavenBundleInfo = {
id: "io.prestosql_presto-jdbc_350",
@@ -0,0 +1,98 @@
{
"format_version": 1,
"repository": "https://repo.maven.apache.org/maven2/",
"drivers": {
"prestosql": {
"bundles": {
"driver": {
"id": "io.prestosql_presto-jdbc_350",
"coordinate": "io.prestosql:presto-jdbc:350",
"artifact": {
"group_id": "io.prestosql",
"artifact_id": "presto-jdbc",
"version": "350",
"classifier": "",
"extension": "jar",
"file_name": "presto-jdbc-350.jar",
"size": 6656599,
"sha256": "664a23e517c120c1d5a9bb88b215612b117415b9f1649bc22c48298eaf3f979b"
},
"redistribution": {
"project_url": "https://prestosql.io/",
"source_url": "https://github.com/prestosql/presto/tree/350",
"root_license_expression": "Apache-2.0",
"license_entries": ["META-INF/LICENSE", "META-INF/LICENSE.txt"],
"notice_entries": ["META-INF/NOTICE", "META-INF/NOTICE.txt"]
}
}
}
},
"phoenix": {
"bundles": {
"direct": {
"id": "org.apache.phoenix_phoenix-client-embedded-hbase-2.5_5.2.1",
"coordinate": "org.apache.phoenix:phoenix-client-embedded-hbase-2.5:5.2.1",
"artifact": {
"group_id": "org.apache.phoenix",
"artifact_id": "phoenix-client-embedded-hbase-2.5",
"version": "5.2.1",
"classifier": "",
"extension": "jar",
"file_name": "phoenix-client-embedded-hbase-2.5-5.2.1.jar",
"size": 158572775,
"sha256": "c698a81f4250dbfe0af4b656b6de9c72bd3fee26e7945ccb893f741fbe05c4ca"
},
"redistribution": {
"project_url": "https://phoenix.apache.org/",
"source_url": "https://github.com/apache/phoenix/tree/5.2.1",
"root_license_expression": "Apache-2.0",
"license_entries": ["META-INF/LICENSE", "META-INF/LICENSE.txt", "LICENSE", "META-INF/licenses-binary/LICENSE.protobuf.txt"],
"notice_entries": ["META-INF/DEPENDENCIES", "META-INF/NOTICE.txt", "META-INF/NOTICE-binary", "META-INF/NOTICE.md", "META-INF/NOTICE.markdown"]
}
},
"logging": {
"id": "ch.qos.reload4j_reload4j_1.2.26",
"coordinate": "ch.qos.reload4j:reload4j:1.2.26",
"artifact": {
"group_id": "ch.qos.reload4j",
"artifact_id": "reload4j",
"version": "1.2.26",
"classifier": "",
"extension": "jar",
"file_name": "reload4j-1.2.26.jar",
"size": 338688,
"sha256": "a7e86ae144541d04d24d1b7123d7425415477d50f387bb868e5c955aeaedd96f"
},
"redistribution": {
"project_url": "https://reload4j.qos.ch/",
"source_url": "https://github.com/qos-ch/reload4j/tree/v_1.2.26",
"root_license_expression": "Apache-2.0",
"license_entries": ["META-INF/LICENSE"],
"notice_entries": ["META-INF/NOTICE"]
}
},
"query_server": {
"id": "org.apache.phoenix_phoenix-queryserver-client_6.0.0",
"coordinate": "org.apache.phoenix:phoenix-queryserver-client:6.0.0",
"artifact": {
"group_id": "org.apache.phoenix",
"artifact_id": "phoenix-queryserver-client",
"version": "6.0.0",
"classifier": "",
"extension": "jar",
"file_name": "phoenix-queryserver-client-6.0.0.jar",
"size": 6294972,
"sha256": "94e634ded958525f1dee090999c77ab1d0e7e022ea2767b0c73416cc67c8ebcf"
},
"redistribution": {
"project_url": "https://phoenix.apache.org/",
"source_url": "https://github.com/apache/phoenix-queryserver/tree/6.0.0",
"root_license_expression": "Apache-2.0",
"license_entries": ["META-INF/LICENSE"],
"notice_entries": ["META-INF/DEPENDENCIES", "META-INF/NOTICE"]
}
}
}
}
}
}
@@ -1,15 +1,17 @@
import type { ConnectionConfig } from "@/types/database";
import type { JdbcProductProfileDefinition } from "@/lib/database/jdbcProductProfile";
import managedJdbcAssets from "@/lib/database/managedJdbcAssets.json";
export const PHOENIX_DRIVER_PROFILE = "phoenix";
export const PHOENIX_DIRECT_JDBC_URL = "jdbc:phoenix:localhost";
export const PHOENIX_DIRECT_JDBC_DRIVER_CLASS = "org.apache.phoenix.jdbc.PhoenixDriver";
export const PHOENIX_QUERY_SERVER_JDBC_URL = "jdbc:phoenix:thin:url=http://127.0.0.1:8765;serialization=PROTOBUF";
export const PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS = "org.apache.phoenix.queryserver.client.Driver";
export const PHOENIX_MAVEN_REPOSITORY = "https://repo.maven.apache.org/maven2/";
export const PHOENIX_DIRECT_MAVEN_COORDINATE = "org.apache.phoenix:phoenix-client-embedded-hbase-2.5:5.2.1";
export const PHOENIX_QUERY_SERVER_MAVEN_COORDINATE = "org.apache.phoenix:phoenix-queryserver-client:6.0.0";
export const PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE = "ch.qos.reload4j:reload4j:1.2.26";
const PHOENIX_JDBC_ASSETS = managedJdbcAssets.drivers.phoenix.bundles;
export const PHOENIX_MAVEN_REPOSITORY = managedJdbcAssets.repository;
export const PHOENIX_DIRECT_MAVEN_COORDINATE = PHOENIX_JDBC_ASSETS.direct.coordinate;
export const PHOENIX_QUERY_SERVER_MAVEN_COORDINATE = PHOENIX_JDBC_ASSETS.query_server.coordinate;
export const PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE = PHOENIX_JDBC_ASSETS.logging.coordinate;
export const PHOENIX_DRIVER_NOT_INSTALLED_ERROR = "Apache Phoenix JDBC driver is not installed. Install it from the Driver Manager, then retry.";
export const PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR = "DBX JDBC plugin is not installed. Install Apache Phoenix JDBC from the Driver Manager, then retry.";
@@ -1,10 +1,12 @@
import type { JdbcMavenBundleInfo } from "@/types/database";
import { managedJdbcDriverPaths, type ManagedJdbcDriverDefinition } from "@/lib/database/managedJdbcDriver";
import managedJdbcAssets from "@/lib/database/managedJdbcAssets.json";
export const PRESTOSQL_DRIVER_DB_TYPE = "prestosql";
export const PRESTOSQL_JDBC_DRIVER_VERSION = "350";
export const PRESTOSQL_JDBC_DRIVER_COORDINATE = `io.prestosql:presto-jdbc:${PRESTOSQL_JDBC_DRIVER_VERSION}`;
export const PRESTOSQL_JDBC_DRIVER_REPOSITORY = "https://repo.maven.apache.org/maven2/";
const PRESTOSQL_JDBC_ASSET = managedJdbcAssets.drivers.prestosql.bundles.driver;
export const PRESTOSQL_JDBC_DRIVER_VERSION = PRESTOSQL_JDBC_ASSET.artifact.version;
export const PRESTOSQL_JDBC_DRIVER_COORDINATE = PRESTOSQL_JDBC_ASSET.coordinate;
export const PRESTOSQL_JDBC_DRIVER_REPOSITORY = managedJdbcAssets.repository;
export const PRESTOSQL_MANAGED_JDBC_DRIVER: ManagedJdbcDriverDefinition = {
id: PRESTOSQL_DRIVER_DB_TYPE,
label: "PrestoSQL",
+321 -2
View File
@@ -4,11 +4,15 @@ use crate::plugins::{PluginManifest, SUPPORTED_PLUGIN_PROTOCOL_VERSION};
use crate::update::{fetch_latest_release, is_newer_version, JdbcPluginLatest};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::io::Read;
use std::path::{Path, PathBuf};
const JDBC_PLUGIN_DOWNLOAD_URL: &str =
"https://github.com/t8y2/dbx/releases/latest/download/dbx-jdbc-plugin-latest.zip";
const JDBC_PLUGIN_R2_PATH: &str = "releases/latest/dbx-jdbc-plugin-latest.zip";
const OFFLINE_JDBC_MANIFEST_ENTRY: &str = "jdbc/offline-manifest.json";
const OFFLINE_JDBC_FORMAT_VERSION: u32 = 1;
const OFFLINE_JDBC_PLUGIN_ENTRY: &str = "jdbc/plugin.zip";
pub const PRESTOSQL_JDBC_DRIVER_VERSION: &str = "350";
pub const PRESTOSQL_JDBC_DRIVER_COORDINATE: &str = "io.prestosql:presto-jdbc:350";
pub const PRESTOSQL_JDBC_DRIVER_REPOSITORY: &str = "https://repo.maven.apache.org/maven2/";
@@ -83,6 +87,25 @@ pub struct JdbcPluginStatus {
pub path: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OfflineJdbcImportResult {
pub plugin_installed: bool,
pub bundles_installed: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct OfflineJdbcManifest {
format_version: u32,
plugin_entry: String,
bundles: Vec<OfflineJdbcBundle>,
}
#[derive(Debug, Deserialize)]
struct OfflineJdbcBundle {
id: String,
coordinate: String,
}
// ---- JDBC Drivers ----
pub fn list_jdbc_drivers(plugins_root: &Path) -> Result<Vec<JdbcDriverInfo>, String> {
@@ -255,6 +278,66 @@ pub async fn install_prestosql_jdbc_driver(plugins_root: &Path) -> Result<Vec<Jd
list_jdbc_drivers(plugins_root)
}
/// Imports the optional JDBC payload embedded in a full offline Agent ZIP.
/// Older Agent packages have no JDBC manifest and remain valid.
pub fn import_offline_jdbc_payload(
plugins_root: &Path,
zip_path: &Path,
) -> Result<Option<OfflineJdbcImportResult>, String> {
if !zip_path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.to_ascii_lowercase().ends_with(".zip"))
{
return Ok(None);
}
let file = std::fs::File::open(zip_path).map_err(|err| format!("Failed to open offline package: {err}"))?;
let mut archive = zip::ZipArchive::new(file).map_err(|err| format!("Invalid offline package ZIP: {err}"))?;
let manifest_raw = match read_zip_entry(&mut archive, OFFLINE_JDBC_MANIFEST_ENTRY, 1024 * 1024) {
Ok(raw) => raw,
Err(error) if error == format!("Offline package entry not found: {OFFLINE_JDBC_MANIFEST_ENTRY}") => {
return Ok(None);
}
Err(error) => return Err(error),
};
let manifest: OfflineJdbcManifest =
serde_json::from_slice(&manifest_raw).map_err(|err| format!("Failed to parse offline JDBC manifest: {err}"))?;
validate_offline_jdbc_manifest(&manifest)?;
let plugin_bytes = read_zip_entry(&mut archive, &manifest.plugin_entry, 512 * 1024 * 1024)?;
let maven_dir = jdbc_maven_drivers_dir(plugins_root);
std::fs::create_dir_all(&maven_dir).map_err(|err| err.to_string())?;
let staging_root = maven_dir.join(format!(".offline-import-{}", uuid::Uuid::new_v4()));
let import_result = (|| {
std::fs::create_dir_all(&staging_root).map_err(|err| err.to_string())?;
let mut staged_bundles = Vec::with_capacity(manifest.bundles.len());
for bundle in &manifest.bundles {
let staged = stage_offline_jdbc_bundle(&mut archive, bundle, &staging_root, &maven_dir)?;
staged_bundles.push((bundle.id.clone(), staged));
}
install_jdbc_plugin_zip(&plugin_bytes, &plugins_root.join("jdbc"))?;
for (bundle_id, staged_dir) in &staged_bundles {
let target = maven_dir.join(bundle_id);
if target.exists() {
std::fs::remove_dir_all(&target).map_err(|err| err.to_string())?;
}
std::fs::rename(staged_dir, &target)
.map_err(|err| format!("Failed to install offline JDBC bundle {bundle_id}: {err}"))?;
}
Ok(Some(OfflineJdbcImportResult {
plugin_installed: true,
bundles_installed: manifest.bundles.iter().map(|bundle| bundle.coordinate.clone()).collect(),
}))
})();
let _ = std::fs::remove_dir_all(&staging_root);
import_result
}
// ---- JDBC Plugin ----
pub async fn get_jdbc_plugin_status(plugins_root: &Path) -> Result<JdbcPluginStatus, String> {
@@ -540,6 +623,123 @@ fn copy_dir_all(source: &Path, target: &Path) -> Result<(), String> {
Ok(())
}
fn read_zip_entry(
archive: &mut zip::ZipArchive<std::fs::File>,
entry_name: &str,
max_size: u64,
) -> Result<Vec<u8>, String> {
let mut entry = archive.by_name(entry_name).map_err(|err| match err {
zip::result::ZipError::FileNotFound => format!("Offline package entry not found: {entry_name}"),
_ => format!("Failed to read offline package entry {entry_name}: {err}"),
})?;
if entry.size() > max_size {
return Err(format!("Offline package entry is too large: {entry_name}"));
}
let mut bytes = Vec::with_capacity(entry.size().try_into().unwrap_or(0));
entry.read_to_end(&mut bytes).map_err(|err| format!("Failed to read offline package entry {entry_name}: {err}"))?;
Ok(bytes)
}
fn validate_offline_jdbc_manifest(manifest: &OfflineJdbcManifest) -> Result<(), String> {
if manifest.format_version != OFFLINE_JDBC_FORMAT_VERSION {
return Err(format!("Unsupported offline JDBC format version: {}", manifest.format_version));
}
if manifest.plugin_entry != OFFLINE_JDBC_PLUGIN_ENTRY {
return Err(format!("Unexpected offline JDBC plugin entry: {}", manifest.plugin_entry));
}
if manifest.bundles.is_empty() {
return Err("Offline JDBC payload contains no Maven bundles".to_string());
}
let mut ids = std::collections::HashSet::new();
let mut coordinates = std::collections::HashSet::new();
for bundle in &manifest.bundles {
if !is_safe_bundle_id(&bundle.id) {
return Err(format!("Invalid offline JDBC bundle id: {}", bundle.id));
}
if bundle.id != maven_bundle_id(&bundle.coordinate) {
return Err(format!("Offline JDBC bundle id does not match coordinate: {}", bundle.coordinate));
}
if !ids.insert(bundle.id.as_str()) {
return Err(format!("Duplicate offline JDBC bundle id: {}", bundle.id));
}
if !coordinates.insert(bundle.coordinate.as_str()) {
return Err(format!("Duplicate offline JDBC coordinate: {}", bundle.coordinate));
}
}
Ok(())
}
fn stage_offline_jdbc_bundle(
archive: &mut zip::ZipArchive<std::fs::File>,
expected: &OfflineJdbcBundle,
staging_root: &Path,
final_maven_dir: &Path,
) -> Result<PathBuf, String> {
let prefix = format!("jdbc/maven/{}/", expected.id);
let manifest_entry = format!("{prefix}manifest.json");
let manifest_raw = read_zip_entry(archive, &manifest_entry, 4 * 1024 * 1024)?;
let portable: JdbcMavenBundleInfo = serde_json::from_slice(&manifest_raw)
.map_err(|err| format!("Failed to parse offline JDBC bundle manifest {manifest_entry}: {err}"))?;
if portable.id != expected.id || portable.coordinate != expected.coordinate {
return Err(format!("Offline JDBC bundle manifest does not match {}", expected.coordinate));
}
if portable.artifacts.is_empty() {
return Err(format!("Offline JDBC bundle contains no JAR artifacts: {}", expected.coordinate));
}
let staged_dir = staging_root.join(&expected.id);
let jars_dir = staged_dir.join("jars");
std::fs::create_dir_all(&jars_dir).map_err(|err| err.to_string())?;
let final_bundle_dir = final_maven_dir.join(&expected.id);
let mut artifacts = Vec::with_capacity(portable.artifacts.len());
let mut file_names = std::collections::HashSet::new();
for mut artifact in portable.artifacts {
let file_name_path = Path::new(&artifact.file_name);
if artifact.extension != "jar"
|| file_name_path.file_name().and_then(|name| name.to_str()) != Some(artifact.file_name.as_str())
|| !file_names.insert(artifact.file_name.clone())
{
return Err(format!("Invalid offline JDBC artifact name: {}", artifact.file_name));
}
let entry_name = format!("{prefix}jars/{}", artifact.file_name);
let mut entry = archive.by_name(&entry_name).map_err(|err| match err {
zip::result::ZipError::FileNotFound => format!("Offline package entry not found: {entry_name}"),
_ => format!("Failed to read offline package entry {entry_name}: {err}"),
})?;
if entry.size() != artifact.size {
return Err(format!("Offline JDBC artifact size mismatch: {entry_name}"));
}
let target = jars_dir.join(&artifact.file_name);
let mut output = std::fs::File::create(&target).map_err(|err| err.to_string())?;
std::io::copy(&mut entry, &mut output)
.map_err(|err| format!("Failed to extract offline JDBC artifact {entry_name}: {err}"))?;
drop(output);
let sha256 = file_sha256(&target)?;
if !sha256.eq_ignore_ascii_case(&artifact.sha256) {
return Err(format!("Offline JDBC artifact checksum mismatch: {entry_name}"));
}
artifact.path = final_bundle_dir.join("jars").join(&artifact.file_name).to_string_lossy().to_string();
artifacts.push(artifact);
}
let installed = JdbcMavenBundleInfo {
id: portable.id,
coordinate: portable.coordinate,
scope: portable.scope,
repositories: portable.repositories,
installed_at: chrono::Utc::now().to_rfc3339(),
path: final_bundle_dir.to_string_lossy().to_string(),
artifacts,
};
std::fs::write(
staged_dir.join("manifest.json"),
serde_json::to_string_pretty(&installed).map_err(|err| err.to_string())?,
)
.map_err(|err| err.to_string())?;
Ok(staged_dir)
}
fn list_jdbc_drivers_from_dir(drivers_dir: &Path) -> Result<Vec<JdbcDriverInfo>, String> {
let entries = match std::fs::read_dir(drivers_dir) {
Ok(entries) => entries,
@@ -858,8 +1058,17 @@ fn is_safe_bundle_id(bundle_id: &str) -> bool {
}
fn file_sha256(path: &Path) -> Result<String, String> {
let bytes = std::fs::read(path).map_err(|err| err.to_string())?;
let hash = Sha256::digest(bytes);
let mut file = std::fs::File::open(path).map_err(|err| err.to_string())?;
let mut hash = Sha256::new();
let mut buffer = [0_u8; 64 * 1024];
loop {
let read = file.read(&mut buffer).map_err(|err| err.to_string())?;
if read == 0 {
break;
}
hash.update(&buffer[..read]);
}
let hash = hash.finalize();
Ok(format!("{hash:x}"))
}
@@ -911,6 +1120,71 @@ fn paths_refer_to_same_file(left: &Path, right: &Path) -> bool {
mod tests {
use super::*;
use crate::plugins::PluginRuntimeEnv;
use std::io::Write;
fn test_jdbc_plugin_zip() -> Vec<u8> {
let cursor = std::io::Cursor::new(Vec::new());
let mut zip = zip::ZipWriter::new(cursor);
let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file("dbx-jdbc-plugin-test/manifest.json", options).unwrap();
zip.write_all(
br#"{
"id": "jdbc",
"name": "DBX JDBC Plugin",
"version": "0.1.30",
"protocol_version": 1,
"executable": "bin/dbx-jdbc-plugin",
"drivers": [{"id":"jdbc","label":"JDBC","kind":"external","database_type":"jdbc"}]
}"#,
)
.unwrap();
zip.start_file("dbx-jdbc-plugin-test/bin/dbx-jdbc-plugin", options).unwrap();
zip.write_all(b"#!/bin/sh\n").unwrap();
zip.finish().unwrap().into_inner()
}
fn write_offline_jdbc_package(path: &Path, artifact_sha256: &str) {
let coordinate = "com.example:demo-driver:1.0.0";
let bundle_id = maven_bundle_id(coordinate);
let artifact = b"offline-driver";
let bundle = JdbcMavenBundleInfo {
id: bundle_id.clone(),
coordinate: coordinate.to_string(),
scope: "runtime".to_string(),
repositories: vec!["https://repo.maven.apache.org/maven2/".to_string()],
installed_at: String::new(),
path: String::new(),
artifacts: vec![JdbcMavenArtifactInfo {
group_id: "com.example".to_string(),
artifact_id: "demo-driver".to_string(),
version: "1.0.0".to_string(),
classifier: String::new(),
extension: "jar".to_string(),
file_name: "demo-driver-1.0.0.jar".to_string(),
path: String::new(),
size: artifact.len() as u64,
sha256: artifact_sha256.to_string(),
}],
};
let manifest = serde_json::json!({
"format_version": 1,
"plugin_entry": "jdbc/plugin.zip",
"bundles": [{"id": bundle_id, "coordinate": coordinate}],
});
let file = std::fs::File::create(path).unwrap();
let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file(OFFLINE_JDBC_MANIFEST_ENTRY, options).unwrap();
zip.write_all(serde_json::to_string(&manifest).unwrap().as_bytes()).unwrap();
zip.start_file(OFFLINE_JDBC_PLUGIN_ENTRY, options).unwrap();
zip.write_all(&test_jdbc_plugin_zip()).unwrap();
zip.start_file(format!("jdbc/maven/{}/manifest.json", bundle.id), options).unwrap();
zip.write_all(serde_json::to_string(&bundle).unwrap().as_bytes()).unwrap();
zip.start_file(format!("jdbc/maven/{}/jars/demo-driver-1.0.0.jar", bundle.id), options).unwrap();
zip.write_all(artifact).unwrap();
zip.finish().unwrap();
}
#[test]
fn maven_bundle_install_lists_nested_jars() {
@@ -957,6 +1231,51 @@ mod tests {
assert!(!is_safe_bundle_id("../hive"));
}
#[test]
fn imports_offline_jdbc_plugin_and_rewrites_bundle_paths() {
let root = std::env::temp_dir().join(format!("dbx-offline-jdbc-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&root).unwrap();
let package = root.join("offline.zip");
let artifact_sha256 = format!("{:x}", Sha256::digest(b"offline-driver"));
write_offline_jdbc_package(&package, &artifact_sha256);
let plugins_root = root.join("plugins");
let result = import_offline_jdbc_payload(&plugins_root, &package).unwrap().unwrap();
assert!(result.plugin_installed);
assert_eq!(result.bundles_installed, vec!["com.example:demo-driver:1.0.0"]);
assert!(plugins_root.join("jdbc/manifest.json").exists());
let bundles = list_jdbc_maven_bundles(&plugins_root).unwrap();
assert_eq!(bundles.len(), 1);
assert_eq!(
bundles[0].path,
plugins_root.join("jdbc/drivers/maven/com.example_demo-driver_1.0.0").to_string_lossy()
);
assert_eq!(
bundles[0].artifacts[0].path,
plugins_root
.join("jdbc/drivers/maven/com.example_demo-driver_1.0.0/jars/demo-driver-1.0.0.jar")
.to_string_lossy()
);
assert_eq!(std::fs::read(&bundles[0].artifacts[0].path).unwrap(), b"offline-driver");
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn rejects_corrupt_offline_jdbc_artifact_before_installing_plugin() {
let root = std::env::temp_dir().join(format!("dbx-offline-jdbc-corrupt-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&root).unwrap();
let package = root.join("offline.zip");
write_offline_jdbc_package(&package, &"0".repeat(64));
let plugins_root = root.join("plugins");
let error = import_offline_jdbc_payload(&plugins_root, &package).unwrap_err();
assert!(error.contains("checksum mismatch"));
assert!(!plugins_root.join("jdbc/manifest.json").exists());
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn multiple_local_jars_are_imported_as_one_bundle() {
let root = std::env::temp_dir().join(format!("dbx-jdbc-local-bundle-test-{}", uuid::Uuid::new_v4()));
+5 -2
View File
@@ -236,11 +236,14 @@ pub async fn import_agents_from_zip(
let plan = inspect_offline_package(&package_path).map_err(AppError::from)?;
ensure_no_offline_import_blockers(&state.app, &plan).await.map_err(AppError::from)?;
import_agents_from_package_core(&state.app.agent_manager, &package_path, |event| {
let import_result = import_agents_from_package_core(&state.app.agent_manager, &package_path, |event| {
send_progress_event(&tx, event.with_operation_id(&operation_id))
})
.await
.map_err(AppError::from)
.map_err(AppError::from)?;
dbx_core::jdbc::import_offline_jdbc_payload(state.app.plugins.root_dir(), &package_path)
.map_err(AppError::from)?;
Ok::<_, AppError>(import_result)
}
.await;
let _ = std::fs::remove_file(&package_path);
+1
View File
@@ -176,6 +176,7 @@ pub async fn import_agents_from_zip(
emit_agent_progress(&app_handle, &operation_id, event)
})
.await?;
dbx_core::jdbc::import_offline_jdbc_payload(state.plugins.root_dir(), &package_path)?;
let count = result.drivers_installed.len() as u32;
emit_agent_progress(&app, &operation_id, AgentProgressEvent::step("done"));
Ok(count)