feat: enable driver management on web version
- Extract JDBC driver/plugin logic from Tauri commands to dbx-core::jdbc - Add web API routes for JDBC drivers, JDBC plugin, Agent JAR import, system fonts - Upgrade zip crate from v2 to v4 in dbx-core - Replace http.ts stubs with real HTTP calls (support File and string paths) - Remove isWeb restrictions in DriverStoreDialog, add browser file picker support - Fix Mac traffic light inset incorrectly applied on web version - Show driver management toolbar button on web version
This commit is contained in:
parent
c55a31b294
commit
6e0f4e1dc3
|
|
@ -1842,6 +1842,7 @@ dependencies = [
|
|||
"csv",
|
||||
"deadpool-postgres",
|
||||
"duckdb",
|
||||
"font-kit",
|
||||
"futures",
|
||||
"iana-time-zone",
|
||||
"log",
|
||||
|
|
@ -1866,7 +1867,7 @@ dependencies = [
|
|||
"tokio-util",
|
||||
"uuid",
|
||||
"webpki-roots 0.26.11",
|
||||
"zip 2.4.2",
|
||||
"zip 4.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -10137,23 +10138,6 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"indexmap 2.14.0",
|
||||
"memchr",
|
||||
"thiserror 2.0.18",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
|
|
|
|||
|
|
@ -273,6 +273,34 @@ function chooseWebOfflineZip(): Promise<File | null> {
|
|||
});
|
||||
}
|
||||
|
||||
function chooseWebFiles(accept: string, multiple: boolean): Promise<File[] | null> {
|
||||
return new Promise((resolve) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = accept;
|
||||
input.multiple = multiple;
|
||||
input.onchange = () => {
|
||||
const files = input.files;
|
||||
if (!files || files.length === 0) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
resolve(Array.from(files));
|
||||
};
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
function chooseWebFile(accept: string): Promise<File | null> {
|
||||
return new Promise((resolve) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = accept;
|
||||
input.onchange = () => resolve(input.files?.[0] ?? null);
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
async function importOfflineZip() {
|
||||
if (importingZip.value) return;
|
||||
let selected: string | File | null = null;
|
||||
|
|
@ -303,7 +331,19 @@ async function importOfflineZip() {
|
|||
}
|
||||
|
||||
async function importDriverJar(dbType: string) {
|
||||
if (isWeb) return;
|
||||
const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType;
|
||||
if (isWeb) {
|
||||
const file = await chooseWebFile(".jar");
|
||||
if (!file) return;
|
||||
try {
|
||||
await api.importAgentJar(dbType, file);
|
||||
await refreshAgents();
|
||||
toast(`${label} 驱动导入成功`);
|
||||
} catch (e: any) {
|
||||
toast(`${label} 驱动导入失败: ${e}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await open({
|
||||
title: "选择驱动 JAR 文件",
|
||||
|
|
@ -311,7 +351,6 @@ async function importDriverJar(dbType: string) {
|
|||
filters: [{ name: "JAR", extensions: ["jar"] }],
|
||||
});
|
||||
if (typeof selected !== "string") return;
|
||||
const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType;
|
||||
try {
|
||||
await api.importAgentJar(dbType, selected);
|
||||
await refreshAgents();
|
||||
|
|
@ -373,7 +412,6 @@ function jreUsageLabel(key: string) {
|
|||
}
|
||||
|
||||
async function loadJdbcDrivers() {
|
||||
if (isWeb) return;
|
||||
isLoadingJdbcDrivers.value = true;
|
||||
try {
|
||||
jdbcDrivers.value = await api.listJdbcDrivers();
|
||||
|
|
@ -394,7 +432,6 @@ async function loadDriverStoreUsage() {
|
|||
}
|
||||
|
||||
async function loadJdbcPluginStatus() {
|
||||
if (isWeb) return;
|
||||
try {
|
||||
jdbcPluginStatus.value = await api.jdbcPluginStatus();
|
||||
emitDriverUpdateCount();
|
||||
|
|
@ -404,7 +441,7 @@ async function loadJdbcPluginStatus() {
|
|||
}
|
||||
|
||||
async function installJdbcPlugin() {
|
||||
if (isWeb || isInstallingJdbcPlugin.value) return;
|
||||
if (isInstallingJdbcPlugin.value) return;
|
||||
isInstallingJdbcPlugin.value = true;
|
||||
try {
|
||||
jdbcPluginStatus.value = await api.installJdbcPlugin();
|
||||
|
|
@ -419,14 +456,20 @@ async function installJdbcPlugin() {
|
|||
}
|
||||
|
||||
async function installJdbcPluginLocal() {
|
||||
if (isWeb || isInstallingJdbcPlugin.value) return;
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await open({
|
||||
title: "选择 JDBC 插件 zip 文件",
|
||||
multiple: false,
|
||||
filters: [{ name: "ZIP", extensions: ["zip"] }],
|
||||
});
|
||||
if (typeof selected !== "string") return;
|
||||
if (isInstallingJdbcPlugin.value) return;
|
||||
let selected: string | File | null = null;
|
||||
if (isWeb) {
|
||||
selected = await chooseWebFile(".zip");
|
||||
} else {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const result = await open({
|
||||
title: "选择 JDBC 插件 zip 文件",
|
||||
multiple: false,
|
||||
filters: [{ name: "ZIP", extensions: ["zip"] }],
|
||||
});
|
||||
selected = typeof result === "string" ? result : null;
|
||||
}
|
||||
if (!selected) return;
|
||||
isInstallingJdbcPlugin.value = true;
|
||||
try {
|
||||
jdbcPluginStatus.value = await api.installJdbcPluginLocal(selected);
|
||||
|
|
@ -441,7 +484,7 @@ async function installJdbcPluginLocal() {
|
|||
}
|
||||
|
||||
async function uninstallJdbcPlugin() {
|
||||
if (isWeb || isUninstallingJdbcPlugin.value) return;
|
||||
if (isUninstallingJdbcPlugin.value) return;
|
||||
isUninstallingJdbcPlugin.value = true;
|
||||
try {
|
||||
jdbcPluginStatus.value = await api.uninstallJdbcPlugin();
|
||||
|
|
@ -468,7 +511,18 @@ async function importJdbcDriverPaths(paths: string[]) {
|
|||
}
|
||||
|
||||
async function importJdbcDrivers() {
|
||||
if (isWeb) return;
|
||||
if (isWeb) {
|
||||
const files = await chooseWebFiles(".jar", true);
|
||||
if (!files || !files.length) return;
|
||||
try {
|
||||
jdbcDrivers.value = await api.importJdbcDrivers(files);
|
||||
void loadDriverStoreUsage();
|
||||
toast(t("settings.jdbcImportSuccess", { count: files.length }));
|
||||
} catch (e: any) {
|
||||
toast(String(e?.message || e), 5000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await open({
|
||||
title: t("settings.jdbcImport"),
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ function onToolbarDblClick(e: MouseEvent) {
|
|||
<template>
|
||||
<div
|
||||
class="h-10 flex items-center gap-1 px-2 border-b bg-muted/30 shrink-0"
|
||||
:class="{ 'pl-17.5': shouldReserveMacTrafficLightInset(isMac, isFullscreen) }"
|
||||
:class="{ 'pl-17.5': shouldReserveMacTrafficLightInset(isMac, isFullscreen, isDesktop) }"
|
||||
data-tauri-drag-region
|
||||
@dblclick="onToolbarDblClick"
|
||||
>
|
||||
|
|
@ -147,7 +147,6 @@ function onToolbarDblClick(e: MouseEvent) {
|
|||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="isDesktop"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 px-2 text-xs gap-1"
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { ref, onMounted, onUnmounted } from "vue";
|
|||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { isMacOS } from "@/lib/platform";
|
||||
|
||||
export function shouldReserveMacTrafficLightInset(isMac: boolean, isFullscreen: boolean): boolean {
|
||||
return isMac && !isFullscreen;
|
||||
export function shouldReserveMacTrafficLightInset(isMac: boolean, isFullscreen: boolean, isDesktop: boolean): boolean {
|
||||
return isDesktop && isMac && !isFullscreen;
|
||||
}
|
||||
|
||||
export function useWindowControls() {
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ export async function loadConnections(): Promise<ConnectionConfig[]> {
|
|||
}
|
||||
|
||||
export async function listSystemFonts(): Promise<string[]> {
|
||||
return [];
|
||||
return get("/api/system/fonts");
|
||||
}
|
||||
|
||||
export async function listPlugins(): Promise<InstalledPlugin[]> {
|
||||
|
|
@ -159,40 +159,57 @@ export async function listPlugins(): Promise<InstalledPlugin[]> {
|
|||
}
|
||||
|
||||
export async function listJdbcDrivers(): Promise<JdbcDriverInfo[]> {
|
||||
return [];
|
||||
return get("/api/jdbc/drivers");
|
||||
}
|
||||
|
||||
export async function importJdbcDrivers(_paths: string[]): Promise<JdbcDriverInfo[]> {
|
||||
return [];
|
||||
export async function importJdbcDrivers(pathsOrFiles: (string | File)[]): Promise<JdbcDriverInfo[]> {
|
||||
const formData = new FormData();
|
||||
for (const item of pathsOrFiles) {
|
||||
if (item instanceof File) {
|
||||
formData.append("files", item, item.name);
|
||||
} else {
|
||||
const fileName = item.split("/").pop() || "driver.jar";
|
||||
const blob = await (await fetch(item)).blob();
|
||||
formData.append("files", blob, fileName);
|
||||
}
|
||||
}
|
||||
const res = await fetch("/api/jdbc/drivers", { method: "POST", body: formData });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function deleteJdbcDriver(_path: string): Promise<JdbcDriverInfo[]> {
|
||||
return [];
|
||||
export async function deleteJdbcDriver(path: string): Promise<JdbcDriverInfo[]> {
|
||||
const fileName = path.split("/").pop() || path;
|
||||
return del(`/api/jdbc/drivers/${encodeURIComponent(fileName)}`);
|
||||
}
|
||||
|
||||
export async function jdbcPluginStatus(): Promise<JdbcPluginStatus> {
|
||||
return {
|
||||
installed: false,
|
||||
version: null,
|
||||
protocol_version: null,
|
||||
compatible: true,
|
||||
latest_version: null,
|
||||
latest_protocol_version: null,
|
||||
update_available: false,
|
||||
path: "",
|
||||
};
|
||||
return get("/api/jdbc/plugin/status");
|
||||
}
|
||||
|
||||
export async function installJdbcPlugin(): Promise<JdbcPluginStatus> {
|
||||
return jdbcPluginStatus();
|
||||
return post("/api/jdbc/plugin/install", {});
|
||||
}
|
||||
|
||||
export async function installJdbcPluginLocal(_path: string): Promise<JdbcPluginStatus> {
|
||||
return jdbcPluginStatus();
|
||||
export async function installJdbcPluginLocal(pathOrFile: string | File): Promise<JdbcPluginStatus> {
|
||||
let blob: Blob;
|
||||
let fileName: string;
|
||||
if (pathOrFile instanceof File) {
|
||||
blob = pathOrFile;
|
||||
fileName = pathOrFile.name;
|
||||
} else {
|
||||
fileName = pathOrFile.split("/").pop() || "plugin.zip";
|
||||
blob = await (await fetch(pathOrFile)).blob();
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append("file", blob, fileName);
|
||||
const uploadRes = await fetch("/api/jdbc/plugin/install-local", { method: "POST", body: formData });
|
||||
if (!uploadRes.ok) throw new Error(await uploadRes.text());
|
||||
return uploadRes.json();
|
||||
}
|
||||
|
||||
export async function uninstallJdbcPlugin(): Promise<JdbcPluginStatus> {
|
||||
return jdbcPluginStatus();
|
||||
return post("/api/jdbc/plugin/uninstall", {});
|
||||
}
|
||||
|
||||
export async function listInstalledAgentsLocal(): Promise<AgentDriverInfo[]> {
|
||||
|
|
@ -244,8 +261,21 @@ export async function importAgentsFromZip(fileOrPath: string | File): Promise<nu
|
|||
return result.count;
|
||||
}
|
||||
|
||||
export async function importAgentJar(_dbType: string, _path: string): Promise<void> {
|
||||
throw new Error("Local JAR import is only available in the desktop app");
|
||||
export async function importAgentJar(dbType: string, pathOrFile: string | File): Promise<void> {
|
||||
let blob: Blob;
|
||||
let fileName: string;
|
||||
if (pathOrFile instanceof File) {
|
||||
blob = pathOrFile;
|
||||
fileName = pathOrFile.name;
|
||||
} else {
|
||||
fileName = pathOrFile.split("/").pop() || "driver.jar";
|
||||
blob = await (await fetch(pathOrFile)).blob();
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append("dbType", dbType);
|
||||
formData.append("file", blob, fileName);
|
||||
const uploadRes = await fetch("/api/agents/import-jar", { method: "POST", body: formData });
|
||||
if (!uploadRes.ok) throw new Error(await uploadRes.text());
|
||||
}
|
||||
|
||||
export async function reinstallJre(jreKey?: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -41,4 +41,5 @@ calamine = "0.30.1"
|
|||
base64 = "0.22"
|
||||
async-trait = "0.1"
|
||||
bytes = "1"
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
font-kit = "0.14.3"
|
||||
zip = { version = "4", default-features = false, features = ["deflate"] }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,341 @@
|
|||
use crate::plugins::{PluginManifest, SUPPORTED_PLUGIN_PROTOCOL_VERSION};
|
||||
use crate::update::{fetch_latest_release, is_newer_version, JdbcPluginLatest};
|
||||
use serde::Serialize;
|
||||
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";
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct JdbcDriverInfo {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
// ---- 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 import_jdbc_drivers(plugins_root: &Path, paths: &[String]) -> Result<Vec<JdbcDriverInfo>, String> {
|
||||
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 = unique_target_path(&drivers_dir, file_name);
|
||||
if source == target {
|
||||
continue;
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// ---- 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> {
|
||||
let bytes = download_jdbc_plugin_zip().await?;
|
||||
let plugin_dir = plugins_root.join("jdbc");
|
||||
install_jdbc_plugin_zip(&bytes, &plugin_dir)?;
|
||||
jdbc_plugin_status_from_dir(&plugin_dir).await
|
||||
}
|
||||
|
||||
pub async fn install_jdbc_plugin_from_file(plugins_root: &Path, file_path: &str) -> Result<JdbcPluginStatus, String> {
|
||||
let bytes = std::fs::read(file_path).map_err(|e| format!("Failed to read file: {e}"))?;
|
||||
let plugin_dir = plugins_root.join("jdbc");
|
||||
install_jdbc_plugin_zip(&bytes, &plugin_dir)?;
|
||||
jdbc_plugin_status_from_dir(&plugin_dir).await
|
||||
}
|
||||
|
||||
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())?;
|
||||
}
|
||||
}
|
||||
// synchronous version: check local manifest only, no network call
|
||||
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 ----
|
||||
|
||||
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![],
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Internal helpers ----
|
||||
|
||||
fn jdbc_drivers_dir(plugins_root: &Path) -> PathBuf {
|
||||
plugins_root.join("jdbc").join("drivers")
|
||||
}
|
||||
|
||||
async fn jdbc_plugin_status_from_dir(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()),
|
||||
};
|
||||
let latest = latest_jdbc_plugin().await;
|
||||
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> {
|
||||
fetch_latest_release().await.ok().and_then(|release| release.jdbc_plugin)
|
||||
}
|
||||
|
||||
async fn download_jdbc_plugin_zip() -> Result<Vec<u8>, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.build()
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let 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 bytes = resp.bytes().await.map_err(|err| err.to_string())?;
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
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("Downloaded JDBC plugin package is missing manifest.json".to_string());
|
||||
}
|
||||
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;
|
||||
let executable = plugin_dir.join("bin").join("dbx-jdbc-plugin");
|
||||
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 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.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(),
|
||||
});
|
||||
}
|
||||
drivers.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(drivers)
|
||||
}
|
||||
|
||||
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!()
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ pub mod db;
|
|||
pub mod db_admin_sql;
|
||||
pub mod external;
|
||||
pub mod history;
|
||||
pub mod jdbc;
|
||||
pub mod models;
|
||||
pub mod mongo_ops;
|
||||
pub mod object_source_sql;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,16 @@ use axum::response::{IntoResponse, Response};
|
|||
|
||||
pub struct AppError(pub String);
|
||||
|
||||
impl AppError {
|
||||
pub fn internal(msg: impl Into<String>) -> Self {
|
||||
AppError(msg.into())
|
||||
}
|
||||
|
||||
pub fn bad_request(msg: impl Into<String>) -> Self {
|
||||
AppError(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, self.0).into_response()
|
||||
|
|
|
|||
|
|
@ -86,6 +86,15 @@ async fn main() {
|
|||
.route("/connection/save", post(routes::connection::save_connections))
|
||||
.route("/connection/list", get(routes::connection::load_connections))
|
||||
.route("/plugins", get(routes::plugins::list_plugins))
|
||||
// JDBC
|
||||
.route("/jdbc/drivers", get(routes::jdbc::list_jdbc_drivers).post(routes::jdbc::import_jdbc_drivers))
|
||||
.route("/jdbc/drivers/{name}", delete(routes::jdbc::delete_jdbc_driver))
|
||||
.route("/jdbc/plugin/status", get(routes::jdbc::get_jdbc_plugin_status))
|
||||
.route("/jdbc/plugin/install", post(routes::jdbc::install_jdbc_plugin))
|
||||
.route("/jdbc/plugin/install-local", post(routes::jdbc::install_jdbc_plugin_local))
|
||||
.route("/jdbc/plugin/uninstall", post(routes::jdbc::uninstall_jdbc_plugin))
|
||||
// System
|
||||
.route("/system/fonts", get(routes::jdbc::list_system_fonts))
|
||||
// Agent drivers
|
||||
.route("/agents/installed-local", get(routes::agents::list_installed_agents_local))
|
||||
.route("/agents/installed", get(routes::agents::list_installed_agents))
|
||||
|
|
@ -94,6 +103,7 @@ async fn main() {
|
|||
.route("/agents/upgrade-all", post(routes::agents::upgrade_all_agents))
|
||||
.route("/agents/uninstall", post(routes::agents::uninstall_agent))
|
||||
.route("/agents/import-offline", post(routes::agents::import_agents_from_zip))
|
||||
.route("/agents/import-jar", post(routes::agents::import_agent_jar))
|
||||
.route(
|
||||
"/agents/java-runtime",
|
||||
get(routes::agents::get_agent_java_runtime_config).post(routes::agents::set_agent_java_runtime_config),
|
||||
|
|
|
|||
|
|
@ -174,6 +174,40 @@ pub async fn import_agents_from_zip(
|
|||
Err(AppError("No file uploaded".to_string()))
|
||||
}
|
||||
|
||||
pub async fn import_agent_jar(
|
||||
State(state): State<Arc<WebState>>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let mut db_type: Option<String> = None;
|
||||
let mut jar_data: Option<Vec<u8>> = None;
|
||||
let mut jar_name = String::new();
|
||||
|
||||
while let Ok(Some(field)) = multipart.next_field().await {
|
||||
let name = field.name().unwrap_or("").to_string();
|
||||
if name == "dbType" {
|
||||
db_type = Some(field.text().await.map_err(|e| AppError(e.to_string()))?);
|
||||
} else if name == "file" {
|
||||
jar_name = field.file_name().unwrap_or("driver.jar").to_string();
|
||||
if !jar_name.to_lowercase().ends_with(".jar") {
|
||||
return Err(AppError("Only .jar files can be imported".to_string()));
|
||||
}
|
||||
jar_data = Some(field.bytes().await.map_err(|e| AppError(e.to_string()))?.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
let db_type = db_type.ok_or_else(|| AppError("Missing dbType field".to_string()))?;
|
||||
let data = jar_data.ok_or_else(|| AppError("No file uploaded".to_string()))?;
|
||||
|
||||
let temp_dir = state.app.plugins.root_dir().join("jar_upload_tmp");
|
||||
std::fs::create_dir_all(&temp_dir).map_err(|e| AppError(e.to_string()))?;
|
||||
let tmp_path = temp_dir.join(&jar_name);
|
||||
std::fs::write(&tmp_path, &data).map_err(|e| AppError(e.to_string()))?;
|
||||
|
||||
dbx_core::agent_service::import_agent_jar(&state.app.agent_manager, &db_type, &tmp_path).map_err(AppError::from)?;
|
||||
let _ = std::fs::remove_file(&tmp_path);
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
pub async fn reinstall_jre(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<JreRequest>,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::{Multipart, Path, State};
|
||||
use axum::Json;
|
||||
use dbx_core::jdbc::{self, JdbcDriverInfo, JdbcPluginStatus};
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::state::WebState;
|
||||
|
||||
// ---- JDBC Drivers ----
|
||||
|
||||
pub async fn list_jdbc_drivers(State(state): State<Arc<WebState>>) -> Result<Json<Vec<JdbcDriverInfo>>, AppError> {
|
||||
let root = state.app.plugins.root_dir();
|
||||
Ok(Json(jdbc::list_jdbc_drivers(&root).map_err(AppError::internal)?))
|
||||
}
|
||||
|
||||
pub async fn import_jdbc_drivers(
|
||||
State(state): State<Arc<WebState>>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<Vec<JdbcDriverInfo>>, AppError> {
|
||||
let root = state.app.plugins.root_dir();
|
||||
let drivers_dir = root.join("jdbc").join("drivers");
|
||||
std::fs::create_dir_all(&drivers_dir).map_err(|e| AppError::internal(e.to_string()))?;
|
||||
|
||||
let mut imported = Vec::new();
|
||||
while let Ok(Some(field)) = multipart.next_field().await {
|
||||
let file_name = field.file_name().unwrap_or("driver.jar").to_string();
|
||||
if !file_name.to_lowercase().ends_with(".jar") {
|
||||
return Err(AppError::bad_request("Only .jar files can be imported"));
|
||||
}
|
||||
let data = field.bytes().await.map_err(|e| AppError::internal(e.to_string()))?;
|
||||
let target = jdbc::unique_target_path(&drivers_dir, &file_name);
|
||||
std::fs::write(&target, &data).map_err(|e| AppError::internal(e.to_string()))?;
|
||||
imported.push(target);
|
||||
}
|
||||
|
||||
jdbc::list_jdbc_drivers(&root).map(Json).map_err(|e| AppError::internal(e))
|
||||
}
|
||||
|
||||
pub async fn delete_jdbc_driver(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Path(name): Path<String>,
|
||||
) -> Result<Json<Vec<JdbcDriverInfo>>, AppError> {
|
||||
if name.contains("..") || name.contains('/') || name.contains('\\') {
|
||||
return Err(AppError::bad_request("Invalid driver name"));
|
||||
}
|
||||
let root = state.app.plugins.root_dir();
|
||||
let driver_path = root.join("jdbc").join("drivers").join(&name);
|
||||
let path_str = driver_path.to_string_lossy().to_string();
|
||||
Ok(Json(jdbc::delete_jdbc_driver(&root, &path_str).map_err(AppError::internal)?))
|
||||
}
|
||||
|
||||
// ---- JDBC Plugin ----
|
||||
|
||||
pub async fn get_jdbc_plugin_status(State(state): State<Arc<WebState>>) -> Result<Json<JdbcPluginStatus>, AppError> {
|
||||
let root = state.app.plugins.root_dir();
|
||||
Ok(Json(jdbc::get_jdbc_plugin_status(&root).await.map_err(AppError::internal)?))
|
||||
}
|
||||
|
||||
pub async fn install_jdbc_plugin(State(state): State<Arc<WebState>>) -> Result<Json<JdbcPluginStatus>, AppError> {
|
||||
let root = state.app.plugins.root_dir();
|
||||
Ok(Json(jdbc::install_jdbc_plugin(&root).await.map_err(AppError::internal)?))
|
||||
}
|
||||
|
||||
pub async fn install_jdbc_plugin_local(
|
||||
State(state): State<Arc<WebState>>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<JdbcPluginStatus>, AppError> {
|
||||
let root = state.app.plugins.root_dir();
|
||||
let temp_dir = root.join("jdbc").with_extension("upload_tmp");
|
||||
std::fs::create_dir_all(&temp_dir).map_err(|e| AppError::internal(e.to_string()))?;
|
||||
|
||||
while let Ok(Some(field)) = multipart.next_field().await {
|
||||
let file_name = field.file_name().unwrap_or("plugin.zip").to_string();
|
||||
if !file_name.to_lowercase().ends_with(".zip") {
|
||||
return Err(AppError::bad_request("Only .zip files can be imported for JDBC plugin"));
|
||||
}
|
||||
let data = field.bytes().await.map_err(|e| AppError::internal(e.to_string()))?;
|
||||
let tmp_path = temp_dir.join(&file_name);
|
||||
std::fs::write(&tmp_path, &data).map_err(|e| AppError::internal(e.to_string()))?;
|
||||
let result = jdbc::install_jdbc_plugin_from_file(&root, &tmp_path.to_string_lossy())
|
||||
.await
|
||||
.map_err(AppError::internal)?;
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
return Ok(Json(result));
|
||||
}
|
||||
|
||||
Err(AppError::bad_request("No file uploaded"))
|
||||
}
|
||||
|
||||
pub async fn uninstall_jdbc_plugin(State(state): State<Arc<WebState>>) -> Result<Json<JdbcPluginStatus>, AppError> {
|
||||
let root = state.app.plugins.root_dir();
|
||||
Ok(Json(jdbc::uninstall_jdbc_plugin(&root).map_err(AppError::internal)?))
|
||||
}
|
||||
|
||||
// ---- System Fonts ----
|
||||
|
||||
pub async fn list_system_fonts() -> Result<Json<Vec<String>>, AppError> {
|
||||
Ok(Json(jdbc::list_system_fonts()))
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ pub mod connection;
|
|||
pub mod data_compare;
|
||||
pub mod database_export;
|
||||
pub mod history;
|
||||
pub mod jdbc;
|
||||
pub mod layout;
|
||||
pub mod mongo;
|
||||
pub mod plugins;
|
||||
|
|
|
|||
|
|
@ -1,51 +1,24 @@
|
|||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
use dbx_core::plugins::{InstalledPlugin, PluginManifest, SUPPORTED_PLUGIN_PROTOCOL_VERSION};
|
||||
use dbx_core::update::{fetch_latest_release, is_newer_version, JdbcPluginLatest};
|
||||
use serde::Serialize;
|
||||
use dbx_core::jdbc::{self, JdbcDriverInfo, JdbcPluginStatus};
|
||||
use dbx_core::plugins::InstalledPlugin;
|
||||
|
||||
use super::connection::AppState;
|
||||
|
||||
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";
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_plugins(state: State<'_, Arc<AppState>>) -> Result<Vec<InstalledPlugin>, String> {
|
||||
state.plugins.list_installed()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct JdbcDriverInfo {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn jdbc_plugin_status(state: State<'_, Arc<AppState>>) -> Result<JdbcPluginStatus, String> {
|
||||
jdbc_plugin_status_from_state(&state).await
|
||||
jdbc::get_jdbc_plugin_status(&state.plugins.root_dir()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_jdbc_plugin(state: State<'_, Arc<AppState>>) -> Result<JdbcPluginStatus, String> {
|
||||
let bytes = download_jdbc_plugin_zip().await?;
|
||||
let plugin_dir = state.plugins.root_dir().join("jdbc");
|
||||
install_jdbc_plugin_zip(&bytes, &plugin_dir)?;
|
||||
jdbc_plugin_status_from_state(&state).await
|
||||
jdbc::install_jdbc_plugin(&state.plugins.root_dir()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -53,32 +26,17 @@ pub async fn install_jdbc_plugin_local(
|
|||
state: State<'_, Arc<AppState>>,
|
||||
path: String,
|
||||
) -> Result<JdbcPluginStatus, String> {
|
||||
let bytes = std::fs::read(&path).map_err(|e| format!("Failed to read file: {e}"))?;
|
||||
let plugin_dir = state.plugins.root_dir().join("jdbc");
|
||||
install_jdbc_plugin_zip(&bytes, &plugin_dir)?;
|
||||
jdbc_plugin_status_from_state(&state).await
|
||||
jdbc::install_jdbc_plugin_from_file(&state.plugins.root_dir(), &path).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_jdbc_plugin(state: State<'_, Arc<AppState>>) -> Result<JdbcPluginStatus, String> {
|
||||
let plugin_dir = state.plugins.root_dir().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_state(&state).await
|
||||
jdbc::uninstall_jdbc_plugin(&state.plugins.root_dir())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_jdbc_drivers(state: State<'_, Arc<AppState>>) -> Result<Vec<JdbcDriverInfo>, String> {
|
||||
list_jdbc_drivers_from_dir(&jdbc_drivers_dir(&state))
|
||||
jdbc::list_jdbc_drivers(&state.plugins.root_dir())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -86,240 +44,10 @@ pub async fn import_jdbc_drivers(
|
|||
state: State<'_, Arc<AppState>>,
|
||||
paths: Vec<String>,
|
||||
) -> Result<Vec<JdbcDriverInfo>, String> {
|
||||
let drivers_dir = jdbc_drivers_dir(&state);
|
||||
std::fs::create_dir_all(&drivers_dir).map_err(|err| err.to_string())?;
|
||||
|
||||
for path in paths {
|
||||
let source = std::path::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 = unique_target_path(&drivers_dir, file_name);
|
||||
if source == target {
|
||||
continue;
|
||||
}
|
||||
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)
|
||||
jdbc::import_jdbc_drivers(&state.plugins.root_dir(), &paths)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_jdbc_driver(state: State<'_, Arc<AppState>>, path: String) -> Result<Vec<JdbcDriverInfo>, String> {
|
||||
let drivers_dir = jdbc_drivers_dir(&state);
|
||||
let drivers_dir = drivers_dir.canonicalize().map_err(|err| err.to_string())?;
|
||||
let target = std::path::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)
|
||||
}
|
||||
|
||||
fn jdbc_drivers_dir(state: &AppState) -> std::path::PathBuf {
|
||||
state.plugins.root_dir().join("jdbc").join("drivers")
|
||||
}
|
||||
|
||||
async fn jdbc_plugin_status_from_state(state: &AppState) -> Result<JdbcPluginStatus, String> {
|
||||
let plugin_dir = state.plugins.root_dir().join("jdbc");
|
||||
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()),
|
||||
};
|
||||
let version =
|
||||
manifest.as_ref().and_then(|manifest| (!manifest.version.is_empty()).then_some(manifest.version.clone()));
|
||||
let protocol_version = manifest.as_ref().map(|manifest| manifest.protocol_version);
|
||||
let compatible = match manifest.as_ref() {
|
||||
Some(manifest) => manifest.protocol_version == SUPPORTED_PLUGIN_PROTOCOL_VERSION,
|
||||
None => true,
|
||||
};
|
||||
let latest = latest_jdbc_plugin().await;
|
||||
let latest_version = latest.as_ref().map(|plugin| plugin.version.clone());
|
||||
let latest_protocol_version = latest.as_ref().map(|plugin| plugin.protocol_version);
|
||||
let update_available = match (version.as_deref(), latest.as_ref()) {
|
||||
(Some(current), Some(latest)) if manifest.is_some() => is_newer_version(&latest.version, current),
|
||||
(None, Some(_)) if manifest.is_some() => true,
|
||||
_ => false,
|
||||
};
|
||||
Ok(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> {
|
||||
fetch_latest_release().await.ok().and_then(|release| release.jdbc_plugin)
|
||||
}
|
||||
|
||||
async fn download_jdbc_plugin_zip() -> Result<Vec<u8>, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.build()
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let resp =
|
||||
dbx_core::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 bytes = resp.bytes().await.map_err(|err| err.to_string())?;
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
fn install_jdbc_plugin_zip(bytes: &[u8], plugin_dir: &std::path::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("Downloaded JDBC plugin package is missing manifest.json".to_string());
|
||||
}
|
||||
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;
|
||||
let executable = plugin_dir.join("bin").join("dbx-jdbc-plugin");
|
||||
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: &std::path::Path) -> std::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: &std::path::Path, target: &std::path::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 list_jdbc_drivers_from_dir(drivers_dir: &std::path::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.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(),
|
||||
});
|
||||
}
|
||||
drivers.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(drivers)
|
||||
}
|
||||
|
||||
fn unique_target_path(dir: &std::path::Path, file_name: &str) -> std::path::PathBuf {
|
||||
let target = dir.join(file_name);
|
||||
if !target.exists() {
|
||||
return target;
|
||||
}
|
||||
|
||||
let path = std::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!()
|
||||
jdbc::delete_jdbc_driver(&state.plugins.root_dir(), &path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,4 @@
|
|||
use std::collections::BTreeSet;
|
||||
|
||||
use font_kit::source::SystemSource;
|
||||
|
||||
pub fn normalize_font_families(families: Vec<String>) -> Vec<String> {
|
||||
families
|
||||
.into_iter()
|
||||
.map(|family| family.trim().to_string())
|
||||
.filter(|family| !family.is_empty())
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_system_fonts() -> Result<Vec<String>, String> {
|
||||
SystemSource::new().all_families().map(normalize_font_families).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::normalize_font_families;
|
||||
|
||||
#[test]
|
||||
fn normalizes_and_sorts_font_family_names() {
|
||||
assert_eq!(
|
||||
normalize_font_families(vec![
|
||||
"Maple Mono NF CN".to_string(),
|
||||
" ".to_string(),
|
||||
"Arial".to_string(),
|
||||
"Maple Mono NF CN".to_string(),
|
||||
]),
|
||||
vec!["Arial".to_string(), "Maple Mono NF CN".to_string()],
|
||||
);
|
||||
}
|
||||
Ok(dbx_core::jdbc::list_system_fonts())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue