1600 lines
65 KiB
Rust
1600 lines
65 KiB
Rust
use crate::agent_service::AgentProgressEvent;
|
|
use crate::plugins::PluginRuntimeEnv;
|
|
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/";
|
|
const PRESTOSQL_JDBC_DRIVER_URL: &str =
|
|
"https://repo.maven.apache.org/maven2/io/prestosql/presto-jdbc/350/presto-jdbc-350.jar";
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct JdbcDriverInfo {
|
|
pub name: String,
|
|
pub path: String,
|
|
pub size: u64,
|
|
pub bundle_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct JdbcMavenInstallRequest {
|
|
pub coordinate: String,
|
|
#[serde(default)]
|
|
pub repositories: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JdbcMavenBundleInfo {
|
|
pub id: String,
|
|
pub coordinate: String,
|
|
pub scope: String,
|
|
pub repositories: Vec<String>,
|
|
pub installed_at: String,
|
|
pub path: String,
|
|
pub artifacts: Vec<JdbcMavenArtifactInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JdbcMavenArtifactInfo {
|
|
pub group_id: String,
|
|
pub artifact_id: String,
|
|
pub version: String,
|
|
pub classifier: String,
|
|
pub extension: String,
|
|
pub file_name: String,
|
|
pub path: String,
|
|
pub size: u64,
|
|
pub sha256: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JdbcLocalBundleInfo {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub installed_at: String,
|
|
pub path: String,
|
|
pub artifacts: Vec<JdbcLocalArtifactInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JdbcLocalArtifactInfo {
|
|
pub file_name: String,
|
|
pub path: String,
|
|
pub size: u64,
|
|
pub sha256: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct JdbcPluginStatus {
|
|
pub installed: bool,
|
|
pub version: Option<String>,
|
|
pub protocol_version: Option<u32>,
|
|
pub compatible: bool,
|
|
pub latest_version: Option<String>,
|
|
pub latest_protocol_version: Option<u32>,
|
|
pub update_available: bool,
|
|
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> {
|
|
list_jdbc_drivers_from_dir(&jdbc_drivers_dir(plugins_root))
|
|
}
|
|
|
|
pub fn list_jdbc_maven_bundles(plugins_root: &Path) -> Result<Vec<JdbcMavenBundleInfo>, String> {
|
|
list_jdbc_maven_bundles_from_dir(&jdbc_maven_drivers_dir(plugins_root))
|
|
}
|
|
|
|
pub fn list_jdbc_local_bundles(plugins_root: &Path) -> Result<Vec<JdbcLocalBundleInfo>, String> {
|
|
list_jdbc_local_bundles_from_dir(&jdbc_local_drivers_dir(plugins_root))
|
|
}
|
|
|
|
pub fn import_jdbc_drivers(plugins_root: &Path, paths: &[String]) -> Result<Vec<JdbcDriverInfo>, String> {
|
|
if paths.len() > 1 {
|
|
install_jdbc_local_bundle(plugins_root, paths)?;
|
|
return list_jdbc_drivers(plugins_root);
|
|
}
|
|
let drivers_dir = jdbc_drivers_dir(plugins_root);
|
|
std::fs::create_dir_all(&drivers_dir).map_err(|err| err.to_string())?;
|
|
|
|
for path in paths {
|
|
let source = PathBuf::from(path);
|
|
if !source.exists() {
|
|
return Err(format!("Driver JAR does not exist: {}", source.display()));
|
|
}
|
|
if source.extension().and_then(|ext| ext.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")) != Some(true) {
|
|
return Err(format!("Only .jar files can be imported: {}", source.display()));
|
|
}
|
|
let file_name = source
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.ok_or_else(|| format!("Invalid driver file path: {}", source.display()))?;
|
|
let target = drivers_dir.join(file_name);
|
|
if paths_refer_to_same_file(&source, &target) {
|
|
continue;
|
|
}
|
|
let target = unique_target_path(&drivers_dir, file_name);
|
|
std::fs::copy(&source, &target)
|
|
.map_err(|err| format!("Failed to import {} to {}: {err}", source.display(), target.display()))?;
|
|
}
|
|
|
|
list_jdbc_drivers_from_dir(&drivers_dir)
|
|
}
|
|
|
|
pub fn delete_jdbc_driver(plugins_root: &Path, path: &str) -> Result<Vec<JdbcDriverInfo>, String> {
|
|
let drivers_dir = jdbc_drivers_dir(plugins_root);
|
|
let drivers_dir = drivers_dir.canonicalize().map_err(|err| err.to_string())?;
|
|
let target = PathBuf::from(path).canonicalize().map_err(|err| err.to_string())?;
|
|
if !target.starts_with(&drivers_dir) {
|
|
return Err("Driver file is outside the JDBC drivers directory".to_string());
|
|
}
|
|
std::fs::remove_file(&target).map_err(|err| err.to_string())?;
|
|
list_jdbc_drivers_from_dir(&drivers_dir)
|
|
}
|
|
|
|
pub fn delete_jdbc_maven_bundle(plugins_root: &Path, bundle_id: &str) -> Result<Vec<JdbcDriverInfo>, String> {
|
|
if !is_safe_bundle_id(bundle_id) {
|
|
return Err("Invalid JDBC Maven bundle id".to_string());
|
|
}
|
|
let bundles_dir = jdbc_maven_drivers_dir(plugins_root);
|
|
let target = bundles_dir.join(bundle_id);
|
|
if target.exists() {
|
|
std::fs::remove_dir_all(&target).map_err(|err| err.to_string())?;
|
|
}
|
|
list_jdbc_drivers(plugins_root)
|
|
}
|
|
|
|
pub fn delete_jdbc_local_bundle(plugins_root: &Path, bundle_id: &str) -> Result<Vec<JdbcDriverInfo>, String> {
|
|
if !is_safe_bundle_id(bundle_id) {
|
|
return Err("Invalid JDBC local bundle id".to_string());
|
|
}
|
|
let target = jdbc_local_drivers_dir(plugins_root).join(bundle_id);
|
|
if target.exists() {
|
|
std::fs::remove_dir_all(&target).map_err(|err| err.to_string())?;
|
|
}
|
|
list_jdbc_drivers(plugins_root)
|
|
}
|
|
|
|
pub async fn install_jdbc_driver_from_maven(
|
|
plugins_root: &Path,
|
|
request: JdbcMavenInstallRequest,
|
|
env: PluginRuntimeEnv,
|
|
) -> Result<Vec<JdbcDriverInfo>, String> {
|
|
let coordinate = request.coordinate.trim().to_string();
|
|
if coordinate.is_empty() {
|
|
return Err("Maven coordinate is required".to_string());
|
|
}
|
|
let plugin_dir = plugins_root.join("jdbc");
|
|
let resolver = jdbc_maven_resolver_executable(&plugin_dir);
|
|
if !resolver.exists() {
|
|
return Err("JDBC Maven resolver is not installed. Update or reinstall the JDBC plugin.".to_string());
|
|
}
|
|
|
|
let mut repositories = if request.repositories.is_empty() {
|
|
vec!["https://repo.maven.apache.org/maven2/".to_string()]
|
|
} else {
|
|
request.repositories.into_iter().map(|repo| repo.trim().to_string()).filter(|repo| !repo.is_empty()).collect()
|
|
};
|
|
if repositories.is_empty() {
|
|
repositories.push("https://repo.maven.apache.org/maven2/".to_string());
|
|
}
|
|
let local_repo = plugin_dir.join("maven-cache");
|
|
std::fs::create_dir_all(&local_repo).map_err(|err| err.to_string())?;
|
|
|
|
let mut resolved = resolve_maven_artifacts(&resolver, &coordinate, &local_repo, &repositories, &env).await?;
|
|
if let Some(orai18n) = oracle_orai18n_companion(&resolved) {
|
|
match resolve_maven_artifacts(&resolver, &orai18n, &local_repo, &repositories, &env).await {
|
|
Ok(mut companion) => resolved.artifacts.append(&mut companion.artifacts),
|
|
Err(_) => {
|
|
// A missing orai18n companion must not block the driver install;
|
|
// affected charsets surface a dedicated hint at runtime.
|
|
}
|
|
}
|
|
}
|
|
let root = plugins_root.to_path_buf();
|
|
tokio::task::spawn_blocking(move || install_jdbc_maven_bundle(&root, &coordinate, &resolved))
|
|
.await
|
|
.map_err(|err| err.to_string())??;
|
|
list_jdbc_drivers(plugins_root)
|
|
}
|
|
|
|
const ORACLE_JDBC_GROUP_ID: &str = "com.oracle.database.jdbc";
|
|
const ORACLE_NLS_GROUP_ID: &str = "com.oracle.database.nls";
|
|
const ORAI18N_ARTIFACT_ID: &str = "orai18n";
|
|
|
|
/// Oracle's thin driver jar ships converters for a small default charset set;
|
|
/// multibyte charsets such as ZHS16GBK need the orai18n companion jar, otherwise
|
|
/// every metadata call that reads dictionary comments fails wholesale. DBeaver
|
|
/// ships the same pairing.
|
|
fn oracle_orai18n_companion(resolved: &MavenResolveOutput) -> Option<String> {
|
|
let ojdbc = resolved
|
|
.artifacts
|
|
.iter()
|
|
.find(|artifact| artifact.group_id == ORACLE_JDBC_GROUP_ID && artifact.artifact_id.starts_with("ojdbc"))?;
|
|
if resolved
|
|
.artifacts
|
|
.iter()
|
|
.any(|artifact| artifact.group_id == ORACLE_NLS_GROUP_ID && artifact.artifact_id == ORAI18N_ARTIFACT_ID)
|
|
{
|
|
return None;
|
|
}
|
|
Some(format!("{ORACLE_NLS_GROUP_ID}:{ORAI18N_ARTIFACT_ID}:{}", ojdbc.version))
|
|
}
|
|
|
|
async fn resolve_maven_artifacts(
|
|
resolver: &Path,
|
|
coordinate: &str,
|
|
local_repo: &Path,
|
|
repositories: &[String],
|
|
env: &PluginRuntimeEnv,
|
|
) -> Result<MavenResolveOutput, String> {
|
|
let mut command = crate::process::new_tokio_command(resolver);
|
|
env.apply_to(&mut command);
|
|
command.arg("resolve").arg("--coordinate").arg(coordinate).arg("--local-repo").arg(local_repo);
|
|
for repo in repositories {
|
|
command.arg("--repo").arg(repo);
|
|
}
|
|
let output = command.output().await.map_err(|err| format!("Failed to run JDBC Maven resolver: {err}"))?;
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
return Err(if stderr.is_empty() { "JDBC Maven resolver failed".to_string() } else { stderr });
|
|
}
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
serde_json::from_str(&stdout).map_err(|err| format!("Failed to parse JDBC Maven resolver output: {err}"))
|
|
}
|
|
|
|
pub async fn install_prestosql_jdbc_driver(plugins_root: &Path) -> Result<Vec<JdbcDriverInfo>, String> {
|
|
let plugin_dir = plugins_root.join("jdbc");
|
|
let temp_dir = plugin_dir.join("downloads");
|
|
std::fs::create_dir_all(&temp_dir).map_err(|err| err.to_string())?;
|
|
let temp_file = temp_dir.join("presto-jdbc-350.jar.tmp");
|
|
|
|
let client = reqwest::Client::builder()
|
|
.timeout(std::time::Duration::from_secs(300))
|
|
.build()
|
|
.map_err(|err| err.to_string())?;
|
|
let mut resp = client
|
|
.get(PRESTOSQL_JDBC_DRIVER_URL)
|
|
.header(reqwest::header::USER_AGENT, "dbx-prestosql-driver-installer")
|
|
.send()
|
|
.await
|
|
.and_then(|resp| resp.error_for_status())
|
|
.map_err(|err| format!("Failed to download PrestoSQL JDBC driver: {err}"))?;
|
|
let mut file = std::fs::File::create(&temp_file)
|
|
.map_err(|err| format!("Failed to create PrestoSQL JDBC driver temp file: {err}"))?;
|
|
while let Some(chunk) = resp.chunk().await.map_err(|err| format!("Download stream error: {err}"))? {
|
|
std::io::Write::write_all(&mut file, &chunk).map_err(|err| format!("Failed to write chunk: {err}"))?;
|
|
}
|
|
std::io::Write::flush(&mut file).map_err(|err| format!("Failed to flush temp file: {err}"))?;
|
|
drop(file);
|
|
|
|
let root = plugins_root.to_path_buf();
|
|
let repositories = vec![PRESTOSQL_JDBC_DRIVER_REPOSITORY.to_string()];
|
|
tokio::task::spawn_blocking(move || {
|
|
install_single_jdbc_maven_artifact(
|
|
&root,
|
|
PRESTOSQL_JDBC_DRIVER_COORDINATE,
|
|
"io.prestosql",
|
|
"presto-jdbc",
|
|
PRESTOSQL_JDBC_DRIVER_VERSION,
|
|
&temp_file,
|
|
&repositories,
|
|
)?;
|
|
let _ = std::fs::remove_file(&temp_file);
|
|
Ok::<(), String>(())
|
|
})
|
|
.await
|
|
.map_err(|err| err.to_string())??;
|
|
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> {
|
|
jdbc_plugin_status_from_dir(&plugins_root.join("jdbc")).await
|
|
}
|
|
|
|
pub async fn install_jdbc_plugin(plugins_root: &Path) -> Result<JdbcPluginStatus, String> {
|
|
install_jdbc_plugin_with_progress(plugins_root, |_| {}).await
|
|
}
|
|
|
|
pub async fn install_jdbc_plugin_with_progress(
|
|
plugins_root: &Path,
|
|
progress: impl Fn(AgentProgressEvent),
|
|
) -> Result<JdbcPluginStatus, String> {
|
|
let bytes = download_jdbc_plugin_zip_with_progress(&progress).await?;
|
|
let plugin_dir = plugins_root.join("jdbc");
|
|
let status_dir = plugin_dir.clone();
|
|
progress(AgentProgressEvent::transfer("jdbc-plugin-extract", 0, 0));
|
|
tokio::task::spawn_blocking(move || install_jdbc_plugin_zip(&bytes, &plugin_dir))
|
|
.await
|
|
.map_err(|err| err.to_string())??;
|
|
let status = jdbc_plugin_status_from_dir(&status_dir).await;
|
|
progress(AgentProgressEvent::step("done"));
|
|
status
|
|
}
|
|
|
|
pub async fn install_jdbc_plugin_from_file(plugins_root: &Path, file_path: &str) -> Result<JdbcPluginStatus, String> {
|
|
let plugin_dir = plugins_root.join("jdbc");
|
|
let status_dir = plugin_dir.clone();
|
|
let file_path = file_path.to_string();
|
|
tokio::task::spawn_blocking(move || {
|
|
let bytes = std::fs::read(file_path).map_err(|e| format!("Failed to read file: {e}"))?;
|
|
install_jdbc_plugin_zip(&bytes, &plugin_dir)
|
|
})
|
|
.await
|
|
.map_err(|err| err.to_string())??;
|
|
// Local install must stay fully offline: build status from the local
|
|
// manifest only, do not check for a newer release over the network.
|
|
jdbc_plugin_status_from_dir_local(&status_dir)
|
|
}
|
|
|
|
pub fn uninstall_jdbc_plugin(plugins_root: &Path) -> Result<JdbcPluginStatus, String> {
|
|
let plugin_dir = plugins_root.join("jdbc");
|
|
for entry in ["manifest.json", "bin", "lib"] {
|
|
let path = plugin_dir.join(entry);
|
|
if !path.exists() {
|
|
continue;
|
|
}
|
|
if path.is_dir() {
|
|
std::fs::remove_dir_all(path).map_err(|err| err.to_string())?;
|
|
} else {
|
|
std::fs::remove_file(path).map_err(|err| err.to_string())?;
|
|
}
|
|
}
|
|
jdbc_plugin_status_from_dir_local(&plugin_dir)
|
|
}
|
|
|
|
/// Builds plugin status from the local manifest only, no network call.
|
|
/// Used by install/uninstall paths that must stay fully offline (uninstall,
|
|
/// and installing a plugin package the user already has on disk).
|
|
fn jdbc_plugin_status_from_dir_local(plugin_dir: &Path) -> Result<JdbcPluginStatus, String> {
|
|
let manifest_path = plugin_dir.join("manifest.json");
|
|
let manifest = match std::fs::read_to_string(&manifest_path) {
|
|
Ok(raw) => Some(serde_json::from_str::<PluginManifest>(&raw).map_err(|err| err.to_string())?),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
|
|
Err(err) => return Err(err.to_string()),
|
|
};
|
|
Ok(build_plugin_status(&manifest, None, plugin_dir))
|
|
}
|
|
|
|
// ---- System Fonts ----
|
|
|
|
#[cfg(feature = "system-fonts")]
|
|
pub fn list_system_fonts() -> Vec<String> {
|
|
let source = font_kit::source::SystemSource::new();
|
|
match source.all_families() {
|
|
Ok(families) => {
|
|
use std::collections::BTreeSet;
|
|
families
|
|
.into_iter()
|
|
.map(|family| family.trim().to_string())
|
|
.filter(|family| !family.is_empty())
|
|
.collect::<BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect()
|
|
}
|
|
Err(_) => vec![],
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "system-fonts"))]
|
|
pub fn list_system_fonts() -> Vec<String> {
|
|
vec![]
|
|
}
|
|
|
|
// ---- Internal helpers ----
|
|
|
|
fn jdbc_drivers_dir(plugins_root: &Path) -> PathBuf {
|
|
plugins_root.join("jdbc").join("drivers")
|
|
}
|
|
|
|
fn jdbc_maven_drivers_dir(plugins_root: &Path) -> PathBuf {
|
|
jdbc_drivers_dir(plugins_root).join("maven")
|
|
}
|
|
|
|
fn jdbc_local_drivers_dir(plugins_root: &Path) -> PathBuf {
|
|
jdbc_drivers_dir(plugins_root).join("local")
|
|
}
|
|
|
|
fn jdbc_maven_resolver_executable(plugin_dir: &Path) -> PathBuf {
|
|
#[cfg(windows)]
|
|
{
|
|
plugin_dir.join("bin").join("dbx-maven-resolver.bat")
|
|
}
|
|
#[cfg(not(windows))]
|
|
{
|
|
plugin_dir.join("bin").join("dbx-maven-resolver")
|
|
}
|
|
}
|
|
|
|
async fn jdbc_plugin_status_from_dir(plugin_dir: &Path) -> Result<JdbcPluginStatus, String> {
|
|
jdbc_plugin_status_from_dir_after(plugin_dir, latest_jdbc_plugin()).await
|
|
}
|
|
|
|
/// Waits on `latest` before reading the manifest, so a slow/offline network
|
|
/// check can never race ahead of a concurrent local install and report a
|
|
/// stale pre-install snapshot back to the caller. `latest` is a parameter
|
|
/// (rather than calling `latest_jdbc_plugin()` directly) so tests can
|
|
/// simulate a delayed network response without a real network call.
|
|
async fn jdbc_plugin_status_from_dir_after(
|
|
plugin_dir: &Path,
|
|
latest: impl std::future::Future<Output = Option<JdbcPluginLatest>>,
|
|
) -> Result<JdbcPluginStatus, String> {
|
|
let latest = latest.await;
|
|
let manifest_path = plugin_dir.join("manifest.json");
|
|
let manifest = match std::fs::read_to_string(&manifest_path) {
|
|
Ok(raw) => Some(serde_json::from_str::<PluginManifest>(&raw).map_err(|err| err.to_string())?),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
|
|
Err(err) => return Err(err.to_string()),
|
|
};
|
|
Ok(build_plugin_status(&manifest, latest.as_ref(), plugin_dir))
|
|
}
|
|
|
|
fn build_plugin_status(
|
|
manifest: &Option<PluginManifest>,
|
|
latest: Option<&JdbcPluginLatest>,
|
|
plugin_dir: &Path,
|
|
) -> JdbcPluginStatus {
|
|
let version = manifest.as_ref().and_then(|m| (!m.version.is_empty()).then_some(m.version.clone()));
|
|
let protocol_version = manifest.as_ref().map(|m| m.protocol_version);
|
|
let compatible = match manifest.as_ref() {
|
|
Some(m) => m.protocol_version == SUPPORTED_PLUGIN_PROTOCOL_VERSION,
|
|
None => true,
|
|
};
|
|
let latest_version = latest.map(|plugin| plugin.version.clone());
|
|
let latest_protocol_version = latest.map(|plugin| plugin.protocol_version);
|
|
let update_available = match (version.as_deref(), latest) {
|
|
(Some(current), Some(latest)) if manifest.is_some() => is_newer_version(&latest.version, current),
|
|
(None, Some(_)) if manifest.is_some() => true,
|
|
_ => false,
|
|
};
|
|
JdbcPluginStatus {
|
|
installed: manifest.is_some(),
|
|
version,
|
|
protocol_version,
|
|
compatible,
|
|
latest_version,
|
|
latest_protocol_version,
|
|
update_available,
|
|
path: plugin_dir.to_string_lossy().to_string(),
|
|
}
|
|
}
|
|
|
|
async fn latest_jdbc_plugin() -> Option<JdbcPluginLatest> {
|
|
// 只需 jdbc_plugin,不关心 release notes;传中文 locale 跳过英文 notes 拉取
|
|
fetch_latest_release("zh-CN", crate::DownloadSource::Official).await.ok().and_then(|release| release.jdbc_plugin)
|
|
}
|
|
|
|
async fn download_jdbc_plugin_zip_with_progress(progress: &impl Fn(AgentProgressEvent)) -> Result<Vec<u8>, String> {
|
|
let client = reqwest::Client::builder()
|
|
.timeout(std::time::Duration::from_secs(60))
|
|
.build()
|
|
.map_err(|err| err.to_string())?;
|
|
|
|
let mut resp =
|
|
crate::race_download(&client, JDBC_PLUGIN_DOWNLOAD_URL, JDBC_PLUGIN_R2_PATH, "dbx-jdbc-plugin-installer")
|
|
.await
|
|
.map_err(|err| format!("Failed to download JDBC plugin: {err}"))?;
|
|
|
|
let total = resp.content_length().unwrap_or(0);
|
|
progress(AgentProgressEvent::transfer("jdbc-plugin", 0, total));
|
|
let mut downloaded = 0;
|
|
let mut bytes = Vec::with_capacity(total.try_into().unwrap_or(0));
|
|
// Emit transfer progress at most once per whole percent (or 500ms on slow
|
|
// links) instead of once per HTTP chunk; see TransferProgressGate.
|
|
let mut transfer_gate = crate::agent_service::TransferProgressGate::new(total);
|
|
while let Some(chunk) = resp.chunk().await.map_err(|err| err.to_string())? {
|
|
downloaded += chunk.len() as u64;
|
|
bytes.extend_from_slice(&chunk);
|
|
if transfer_gate.record(downloaded, std::time::Instant::now()) {
|
|
progress(AgentProgressEvent::transfer("jdbc-plugin", downloaded, total));
|
|
}
|
|
}
|
|
// The gate may have suppressed the final chunk; always publish the
|
|
// completed transfer so the UI reaches 100% before extraction starts.
|
|
progress(AgentProgressEvent::transfer("jdbc-plugin", downloaded, if total == 0 { downloaded } else { total }));
|
|
Ok(bytes)
|
|
}
|
|
|
|
fn install_jdbc_plugin_zip(bytes: &[u8], plugin_dir: &Path) -> Result<(), String> {
|
|
let cursor = std::io::Cursor::new(bytes);
|
|
let mut archive = zip::ZipArchive::new(cursor).map_err(|err| err.to_string())?;
|
|
let temp_dir = plugin_dir.with_extension("tmp");
|
|
if temp_dir.exists() {
|
|
std::fs::remove_dir_all(&temp_dir).map_err(|err| err.to_string())?;
|
|
}
|
|
std::fs::create_dir_all(&temp_dir).map_err(|err| err.to_string())?;
|
|
|
|
for index in 0..archive.len() {
|
|
let mut file = archive.by_index(index).map_err(|err| err.to_string())?;
|
|
if file.is_dir() {
|
|
continue;
|
|
}
|
|
let Some(enclosed) = file.enclosed_name().map(|path| path.to_path_buf()) else {
|
|
continue;
|
|
};
|
|
let relative = strip_zip_root(&enclosed);
|
|
if relative.as_os_str().is_empty() {
|
|
continue;
|
|
}
|
|
let output = temp_dir.join(relative);
|
|
if let Some(parent) = output.parent() {
|
|
std::fs::create_dir_all(parent).map_err(|err| err.to_string())?;
|
|
}
|
|
let mut target = std::fs::File::create(&output).map_err(|err| err.to_string())?;
|
|
std::io::copy(&mut file, &mut target).map_err(|err| err.to_string())?;
|
|
}
|
|
|
|
if !temp_dir.join("manifest.json").exists() {
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
return Err(format!(
|
|
"This ZIP is not a valid DBX JDBC plugin package (missing manifest.json). \
|
|
The correct package is available at: {JDBC_PLUGIN_DOWNLOAD_URL}"
|
|
));
|
|
}
|
|
let manifest_path = temp_dir.join("manifest.json");
|
|
let manifest = std::fs::read_to_string(&manifest_path)
|
|
.map_err(|err| format!("Failed to read downloaded JDBC plugin manifest: {err}"))?;
|
|
let manifest: PluginManifest = serde_json::from_str(&manifest)
|
|
.map_err(|err| format!("Failed to parse downloaded JDBC plugin manifest: {err}"))?;
|
|
if manifest.id != "jdbc" {
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
return Err(format!("Downloaded plugin has unexpected id '{}'", manifest.id));
|
|
}
|
|
if manifest.protocol_version != SUPPORTED_PLUGIN_PROTOCOL_VERSION {
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
return Err(format!(
|
|
"Downloaded JDBC plugin uses protocol version {}, but this DBX build supports protocol version {}",
|
|
manifest.protocol_version, SUPPORTED_PLUGIN_PROTOCOL_VERSION
|
|
));
|
|
}
|
|
|
|
let drivers_dir = plugin_dir.join("drivers");
|
|
let temp_drivers_dir = temp_dir.join("drivers");
|
|
if drivers_dir.exists() && !temp_drivers_dir.exists() {
|
|
copy_dir_all(&drivers_dir, &temp_drivers_dir)?;
|
|
}
|
|
if plugin_dir.exists() {
|
|
std::fs::remove_dir_all(plugin_dir).map_err(|err| err.to_string())?;
|
|
}
|
|
std::fs::rename(&temp_dir, plugin_dir).map_err(|err| err.to_string())?;
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
for executable in ["dbx-jdbc-plugin", "dbx-maven-resolver"] {
|
|
let executable = plugin_dir.join("bin").join(executable);
|
|
if executable.exists() {
|
|
let mut permissions = std::fs::metadata(&executable).map_err(|err| err.to_string())?.permissions();
|
|
permissions.set_mode(0o755);
|
|
std::fs::set_permissions(executable, permissions).map_err(|err| err.to_string())?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn strip_zip_root(path: &Path) -> PathBuf {
|
|
let mut components = path.components();
|
|
let first = components.next();
|
|
if let (Some(std::path::Component::Normal(_)), Some(_)) = (first, components.clone().next()) {
|
|
components.collect()
|
|
} else {
|
|
path.to_path_buf()
|
|
}
|
|
}
|
|
|
|
fn copy_dir_all(source: &Path, target: &Path) -> Result<(), String> {
|
|
std::fs::create_dir_all(target).map_err(|err| err.to_string())?;
|
|
for entry in std::fs::read_dir(source).map_err(|err| err.to_string())? {
|
|
let entry = entry.map_err(|err| err.to_string())?;
|
|
let file_type = entry.file_type().map_err(|err| err.to_string())?;
|
|
let dest = target.join(entry.file_name());
|
|
if file_type.is_dir() {
|
|
copy_dir_all(&entry.path(), &dest)?;
|
|
} else {
|
|
std::fs::copy(entry.path(), dest).map_err(|err| err.to_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,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(vec![]),
|
|
Err(err) => return Err(err.to_string()),
|
|
};
|
|
|
|
let mut drivers = Vec::new();
|
|
for entry in entries {
|
|
let entry = entry.map_err(|err| err.to_string())?;
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
match path.file_name().and_then(|name| name.to_str()) {
|
|
Some("maven") | Some("local") => {
|
|
append_jdbc_bundle_jars(&mut drivers, &path)?;
|
|
continue;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if path.extension().and_then(|ext| ext.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")) != Some(true) {
|
|
continue;
|
|
}
|
|
let metadata = entry.metadata().map_err(|err| err.to_string())?;
|
|
drivers.push(JdbcDriverInfo {
|
|
name: path.file_name().and_then(|name| name.to_str()).unwrap_or("driver.jar").to_string(),
|
|
path: path.to_string_lossy().to_string(),
|
|
size: metadata.len(),
|
|
bundle_id: None,
|
|
});
|
|
}
|
|
drivers.sort_by(|a, b| a.name.cmp(&b.name));
|
|
Ok(drivers)
|
|
}
|
|
|
|
fn append_jdbc_bundle_jars(drivers: &mut Vec<JdbcDriverInfo>, bundles_dir: &Path) -> Result<(), String> {
|
|
let entries = match std::fs::read_dir(bundles_dir) {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
|
Err(err) => return Err(err.to_string()),
|
|
};
|
|
for entry in entries {
|
|
let entry = entry.map_err(|err| err.to_string())?;
|
|
let bundle_dir = entry.path();
|
|
if !bundle_dir.is_dir() {
|
|
continue;
|
|
}
|
|
let bundle_id = entry.file_name().to_string_lossy().to_string();
|
|
let jars_dir = bundle_dir.join("jars");
|
|
let jars = match std::fs::read_dir(&jars_dir) {
|
|
Ok(jars) => jars,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
|
|
Err(err) => return Err(err.to_string()),
|
|
};
|
|
for jar in jars {
|
|
let jar = jar.map_err(|err| err.to_string())?;
|
|
let path = jar.path();
|
|
if path.extension().and_then(|ext| ext.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")) != Some(true) {
|
|
continue;
|
|
}
|
|
let metadata = jar.metadata().map_err(|err| err.to_string())?;
|
|
drivers.push(JdbcDriverInfo {
|
|
name: path.file_name().and_then(|name| name.to_str()).unwrap_or("driver.jar").to_string(),
|
|
path: path.to_string_lossy().to_string(),
|
|
size: metadata.len(),
|
|
bundle_id: Some(bundle_id.clone()),
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn list_jdbc_local_bundles_from_dir(local_dir: &Path) -> Result<Vec<JdbcLocalBundleInfo>, String> {
|
|
let entries = match std::fs::read_dir(local_dir) {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(vec![]),
|
|
Err(err) => return Err(err.to_string()),
|
|
};
|
|
let mut bundles = Vec::new();
|
|
for entry in entries {
|
|
let entry = entry.map_err(|err| err.to_string())?;
|
|
let manifest_path = entry.path().join("manifest.json");
|
|
if !manifest_path.exists() {
|
|
continue;
|
|
}
|
|
let raw = std::fs::read_to_string(&manifest_path).map_err(|err| err.to_string())?;
|
|
bundles.push(serde_json::from_str::<JdbcLocalBundleInfo>(&raw).map_err(|err| err.to_string())?);
|
|
}
|
|
bundles.sort_by(|a, b| a.name.cmp(&b.name));
|
|
Ok(bundles)
|
|
}
|
|
|
|
fn install_jdbc_local_bundle(plugins_root: &Path, paths: &[String]) -> Result<(), String> {
|
|
let sources = paths
|
|
.iter()
|
|
.map(PathBuf::from)
|
|
.map(|source| {
|
|
if !source.exists() {
|
|
return Err(format!("Driver JAR does not exist: {}", source.display()));
|
|
}
|
|
if source.extension().and_then(|ext| ext.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")) != Some(true)
|
|
{
|
|
return Err(format!("Only .jar files can be imported: {}", source.display()));
|
|
}
|
|
Ok(source)
|
|
})
|
|
.collect::<Result<Vec<_>, String>>()?;
|
|
let name = sources
|
|
.first()
|
|
.and_then(|path| path.file_stem())
|
|
.and_then(|name| name.to_str())
|
|
.filter(|name| !name.is_empty())
|
|
.unwrap_or("JDBC driver")
|
|
.to_string();
|
|
let bundle_id = format!("local-{}", uuid::Uuid::new_v4().simple());
|
|
let bundle_dir = jdbc_local_drivers_dir(plugins_root).join(&bundle_id);
|
|
let temp_dir = bundle_dir.with_extension("tmp");
|
|
std::fs::create_dir_all(temp_dir.join("jars")).map_err(|err| err.to_string())?;
|
|
|
|
let mut artifacts = Vec::with_capacity(sources.len());
|
|
for source in sources {
|
|
let file_name = source
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.ok_or_else(|| format!("Invalid driver file path: {}", source.display()))?;
|
|
let target = unique_target_path(&temp_dir.join("jars"), file_name);
|
|
std::fs::copy(&source, &target)
|
|
.map_err(|err| format!("Failed to import {} to {}: {err}", source.display(), target.display()))?;
|
|
let metadata = std::fs::metadata(&target).map_err(|err| err.to_string())?;
|
|
let final_path = bundle_dir.join("jars").join(target.file_name().unwrap_or_default());
|
|
artifacts.push(JdbcLocalArtifactInfo {
|
|
file_name: target.file_name().and_then(|value| value.to_str()).unwrap_or(file_name).to_string(),
|
|
path: final_path.to_string_lossy().to_string(),
|
|
size: metadata.len(),
|
|
sha256: file_sha256(&target)?,
|
|
});
|
|
}
|
|
let manifest = JdbcLocalBundleInfo {
|
|
id: bundle_id,
|
|
name,
|
|
installed_at: chrono::Utc::now().to_rfc3339(),
|
|
path: bundle_dir.to_string_lossy().to_string(),
|
|
artifacts,
|
|
};
|
|
std::fs::write(
|
|
temp_dir.join("manifest.json"),
|
|
serde_json::to_string_pretty(&manifest).map_err(|err| err.to_string())?,
|
|
)
|
|
.map_err(|err| err.to_string())?;
|
|
std::fs::rename(&temp_dir, &bundle_dir).map_err(|err| err.to_string())
|
|
}
|
|
|
|
fn list_jdbc_maven_bundles_from_dir(maven_dir: &Path) -> Result<Vec<JdbcMavenBundleInfo>, String> {
|
|
let entries = match std::fs::read_dir(maven_dir) {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(vec![]),
|
|
Err(err) => return Err(err.to_string()),
|
|
};
|
|
let mut bundles = Vec::new();
|
|
for entry in entries {
|
|
let entry = entry.map_err(|err| err.to_string())?;
|
|
let manifest_path = entry.path().join("manifest.json");
|
|
if !manifest_path.exists() {
|
|
continue;
|
|
}
|
|
let raw = std::fs::read_to_string(&manifest_path).map_err(|err| err.to_string())?;
|
|
bundles.push(serde_json::from_str::<JdbcMavenBundleInfo>(&raw).map_err(|err| err.to_string())?);
|
|
}
|
|
bundles.sort_by(|a, b| a.coordinate.cmp(&b.coordinate));
|
|
Ok(bundles)
|
|
}
|
|
|
|
fn install_jdbc_maven_bundle(
|
|
plugins_root: &Path,
|
|
coordinate: &str,
|
|
resolved: &MavenResolveOutput,
|
|
) -> Result<(), String> {
|
|
if resolved.artifacts.is_empty() {
|
|
return Err("Maven resolver did not return any JAR artifacts".to_string());
|
|
}
|
|
let bundle_id = maven_bundle_id(coordinate);
|
|
let bundle_dir = jdbc_maven_drivers_dir(plugins_root).join(&bundle_id);
|
|
let temp_dir = bundle_dir.with_extension("tmp");
|
|
if temp_dir.exists() {
|
|
std::fs::remove_dir_all(&temp_dir).map_err(|err| err.to_string())?;
|
|
}
|
|
std::fs::create_dir_all(temp_dir.join("jars")).map_err(|err| err.to_string())?;
|
|
|
|
let mut artifacts = Vec::new();
|
|
for artifact in &resolved.artifacts {
|
|
if !artifact.extension.eq_ignore_ascii_case("jar") {
|
|
continue;
|
|
}
|
|
let source = PathBuf::from(&artifact.file);
|
|
if !source.exists() {
|
|
return Err(format!("Resolved artifact file does not exist: {}", source.display()));
|
|
}
|
|
let file_name = source
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.ok_or_else(|| format!("Invalid resolved artifact path: {}", source.display()))?;
|
|
let target = unique_target_path(&temp_dir.join("jars"), file_name);
|
|
std::fs::copy(&source, &target)
|
|
.map_err(|err| format!("Failed to copy {} to {}: {err}", source.display(), target.display()))?;
|
|
let metadata = std::fs::metadata(&target).map_err(|err| err.to_string())?;
|
|
let sha256 = file_sha256(&target)?;
|
|
let final_path = bundle_dir.join("jars").join(target.file_name().unwrap_or_default());
|
|
artifacts.push(JdbcMavenArtifactInfo {
|
|
group_id: artifact.group_id.clone(),
|
|
artifact_id: artifact.artifact_id.clone(),
|
|
version: artifact.version.clone(),
|
|
classifier: artifact.classifier.clone(),
|
|
extension: artifact.extension.clone(),
|
|
file_name: target.file_name().and_then(|name| name.to_str()).unwrap_or(file_name).to_string(),
|
|
path: final_path.to_string_lossy().to_string(),
|
|
size: metadata.len(),
|
|
sha256,
|
|
});
|
|
}
|
|
if artifacts.is_empty() {
|
|
return Err("Maven resolver did not return any JAR artifacts".to_string());
|
|
}
|
|
let manifest = JdbcMavenBundleInfo {
|
|
id: bundle_id.clone(),
|
|
coordinate: resolved.coordinate.clone(),
|
|
scope: resolved.scope.clone(),
|
|
repositories: resolved.repositories.clone(),
|
|
installed_at: chrono::Utc::now().to_rfc3339(),
|
|
path: bundle_dir.to_string_lossy().to_string(),
|
|
artifacts,
|
|
};
|
|
let manifest_json = serde_json::to_string_pretty(&manifest).map_err(|err| err.to_string())?;
|
|
std::fs::write(temp_dir.join("manifest.json"), manifest_json).map_err(|err| err.to_string())?;
|
|
if bundle_dir.exists() {
|
|
std::fs::remove_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
|
|
}
|
|
std::fs::rename(&temp_dir, &bundle_dir).map_err(|err| err.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
fn install_single_jdbc_maven_artifact(
|
|
plugins_root: &Path,
|
|
coordinate: &str,
|
|
group_id: &str,
|
|
artifact_id: &str,
|
|
version: &str,
|
|
source: &Path,
|
|
repositories: &[String],
|
|
) -> Result<(), String> {
|
|
if !source.exists() {
|
|
return Err(format!("Driver JAR does not exist: {}", source.display()));
|
|
}
|
|
let bundle_id = maven_bundle_id(coordinate);
|
|
let bundle_dir = jdbc_maven_drivers_dir(plugins_root).join(&bundle_id);
|
|
let temp_dir = bundle_dir.with_extension("tmp");
|
|
if temp_dir.exists() {
|
|
std::fs::remove_dir_all(&temp_dir).map_err(|err| err.to_string())?;
|
|
}
|
|
std::fs::create_dir_all(temp_dir.join("jars")).map_err(|err| err.to_string())?;
|
|
|
|
let file_name = source
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.ok_or_else(|| format!("Invalid driver file path: {}", source.display()))?;
|
|
let target = temp_dir.join("jars").join(file_name);
|
|
std::fs::copy(source, &target)
|
|
.map_err(|err| format!("Failed to copy {} to {}: {err}", source.display(), target.display()))?;
|
|
let metadata = std::fs::metadata(&target).map_err(|err| err.to_string())?;
|
|
let final_path = bundle_dir.join("jars").join(file_name);
|
|
let manifest = JdbcMavenBundleInfo {
|
|
id: bundle_id.clone(),
|
|
coordinate: coordinate.to_string(),
|
|
scope: "runtime".to_string(),
|
|
repositories: repositories.to_vec(),
|
|
installed_at: chrono::Utc::now().to_rfc3339(),
|
|
path: bundle_dir.to_string_lossy().to_string(),
|
|
artifacts: vec![JdbcMavenArtifactInfo {
|
|
group_id: group_id.to_string(),
|
|
artifact_id: artifact_id.to_string(),
|
|
version: version.to_string(),
|
|
classifier: String::new(),
|
|
extension: "jar".to_string(),
|
|
file_name: file_name.to_string(),
|
|
path: final_path.to_string_lossy().to_string(),
|
|
size: metadata.len(),
|
|
sha256: file_sha256(&target)?,
|
|
}],
|
|
};
|
|
let manifest_json = serde_json::to_string_pretty(&manifest).map_err(|err| err.to_string())?;
|
|
std::fs::write(temp_dir.join("manifest.json"), manifest_json).map_err(|err| err.to_string())?;
|
|
if bundle_dir.exists() {
|
|
std::fs::remove_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
|
|
}
|
|
std::fs::rename(&temp_dir, &bundle_dir).map_err(|err| err.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
fn maven_bundle_id(coordinate: &str) -> String {
|
|
let mut id = String::with_capacity(coordinate.len());
|
|
for ch in coordinate.trim().chars() {
|
|
if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-') {
|
|
id.push(ch);
|
|
} else {
|
|
id.push('_');
|
|
}
|
|
}
|
|
if id.is_empty() {
|
|
"maven-driver".to_string()
|
|
} else {
|
|
id
|
|
}
|
|
}
|
|
|
|
fn is_safe_bundle_id(bundle_id: &str) -> bool {
|
|
!bundle_id.is_empty() && bundle_id.chars().all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_'))
|
|
}
|
|
|
|
fn file_sha256(path: &Path) -> Result<String, String> {
|
|
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}"))
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct MavenResolveOutput {
|
|
coordinate: String,
|
|
scope: String,
|
|
repositories: Vec<String>,
|
|
artifacts: Vec<MavenResolvedArtifact>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct MavenResolvedArtifact {
|
|
group_id: String,
|
|
artifact_id: String,
|
|
version: String,
|
|
classifier: String,
|
|
extension: String,
|
|
file: String,
|
|
}
|
|
|
|
pub fn unique_target_path(dir: &Path, file_name: &str) -> PathBuf {
|
|
let target = dir.join(file_name);
|
|
if !target.exists() {
|
|
return target;
|
|
}
|
|
|
|
let path = Path::new(file_name);
|
|
let stem = path.file_stem().and_then(|value| value.to_str()).unwrap_or("driver");
|
|
let ext = path.extension().and_then(|value| value.to_str()).unwrap_or("jar");
|
|
for index in 1.. {
|
|
let candidate = dir.join(format!("{stem}-{index}.{ext}"));
|
|
if !candidate.exists() {
|
|
return candidate;
|
|
}
|
|
}
|
|
unreachable!()
|
|
}
|
|
|
|
fn paths_refer_to_same_file(left: &Path, right: &Path) -> bool {
|
|
if left == right {
|
|
return true;
|
|
}
|
|
matches!((left.canonicalize(), right.canonicalize()), (Ok(left), Ok(right)) if left == right)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
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() {
|
|
let root = std::env::temp_dir().join(format!("dbx-jdbc-maven-test-{}", uuid::Uuid::new_v4()));
|
|
let source_dir = root.join("source");
|
|
std::fs::create_dir_all(&source_dir).unwrap();
|
|
let source = source_dir.join("demo-driver.jar");
|
|
std::fs::write(&source, b"jar").unwrap();
|
|
|
|
let resolved = MavenResolveOutput {
|
|
coordinate: "com.example:demo-driver:1.0.0".to_string(),
|
|
scope: "runtime".to_string(),
|
|
repositories: vec!["https://repo.maven.apache.org/maven2/".to_string()],
|
|
artifacts: vec![MavenResolvedArtifact {
|
|
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: source.to_string_lossy().to_string(),
|
|
}],
|
|
};
|
|
|
|
install_jdbc_maven_bundle(&root, "com.example:demo-driver:1.0.0", &resolved).unwrap();
|
|
let drivers = list_jdbc_drivers(&root).unwrap();
|
|
assert_eq!(drivers.len(), 1);
|
|
assert_eq!(drivers[0].name, "demo-driver.jar");
|
|
assert_eq!(drivers[0].bundle_id.as_deref(), Some("com.example_demo-driver_1.0.0"));
|
|
|
|
let bundles = list_jdbc_maven_bundles(&root).unwrap();
|
|
assert_eq!(bundles.len(), 1);
|
|
assert_eq!(bundles[0].artifacts.len(), 1);
|
|
|
|
let _ = std::fs::remove_dir_all(root);
|
|
}
|
|
|
|
#[test]
|
|
fn maven_bundle_ids_are_path_safe() {
|
|
assert_eq!(
|
|
maven_bundle_id("org.apache.hive:hive-jdbc:4.0.1:standalone"),
|
|
"org.apache.hive_hive-jdbc_4.0.1_standalone"
|
|
);
|
|
assert!(is_safe_bundle_id("org.apache.hive_hive-jdbc_4.0.1_standalone"));
|
|
assert!(!is_safe_bundle_id("../hive"));
|
|
}
|
|
|
|
fn resolved_artifact(group_id: &str, artifact_id: &str, version: &str) -> MavenResolvedArtifact {
|
|
MavenResolvedArtifact {
|
|
group_id: group_id.to_string(),
|
|
artifact_id: artifact_id.to_string(),
|
|
version: version.to_string(),
|
|
classifier: String::new(),
|
|
extension: "jar".to_string(),
|
|
file: format!("/tmp/{artifact_id}-{version}.jar"),
|
|
}
|
|
}
|
|
|
|
fn resolved_with(artifacts: Vec<MavenResolvedArtifact>) -> MavenResolveOutput {
|
|
MavenResolveOutput {
|
|
coordinate: String::new(),
|
|
scope: "runtime".to_string(),
|
|
repositories: vec!["https://repo.maven.apache.org/maven2/".to_string()],
|
|
artifacts,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_ojdbc_resolves_matching_orai18n_companion() {
|
|
let resolved = resolved_with(vec![resolved_artifact("com.oracle.database.jdbc", "ojdbc11", "21.9.0.0")]);
|
|
assert_eq!(oracle_orai18n_companion(&resolved).as_deref(), Some("com.oracle.database.nls:orai18n:21.9.0.0"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_orai18n_companion_skips_existing_nls_artifact() {
|
|
let resolved = resolved_with(vec![
|
|
resolved_artifact("com.oracle.database.jdbc", "ojdbc11", "21.9.0.0"),
|
|
resolved_artifact("com.oracle.database.nls", "orai18n", "21.9.0.0"),
|
|
]);
|
|
assert_eq!(oracle_orai18n_companion(&resolved), None);
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_orai18n_companion_ignores_non_oracle_drivers() {
|
|
for coordinate in
|
|
[("org.postgresql", "postgresql"), ("com.oracle.database.xml", "xdb6"), ("mysql", "mysql-connector-java")]
|
|
{
|
|
let resolved = resolved_with(vec![resolved_artifact(coordinate.0, coordinate.1, "8.0.33")]);
|
|
assert_eq!(oracle_orai18n_companion(&resolved), None, "{}", coordinate.1);
|
|
}
|
|
}
|
|
|
|
#[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()));
|
|
let source_dir = root.join("source");
|
|
std::fs::create_dir_all(&source_dir).unwrap();
|
|
let driver = source_dir.join("vendor-driver.jar");
|
|
let dependency = source_dir.join("vendor-codec.jar");
|
|
std::fs::write(&driver, b"driver").unwrap();
|
|
std::fs::write(&dependency, b"dependency").unwrap();
|
|
|
|
let paths = vec![driver.to_string_lossy().to_string(), dependency.to_string_lossy().to_string()];
|
|
let drivers = import_jdbc_drivers(&root, &paths).unwrap();
|
|
let bundles = list_jdbc_local_bundles(&root).unwrap();
|
|
|
|
assert_eq!(bundles.len(), 1);
|
|
assert_eq!(bundles[0].name, "vendor-driver");
|
|
assert_eq!(bundles[0].artifacts.len(), 2);
|
|
assert_eq!(drivers.len(), 2);
|
|
assert!(drivers.iter().all(|item| item.bundle_id.as_deref() == Some(bundles[0].id.as_str())));
|
|
|
|
let remaining = delete_jdbc_local_bundle(&root, &bundles[0].id).unwrap();
|
|
assert!(remaining.is_empty());
|
|
assert!(list_jdbc_local_bundles(&root).unwrap().is_empty());
|
|
let _ = std::fs::remove_dir_all(root);
|
|
}
|
|
|
|
#[test]
|
|
fn single_local_jar_keeps_legacy_standalone_behavior() {
|
|
let root = std::env::temp_dir().join(format!("dbx-jdbc-single-jar-test-{}", uuid::Uuid::new_v4()));
|
|
let source = root.join("source").join("standalone.jar");
|
|
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
|
std::fs::write(&source, b"driver").unwrap();
|
|
|
|
let drivers = import_jdbc_drivers(&root, &[source.to_string_lossy().to_string()]).unwrap();
|
|
|
|
assert_eq!(drivers.len(), 1);
|
|
assert_eq!(drivers[0].name, "standalone.jar");
|
|
assert_eq!(drivers[0].bundle_id, None);
|
|
assert!(list_jdbc_local_bundles(&root).unwrap().is_empty());
|
|
let _ = std::fs::remove_dir_all(root);
|
|
}
|
|
|
|
#[test]
|
|
fn single_local_jar_already_in_drivers_dir_is_not_duplicated() {
|
|
let root = std::env::temp_dir().join(format!("dbx-jdbc-managed-jar-test-{}", uuid::Uuid::new_v4()));
|
|
let source = root.join("jdbc").join("drivers").join("standalone.jar");
|
|
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
|
std::fs::write(&source, b"driver").unwrap();
|
|
|
|
let drivers = import_jdbc_drivers(&root, &[source.to_string_lossy().to_string()]).unwrap();
|
|
|
|
assert_eq!(drivers.len(), 1);
|
|
assert_eq!(drivers[0].name, "standalone.jar");
|
|
assert!(!source.with_file_name("standalone-1.jar").exists());
|
|
let _ = std::fs::remove_dir_all(root);
|
|
}
|
|
|
|
#[test]
|
|
fn single_local_jar_with_existing_name_keeps_unique_import_behavior() {
|
|
let root = std::env::temp_dir().join(format!("dbx-jdbc-duplicate-name-test-{}", uuid::Uuid::new_v4()));
|
|
let installed = root.join("jdbc").join("drivers").join("standalone.jar");
|
|
let source = root.join("source").join("standalone.jar");
|
|
std::fs::create_dir_all(installed.parent().unwrap()).unwrap();
|
|
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
|
std::fs::write(&installed, b"installed").unwrap();
|
|
std::fs::write(&source, b"new").unwrap();
|
|
|
|
let drivers = import_jdbc_drivers(&root, &[source.to_string_lossy().to_string()]).unwrap();
|
|
|
|
assert_eq!(drivers.len(), 2);
|
|
assert!(drivers.iter().any(|driver| driver.name == "standalone.jar"));
|
|
assert!(drivers.iter().any(|driver| driver.name == "standalone-1.jar"));
|
|
assert_eq!(std::fs::read(root.join("jdbc/drivers/standalone-1.jar")).unwrap(), b"new");
|
|
let _ = std::fs::remove_dir_all(root);
|
|
}
|
|
|
|
#[test]
|
|
fn installs_prestosql_direct_driver_as_builtin_bundle() {
|
|
let root = std::env::temp_dir().join(format!("dbx-prestosql-direct-driver-test-{}", uuid::Uuid::new_v4()));
|
|
let source_dir = root.join("source");
|
|
std::fs::create_dir_all(&source_dir).unwrap();
|
|
let source = source_dir.join("presto-jdbc-350.jar");
|
|
std::fs::write(&source, b"presto").unwrap();
|
|
|
|
install_single_jdbc_maven_artifact(
|
|
&root,
|
|
PRESTOSQL_JDBC_DRIVER_COORDINATE,
|
|
"io.prestosql",
|
|
"presto-jdbc",
|
|
PRESTOSQL_JDBC_DRIVER_VERSION,
|
|
&source,
|
|
&[PRESTOSQL_JDBC_DRIVER_REPOSITORY.to_string()],
|
|
)
|
|
.unwrap();
|
|
|
|
let bundles = list_jdbc_maven_bundles(&root).unwrap();
|
|
assert_eq!(bundles.len(), 1);
|
|
assert_eq!(bundles[0].id, "io.prestosql_presto-jdbc_350");
|
|
assert_eq!(bundles[0].coordinate, PRESTOSQL_JDBC_DRIVER_COORDINATE);
|
|
assert_eq!(bundles[0].artifacts[0].file_name, "presto-jdbc-350.jar");
|
|
|
|
let drivers = list_jdbc_drivers(&root).unwrap();
|
|
assert_eq!(drivers.len(), 1);
|
|
assert_eq!(drivers[0].bundle_id.as_deref(), Some("io.prestosql_presto-jdbc_350"));
|
|
|
|
let _ = std::fs::remove_dir_all(root);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn maven_resolver_receives_runtime_env() {
|
|
let root = std::env::temp_dir().join(format!("dbx-jdbc-maven-env-test-{}", uuid::Uuid::new_v4()));
|
|
let plugin_dir = root.join("jdbc");
|
|
let bin_dir = plugin_dir.join("bin");
|
|
let source_dir = root.join("source");
|
|
std::fs::create_dir_all(&bin_dir).unwrap();
|
|
std::fs::create_dir_all(&source_dir).unwrap();
|
|
let source = source_dir.join("env-driver.jar");
|
|
std::fs::write(&source, b"jar").unwrap();
|
|
|
|
let output = serde_json::json!({
|
|
"coordinate": "com.example:env-driver:1.0.0",
|
|
"scope": "runtime",
|
|
"repositories": ["https://repo.maven.apache.org/maven2/"],
|
|
"artifacts": [{
|
|
"groupId": "com.example",
|
|
"artifactId": "env-driver",
|
|
"version": "1.0.0",
|
|
"classifier": "",
|
|
"extension": "jar",
|
|
"file": source.to_string_lossy(),
|
|
}],
|
|
})
|
|
.to_string();
|
|
let resolver = jdbc_maven_resolver_executable(&plugin_dir);
|
|
if cfg!(windows) {
|
|
std::fs::write(
|
|
&resolver,
|
|
format!(
|
|
"@echo off\r\nif \"%DBX_JAVA_BIN%\"==\"\" (\r\n echo missing DBX_JAVA_BIN 1>&2\r\n exit /b 2\r\n)\r\necho {output}\r\n"
|
|
),
|
|
)
|
|
.unwrap();
|
|
} else {
|
|
std::fs::write(
|
|
&resolver,
|
|
format!(
|
|
"#!/usr/bin/env sh\nif [ -z \"${{DBX_JAVA_BIN:-}}\" ]; then\n echo missing DBX_JAVA_BIN >&2\n exit 2\nfi\necho '{}'\n",
|
|
output.replace('\'', "'\\''")
|
|
),
|
|
)
|
|
.unwrap();
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(&resolver, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
}
|
|
}
|
|
|
|
let drivers = install_jdbc_driver_from_maven(
|
|
&root,
|
|
JdbcMavenInstallRequest { coordinate: "com.example:env-driver:1.0.0".to_string(), repositories: vec![] },
|
|
PluginRuntimeEnv::default().with_var("DBX_JAVA_BIN", "java-from-managed-jre"),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(drivers.len(), 1);
|
|
assert_eq!(drivers[0].name, "env-driver.jar");
|
|
|
|
let _ = std::fs::remove_dir_all(root);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn delayed_network_status_check_does_not_revert_a_concurrent_local_install() {
|
|
let root = std::env::temp_dir().join(format!("dbx-jdbc-status-race-test-{}", uuid::Uuid::new_v4()));
|
|
let plugin_dir = root.join("jdbc");
|
|
std::fs::create_dir_all(&plugin_dir).unwrap();
|
|
|
|
// Mirrors DriverStoreDialog's mount-time status check: it starts
|
|
// before the local install and only finishes its (simulated)
|
|
// network wait after the local install has already written the
|
|
// new manifest to disk.
|
|
let status_plugin_dir = plugin_dir.clone();
|
|
let status_task = tokio::spawn(async move {
|
|
jdbc_plugin_status_from_dir_after(&status_plugin_dir, async {
|
|
tokio::time::sleep(std::time::Duration::from_millis(80)).await;
|
|
None
|
|
})
|
|
.await
|
|
});
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
|
// Local install completes fully offline while the status check
|
|
// above is still waiting on its simulated network round-trip.
|
|
std::fs::write(
|
|
plugin_dir.join("manifest.json"),
|
|
r#"{"id":"jdbc","name":"DBX JDBC Plugin","version":"0.1.30","protocol_version":1,"executable":"bin/dbx-jdbc-plugin","drivers":[]}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let status = status_task.await.unwrap().unwrap();
|
|
assert!(status.installed, "stale pre-install snapshot must not overwrite the completed local install");
|
|
assert_eq!(status.version.as_deref(), Some("0.1.30"));
|
|
|
|
let _ = std::fs::remove_dir_all(root);
|
|
}
|
|
}
|