diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue index 4f2194cdd..4a2d35fc3 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -45,8 +45,12 @@ import { useSettingsStore } from "@/stores/settingsStore"; import type { DriverStorePathInfo } from "@/lib/api"; const settingsStore = useSettingsStore(); -const driverStoreDir = computed(() => settingsStore.desktopSettings.driver_store_dir ?? null); -const driverStoreDirMigrating = ref(false); +type DriverStoreDirKind = "plugin" | "agent"; + +const legacyDriverStoreDir = computed(() => settingsStore.desktopSettings.driver_store_dir ?? null); +const pluginStoreDir = computed(() => settingsStore.desktopSettings.plugin_store_dir ?? null); +const agentStoreDir = computed(() => settingsStore.desktopSettings.agent_store_dir ?? null); +const driverStoreDirMigrating = ref(null); const currentDriverStorePath = ref(null); async function loadDriverStorePath() { @@ -58,49 +62,82 @@ async function loadDriverStorePath() { } } -const driverStoreDirDisplay = computed(() => { - if (currentDriverStorePath.value) { - return driverStoreDir.value || currentDriverStorePath.value.plugins_dir; +function configuredDriverStoreDir(kind: DriverStoreDirKind): string | null { + if (kind === "plugin") { + return pluginStoreDir.value ?? (legacyDriverStoreDir.value ? `${legacyDriverStoreDir.value}/plugins` : null); } - return driverStoreDir.value || t("driverStore.driverStoreDirDefault"); -}); + return agentStoreDir.value ?? (legacyDriverStoreDir.value ? `${legacyDriverStoreDir.value}/agents` : null); +} -async function chooseDriverStoreDir() { +function actualDriverStoreDir(kind: DriverStoreDirKind): string | null { + if (!currentDriverStorePath.value) return null; + return kind === "plugin" ? currentDriverStorePath.value.plugins_dir : currentDriverStorePath.value.agents_dir; +} + +function driverStoreDirDisplay(kind: DriverStoreDirKind): string { + return actualDriverStoreDir(kind) ?? configuredDriverStoreDir(kind) ?? t("driverStore.driverStoreDirDefault"); +} + +function driverStoreTargetLabel(kind: DriverStoreDirKind): string { + return kind === "plugin" ? t("driverStore.pluginStoreDir") : t("driverStore.agentStoreDir"); +} + +const driverStorePathRows = computed(() => [ + { + kind: "plugin" as const, + label: t("driverStore.pluginStoreDir"), + description: t("driverStore.pluginStoreDirDescription"), + display: driverStoreDirDisplay("plugin"), + custom: Boolean(pluginStoreDir.value || legacyDriverStoreDir.value), + }, + { + kind: "agent" as const, + label: t("driverStore.agentStoreDir"), + description: t("driverStore.agentStoreDirDescription"), + display: driverStoreDirDisplay("agent"), + custom: Boolean(agentStoreDir.value || legacyDriverStoreDir.value), + }, +]); + +async function chooseDriverStoreDir(kind: DriverStoreDirKind) { if (isWeb || driverStoreDirMigrating.value) return; const { open } = await import("@tauri-apps/plugin-dialog"); const selected = await open({ - title: t("driverStore.driverStoreDirDialogTitle"), + title: t("driverStore.driverStoreDirDialogTitle", { target: driverStoreTargetLabel(kind) }), directory: true, multiple: false, }); if (typeof selected === "string") { - await applyDriverStoreDir(selected); + await applyDriverStoreDir(kind, selected); } } -async function resetDriverStoreDir() { +async function resetDriverStoreDir(kind: DriverStoreDirKind) { if (driverStoreDirMigrating.value) return; - await applyDriverStoreDir(null); + await applyDriverStoreDir(kind, null); } -async function applyDriverStoreDir(newDir: string | null) { +async function applyDriverStoreDir(kind: DriverStoreDirKind, newDir: string | null) { if (driverStoreDirMigrating.value) return; - const confirmed = window.confirm(t("driverStore.driverStoreDirConfirm")); + const target = driverStoreTargetLabel(kind); + const confirmed = window.confirm(t("driverStore.driverStoreDirConfirm", { target })); if (!confirmed) return; - driverStoreDirMigrating.value = true; + driverStoreDirMigrating.value = kind; try { - const result = await api.setDriverStoreDir(newDir); + const result = kind === "plugin" ? await api.setPluginStoreDir(newDir) : await api.setAgentStoreDir(newDir); settingsStore.desktopSettings.driver_store_dir = result.driver_store_dir; - toast(t("driverStore.driverStoreDirSuccess")); + settingsStore.desktopSettings.plugin_store_dir = result.plugin_store_dir; + settingsStore.desktopSettings.agent_store_dir = result.agent_store_dir; + toast(t("driverStore.driverStoreDirSuccess", { target })); // Restart the app to use the new paths const { relaunch } = await import("@tauri-apps/plugin-process"); relaunch(); } catch (e: any) { toast(t("driverStore.driverStoreDirMigrationFailed", { error: e?.message || String(e) }), 5000); } finally { - driverStoreDirMigrating.value = false; + driverStoreDirMigrating.value = null; } } @@ -1226,28 +1263,39 @@ watch(driverStoreTab, (tab) => {
{{ t("driverStore.driverStoreDir") }}

{{ t("driverStore.driverStoreDirDescription") }}

-
- - -
- {{ driverStoreDirDisplay }} +
+
+
+
+ {{ row.label }} + {{ row.description }}
- - - {{ driverStoreDirDisplay }} - - - - + +
+
+ + +
+ {{ row.display }} +
+
+ + {{ row.display }} + +
+ + +
+

- {{ t("driverStore.driverStoreDirMigrating") }} + {{ t("driverStore.driverStoreDirMigrating", { target: driverStoreTargetLabel(driverStoreDirMigrating) }) }}

diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 64c1d57d4..ca8a5bf8a 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -2296,15 +2296,19 @@ runtimeRestartSuccess: "{label} runtime restarted", runtimeRestartFailed: "Failed to restart {label}: {error}", driverStoreDir: "Driver Store Path", - driverStoreDirDescription: "Customize where drivers and JREs are stored. The app will restart automatically after migration.", + driverStoreDirDescription: "Set separate storage locations for the JDBC plugin and Agent/JRE data. The app will restart automatically after migration.", + pluginStoreDir: "JDBC plugin directory", + pluginStoreDirDescription: "Stores the JDBC plugin, Maven resolver, and JDBC driver jars.", + agentStoreDir: "Agent / JRE directory", + agentStoreDirDescription: "Stores Agent driver packages, managed JREs, and runtime cache.", driverStoreDirDefault: "Default (install directory)", driverStoreDirChange: "Change", driverStoreDirReset: "Reset to Default", - driverStoreDirMigrating: "Migrating driver data…", - driverStoreDirConfirm: "Changing the driver store path will migrate all driver data to the new location. Active driver connections will be disconnected during migration. The app will restart automatically after migration. Continue?", + driverStoreDirMigrating: "Migrating {target}…", + driverStoreDirConfirm: "Changing {target} will migrate the related driver data to the new location. Related driver connections may be disconnected during migration. The app will restart automatically after migration. Continue?", driverStoreDirMigrationFailed: "Driver store migration failed: {error}", - driverStoreDirDialogTitle: "Select Driver Store Directory", - driverStoreDirSuccess: "Driver store path changed, restarting…", + driverStoreDirDialogTitle: "Select {target}", + driverStoreDirSuccess: "{target} changed, restarting…", }, databaseExport: { title: "Export Database", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index c4527edd5..31f695da4 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2000,15 +2000,19 @@ runtimeRestartSuccess: "Runtime de {label} reiniciado", runtimeRestartFailed: "Error al reiniciar {label}: {error}", driverStoreDir: "Ruta de almacenamiento de controladores", - driverStoreDirDescription: "Personaliza dónde se almacenan los controladores y JRE. La aplicación se reiniciará automáticamente después de la migración.", + driverStoreDirDescription: "Configura ubicaciones separadas para el plugin JDBC y los datos de Agent/JRE. La aplicación se reiniciará automáticamente después de la migración.", + pluginStoreDir: "Directorio del plugin JDBC", + pluginStoreDirDescription: "Almacena el plugin JDBC, Maven resolver y los jars de controladores JDBC.", + agentStoreDir: "Directorio Agent / JRE", + agentStoreDirDescription: "Almacena paquetes de controladores Agent, JRE gestionados y caché de runtime.", driverStoreDirDefault: "Predeterminado (directorio de instalación)", driverStoreDirChange: "Cambiar", driverStoreDirReset: "Restablecer", - driverStoreDirMigrating: "Migrando datos de controladores…", - driverStoreDirConfirm: "Cambiar la ruta de almacenamiento migrará todos los datos de controladores a la nueva ubicación. Las conexiones activas se desconectarán durante la migración. La aplicación se reiniciará automáticamente después. ¿Continuar?", + driverStoreDirMigrating: "Migrando {target}…", + driverStoreDirConfirm: "Cambiar {target} migrará los datos relacionados a la nueva ubicación. Las conexiones relacionadas pueden desconectarse durante la migración. La aplicación se reiniciará automáticamente después. ¿Continuar?", driverStoreDirMigrationFailed: "Error en la migración de controladores: {error}", - driverStoreDirDialogTitle: "Seleccionar directorio de controladores", - driverStoreDirSuccess: "Ruta de almacenamiento cambiada, reiniciando…", + driverStoreDirDialogTitle: "Seleccionar {target}", + driverStoreDirSuccess: "{target} cambiado, reiniciando…", }, databaseExport: { title: "Exportar base de datos", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 77cf87cf2..04aa0f1ab 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2069,15 +2069,19 @@ runtimeRestartSuccess: "Runtime {label} riavviato", runtimeRestartFailed: "Riavvio di {label} non riuscito: {error}", driverStoreDir: "Percorso archiviazione driver", - driverStoreDirDescription: "Personalizza dove vengono archiviati driver e JRE. L'app si riavvierà automaticamente dopo la migrazione.", + driverStoreDirDescription: "Configura percorsi separati per il plugin JDBC e i dati Agent/JRE. L'app si riavvierà automaticamente dopo la migrazione.", + pluginStoreDir: "Directory plugin JDBC", + pluginStoreDirDescription: "Archivia il plugin JDBC, Maven resolver e i jar dei driver JDBC.", + agentStoreDir: "Directory Agent / JRE", + agentStoreDirDescription: "Archivia pacchetti driver Agent, JRE gestiti e cache runtime.", driverStoreDirDefault: "Predefinito (directory di installazione)", driverStoreDirChange: "Cambia", driverStoreDirReset: "Ripristina", - driverStoreDirMigrating: "Migrazione dati driver in corso…", - driverStoreDirConfirm: "La modifica del percorso di archiviazione migrerà tutti i dati dei driver nella nuova posizione. Le connessioni attive verranno disconnesse durante la migrazione. L'app si riavvierà automaticamente dopo. Continuare?", + driverStoreDirMigrating: "Migrazione {target} in corso…", + driverStoreDirConfirm: "La modifica di {target} migrerà i dati correlati nella nuova posizione. Le connessioni correlate potrebbero essere disconnesse durante la migrazione. L'app si riavvierà automaticamente dopo. Continuare?", driverStoreDirMigrationFailed: "Migrazione driver fallita: {error}", - driverStoreDirDialogTitle: "Seleziona directory driver", - driverStoreDirSuccess: "Percorso cambiato, riavvio in corso…", + driverStoreDirDialogTitle: "Seleziona {target}", + driverStoreDirSuccess: "{target} cambiato, riavvio in corso…", }, databaseExport: { title: "Esporta Database", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index ab0aaae78..4ac8fa406 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2080,15 +2080,19 @@ runtimeRestartSuccess: "Runtime {label} reiniciado", runtimeRestartFailed: "Falha ao reiniciar {label}: {error}", driverStoreDir: "Caminho de armazenamento de drivers", - driverStoreDirDescription: "Personalize onde drivers e JREs são armazenados. O app será reiniciado automaticamente após a migração.", + driverStoreDirDescription: "Configure locais separados para o plugin JDBC e dados de Agent/JRE. O app será reiniciado automaticamente após a migração.", + pluginStoreDir: "Diretório do plugin JDBC", + pluginStoreDirDescription: "Armazena o plugin JDBC, Maven resolver e jars de drivers JDBC.", + agentStoreDir: "Diretório Agent / JRE", + agentStoreDirDescription: "Armazena pacotes de drivers Agent, JREs gerenciados e cache de runtime.", driverStoreDirDefault: "Padrão (diretório de instalação)", driverStoreDirChange: "Alterar", driverStoreDirReset: "Restaurar padrão", - driverStoreDirMigrating: "Migrando dados de drivers…", - driverStoreDirConfirm: "Alterar o caminho de armazenamento migrará todos os dados de drivers para o novo local. Conexões ativas serão desconectadas durante a migração. O app será reiniciado automaticamente após. Continuar?", + driverStoreDirMigrating: "Migrando {target}…", + driverStoreDirConfirm: "Alterar {target} migrará os dados relacionados para o novo local. Conexões relacionadas podem ser desconectadas durante a migração. O app será reiniciado automaticamente após. Continuar?", driverStoreDirMigrationFailed: "Falha na migração de drivers: {error}", - driverStoreDirDialogTitle: "Selecionar diretório de drivers", - driverStoreDirSuccess: "Caminho alterado, reiniciando…", + driverStoreDirDialogTitle: "Selecionar {target}", + driverStoreDirSuccess: "{target} alterado, reiniciando…", }, databaseExport: { title: "Exportar banco de dados", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 4e3983339..f19a5ba9e 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2320,15 +2320,19 @@ runtimeRestartSuccess: "{label} 运行时已重启", runtimeRestartFailed: "重启 {label} 失败: {error}", driverStoreDir: "驱动存储路径", - driverStoreDirDescription: "自定义驱动和 JRE 的存储位置,迁移后应用将自动重启。", + driverStoreDirDescription: "JDBC 插件和 Agent/JRE 可分别指定存储位置,迁移后应用将自动重启。", + pluginStoreDir: "JDBC 插件目录", + pluginStoreDirDescription: "存放 JDBC 插件、Maven resolver 和 JDBC 驱动 jar。", + agentStoreDir: "Agent / JRE 目录", + agentStoreDirDescription: "存放 Agent 驱动包、托管 JRE 和运行时缓存。", driverStoreDirDefault: "默认(安装目录)", driverStoreDirChange: "更改", driverStoreDirReset: "恢复默认", - driverStoreDirMigrating: "正在迁移驱动数据…", - driverStoreDirConfirm: "更改驱动存储路径将迁移所有驱动数据到新位置,迁移期间会断开驱动连接。迁移完成后应用将自动重启。是否继续?", + driverStoreDirMigrating: "正在迁移 {target}…", + driverStoreDirConfirm: "更改 {target} 将迁移对应驱动数据到新位置,迁移期间可能会断开相关驱动连接。迁移完成后应用将自动重启。是否继续?", driverStoreDirMigrationFailed: "驱动存储路径迁移失败:{error}", - driverStoreDirDialogTitle: "选择驱动存储目录", - driverStoreDirSuccess: "驱动存储路径已更改,正在重启…", + driverStoreDirDialogTitle: "选择 {target}", + driverStoreDirSuccess: "{target} 已更改,正在重启…", }, databaseExport: { title: "导出数据库", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 8efac630d..a694dc53e 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2075,15 +2075,19 @@ runtimeRestartSuccess: "{label} 執行環境已重新啟動", runtimeRestartFailed: "重新啟動 {label} 失敗: {error}", driverStoreDir: "驅動儲存路徑", - driverStoreDirDescription: "自訂驅動和 JRE 的儲存位置,遷移後應用將自動重啟。", + driverStoreDirDescription: "JDBC 外掛程式和 Agent/JRE 可分別指定儲存位置,遷移後應用將自動重啟。", + pluginStoreDir: "JDBC 外掛程式目錄", + pluginStoreDirDescription: "存放 JDBC 外掛程式、Maven resolver 和 JDBC 驅動 jar。", + agentStoreDir: "Agent / JRE 目錄", + agentStoreDirDescription: "存放 Agent 驅動包、託管 JRE 和執行環境快取。", driverStoreDirDefault: "預設(安裝目錄)", driverStoreDirChange: "更改", driverStoreDirReset: "恢復預設", - driverStoreDirMigrating: "正在遷移驅動資料…", - driverStoreDirConfirm: "更改驅動儲存路徑將遷移所有驅動資料到新位置,遷移期間會斷開驅動連線。遷移完成後應用將自動重啟。是否繼續?", + driverStoreDirMigrating: "正在遷移 {target}…", + driverStoreDirConfirm: "更改 {target} 將遷移對應驅動資料到新位置,遷移期間可能會斷開相關驅動連線。遷移完成後應用將自動重啟。是否繼續?", driverStoreDirMigrationFailed: "驅動儲存路徑遷移失敗:{error}", - driverStoreDirDialogTitle: "選擇驅動儲存目錄", - driverStoreDirSuccess: "驅動儲存路徑已更改,正在重啟…", + driverStoreDirDialogTitle: "選擇 {target}", + driverStoreDirSuccess: "{target} 已更改,正在重啟…", }, databaseExport: { title: "匯出資料庫", diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index 599848f38..50861a95c 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -188,6 +188,8 @@ export const loadAiConfig = forward("loadAiConfig"); export const loadDesktopSettings = forward("loadDesktopSettings"); export const saveDesktopSettings = forward("saveDesktopSettings"); export const setDriverStoreDir = forward("setDriverStoreDir"); +export const setPluginStoreDir = forward("setPluginStoreDir"); +export const setAgentStoreDir = forward("setAgentStoreDir"); export const getDriverStorePath = forward("getDriverStorePath"); export const loadPinnedTreeNodeIds = forward("loadPinnedTreeNodeIds"); export const savePinnedTreeNodeIds = forward("savePinnedTreeNodeIds"); diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index 54b24a9a8..997556c9e 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -851,7 +851,7 @@ export async function loadAiConfig(): Promise { } export async function loadDesktopSettings(): Promise { - return { show_tray_icon: true, icon_theme: "default", debug_logging_enabled: false, saved_sql_sync_dir: null, driver_store_dir: null }; + return { show_tray_icon: true, icon_theme: "default", debug_logging_enabled: false, saved_sql_sync_dir: null, driver_store_dir: null, plugin_store_dir: null, agent_store_dir: null }; } export async function saveDesktopSettings(_settings: DesktopSettings): Promise { @@ -860,6 +860,8 @@ export async function saveDesktopSettings(_settings: DesktopSettings): Promise { + throw new Error("Not available in web mode"); +} + +export async function setAgentStoreDir(_newDir: string | null): Promise { + throw new Error("Not available in web mode"); +} + export interface DriverStorePathInfo { driver_store_dir: string | null; + plugin_store_dir: string | null; + agent_store_dir: string | null; plugins_dir: string; agents_dir: string; } diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 29c97d321..4ad7fd53f 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -127,6 +127,8 @@ export interface DesktopSettings { debug_logging_enabled: boolean; saved_sql_sync_dir?: string | null; driver_store_dir?: string | null; + plugin_store_dir?: string | null; + agent_store_dir?: string | null; } export interface SavedSqlSyncEntry { @@ -334,6 +336,8 @@ export async function saveDesktopSettings(settings: DesktopSettings): Promise { + return invoke("set_plugin_store_dir", { newDir }); +} + +export async function setAgentStoreDir(newDir: string | null): Promise { + return invoke("set_agent_store_dir", { newDir }); +} + export interface DriverStorePathInfo { driver_store_dir: string | null; + plugin_store_dir: string | null; + agent_store_dir: string | null; plugins_dir: string; agents_dir: string; } diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index e4395fbc3..422aae7cf 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -41,6 +41,8 @@ export interface DesktopSettings { debug_logging_enabled: boolean; saved_sql_sync_dir?: string | null; driver_store_dir?: string | null; + plugin_store_dir?: string | null; + agent_store_dir?: string | null; } export type DesktopIconTheme = "default" | "black"; @@ -51,6 +53,8 @@ export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { debug_logging_enabled: false, saved_sql_sync_dir: null, driver_store_dir: null, + plugin_store_dir: null, + agent_store_dir: null, }; function normalizeDesktopSettings(settings: Partial | null | undefined): DesktopSettings { @@ -61,6 +65,8 @@ function normalizeDesktopSettings(settings: Partial | null | un debug_logging_enabled: settings?.debug_logging_enabled ?? DEFAULT_DESKTOP_SETTINGS.debug_logging_enabled, saved_sql_sync_dir: settings?.saved_sql_sync_dir?.trim() || DEFAULT_DESKTOP_SETTINGS.saved_sql_sync_dir, driver_store_dir: settings?.driver_store_dir?.trim() || DEFAULT_DESKTOP_SETTINGS.driver_store_dir, + plugin_store_dir: settings?.plugin_store_dir?.trim() || DEFAULT_DESKTOP_SETTINGS.plugin_store_dir, + agent_store_dir: settings?.agent_store_dir?.trim() || DEFAULT_DESKTOP_SETTINGS.agent_store_dir, }; } diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 53fa03484..e4ac498e2 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -1112,7 +1112,7 @@ fn default_plugin_dir() -> PathBuf { default_dbx_dir().join("plugins") } -fn default_agent_dir() -> PathBuf { +pub fn default_agent_dir() -> PathBuf { default_dbx_dir().join("agents") } diff --git a/crates/dbx-core/src/storage.rs b/crates/dbx-core/src/storage.rs index 6b953002c..b23bf48cb 100644 --- a/crates/dbx-core/src/storage.rs +++ b/crates/dbx-core/src/storage.rs @@ -38,6 +38,10 @@ pub struct DesktopSettings { pub saved_sql_sync_dir: Option, #[serde(default)] pub driver_store_dir: Option, + #[serde(default)] + pub plugin_store_dir: Option, + #[serde(default)] + pub agent_store_dir: Option, } impl Default for DesktopSettings { @@ -48,6 +52,8 @@ impl Default for DesktopSettings { debug_logging_enabled: false, saved_sql_sync_dir: None, driver_store_dir: None, + plugin_store_dir: None, + agent_store_dir: None, } } } @@ -518,6 +524,22 @@ impl Storage { settings.remove("driver_store_dir"); } } + match desktop_settings.plugin_store_dir.as_ref().filter(|path| !path.trim().is_empty()) { + Some(path) => { + settings.insert("plugin_store_dir".to_string(), serde_json::Value::String(path.clone())); + } + None => { + settings.remove("plugin_store_dir"); + } + } + match desktop_settings.agent_store_dir.as_ref().filter(|path| !path.trim().is_empty()) { + Some(path) => { + settings.insert("agent_store_dir".to_string(), serde_json::Value::String(path.clone())); + } + None => { + settings.remove("agent_store_dir"); + } + } self.save_app_settings_json(&settings).await } @@ -546,6 +568,18 @@ impl Storage { .map(str::trim) .filter(|value| !value.is_empty()) .map(ToString::to_string), + plugin_store_dir: settings + .get("plugin_store_dir") + .and_then(|value| value.as_str()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToString::to_string), + agent_store_dir: settings + .get("agent_store_dir") + .and_then(|value| value.as_str()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToString::to_string), }) } @@ -1495,6 +1529,8 @@ mod tests { debug_logging_enabled: true, saved_sql_sync_dir: None, driver_store_dir: Some("/tmp/dbx-drivers".to_string()), + plugin_store_dir: Some("/tmp/dbx-plugins".to_string()), + agent_store_dir: Some("/tmp/dbx-agents".to_string()), }) .await .unwrap(); @@ -1507,7 +1543,9 @@ mod tests { icon_theme: DesktopIconTheme::Black, debug_logging_enabled: true, saved_sql_sync_dir: None, - driver_store_dir: Some("/tmp/dbx-drivers".to_string()) + driver_store_dir: Some("/tmp/dbx-drivers".to_string()), + plugin_store_dir: Some("/tmp/dbx-plugins".to_string()), + agent_store_dir: Some("/tmp/dbx-agents".to_string()) } ); } diff --git a/src-tauri/src/commands/app_settings.rs b/src-tauri/src/commands/app_settings.rs index 71394c61c..77adb70c9 100644 --- a/src-tauri/src/commands/app_settings.rs +++ b/src-tauri/src/commands/app_settings.rs @@ -1,4 +1,7 @@ -use std::{path::PathBuf, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use dbx_core::storage::DesktopSettings; use tauri::{AppHandle, Manager, State}; @@ -9,6 +12,8 @@ use crate::{apply_debug_log_level, apply_desktop_settings}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct DriverStoreMigrationResult { pub driver_store_dir: Option, + pub plugin_store_dir: Option, + pub agent_store_dir: Option, pub migrated_plugins: bool, pub migrated_agents: bool, } @@ -53,6 +58,8 @@ pub async fn load_native_debug_logs(app: AppHandle) -> Result { #[derive(Debug, Clone, serde::Serialize)] pub struct DriverStorePathInfo { pub driver_store_dir: Option, + pub plugin_store_dir: Option, + pub agent_store_dir: Option, pub plugins_dir: String, pub agents_dir: String, } @@ -62,6 +69,8 @@ pub async fn get_driver_store_path(state: State<'_, Arc>) -> Result>) -> Result>, new_dir: Option, ) -> Result { - let new_dir = new_dir.filter(|d| !d.trim().is_empty()).map(|d| d.trim().to_string()); - let new_path = new_dir.as_ref().map(PathBuf::from); - - // Resolve current plugin/agent directories + let new_dir = normalize_store_dir(new_dir); let current_plugins_dir = state.plugins.root_dir().to_path_buf(); let current_agents_dir = state.agent_manager.base_dir().clone(); - - // Validate: if setting a custom dir, it must be different from current parent - if let Some(ref np) = new_path { - let target_plugins = np.join("plugins"); - let target_agents = np.join("agents"); - - // Canonicalize for comparison (both sides) - let np_canonical = if np.exists() { - np.canonicalize().map_err(|e| format!("Invalid path {}: {e}", np.display()))? - } else { - std::fs::create_dir_all(np).map_err(|e| format!("Failed to create directory {}: {e}", np.display()))?; - np.canonicalize().map_err(|e| format!("Invalid path {}: {e}", np.display()))? - }; - - let current_plugins_parent = current_plugins_dir.parent(); - let current_agents_parent = current_agents_dir.parent(); - - // Check if the new dir is the same as the current parent (no-op) - if current_plugins_parent == Some(&np_canonical) && current_agents_parent == Some(&np_canonical) { - return Ok(DriverStoreMigrationResult { - driver_store_dir: new_dir, - migrated_plugins: false, - migrated_agents: false, - }); + let (target_plugins_dir, target_agents_dir) = match new_dir.as_ref() { + Some(dir) => { + let driver_base = PathBuf::from(dir); + (driver_base.join("plugins"), driver_base.join("agents")) } + None => default_store_dirs(&app)?, + }; - // Stop all running agent daemons before migration - state.agent_manager.stop_daemons().await; + state.agent_manager.stop_daemons().await; + let migrated_plugins = migrate_store_directory(¤t_plugins_dir, &target_plugins_dir)?; + let migrated_agents = migrate_store_directory(¤t_agents_dir, &target_agents_dir)?; - // Migrate plugins directory - let migrated_plugins = if current_plugins_dir.exists() { - migrate_directory(¤t_plugins_dir, &target_plugins)?; - true - } else { - false - }; - - // Migrate agents directory - let migrated_agents = if current_agents_dir.exists() { - migrate_directory(¤t_agents_dir, &target_agents)?; - true - } else { - false - }; - - // Verify migration - if migrated_plugins { - verify_migration(¤t_plugins_dir, &target_plugins)?; - } - if migrated_agents { - verify_migration(¤t_agents_dir, &target_agents)?; - } - - // Delete old data after successful verification - if migrated_plugins { - if let Err(err) = std::fs::remove_dir_all(¤t_plugins_dir) { - log::warn!("Failed to remove old plugins dir {}: {err}", current_plugins_dir.display()); - } - } - if migrated_agents { - if let Err(err) = std::fs::remove_dir_all(¤t_agents_dir) { - log::warn!("Failed to remove old agents dir {}: {err}", current_agents_dir.display()); - } - } - } - - // Save the setting let mut settings = state.storage.load_desktop_settings().await.unwrap_or_default(); settings.driver_store_dir = new_dir.clone(); + settings.plugin_store_dir = None; + settings.agent_store_dir = None; state.storage.save_desktop_settings(&settings).await?; - Ok(DriverStoreMigrationResult { driver_store_dir: new_dir, migrated_plugins: true, migrated_agents: true }) + Ok(driver_store_migration_result(settings, migrated_plugins, migrated_agents)) +} + +#[tauri::command] +pub async fn set_plugin_store_dir( + app: AppHandle, + state: State<'_, Arc>, + new_dir: Option, +) -> Result { + let new_dir = normalize_store_dir(new_dir); + let current_plugins_dir = state.plugins.root_dir().to_path_buf(); + let current_agents_dir = state.agent_manager.base_dir().clone(); + let target_plugins_dir = match new_dir.as_ref() { + Some(dir) => PathBuf::from(dir), + None => default_plugin_store_dir(&app)?, + }; + + let migrated_plugins = migrate_store_directory(¤t_plugins_dir, &target_plugins_dir)?; + + let mut settings = state.storage.load_desktop_settings().await.unwrap_or_default(); + convert_legacy_driver_store(&mut settings, ¤t_plugins_dir, ¤t_agents_dir); + settings.plugin_store_dir = new_dir; + state.storage.save_desktop_settings(&settings).await?; + + Ok(driver_store_migration_result(settings, migrated_plugins, false)) +} + +#[tauri::command] +pub async fn set_agent_store_dir( + app: AppHandle, + state: State<'_, Arc>, + new_dir: Option, +) -> Result { + let new_dir = normalize_store_dir(new_dir); + let current_plugins_dir = state.plugins.root_dir().to_path_buf(); + let current_agents_dir = state.agent_manager.base_dir().clone(); + let target_agents_dir = match new_dir.as_ref() { + Some(dir) => PathBuf::from(dir), + None => default_agent_store_dir(&app)?, + }; + + state.agent_manager.stop_daemons().await; + let migrated_agents = migrate_store_directory(¤t_agents_dir, &target_agents_dir)?; + + let mut settings = state.storage.load_desktop_settings().await.unwrap_or_default(); + convert_legacy_driver_store(&mut settings, ¤t_plugins_dir, ¤t_agents_dir); + settings.agent_store_dir = new_dir; + state.storage.save_desktop_settings(&settings).await?; + + Ok(driver_store_migration_result(settings, false, migrated_agents)) +} + +fn normalize_store_dir(dir: Option) -> Option { + dir.and_then(|value| { + let trimmed = value.trim(); + (!trimmed.is_empty()).then(|| trimmed.to_string()) + }) +} + +fn default_store_dirs(app: &AppHandle) -> Result<(PathBuf, PathBuf), String> { + Ok((default_plugin_store_dir(app)?, default_agent_store_dir(app)?)) +} + +fn default_plugin_store_dir(app: &AppHandle) -> Result { + let default_data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + Ok(crate::data_dir::resolve_data_dir(default_data_dir).join("plugins")) +} + +fn default_agent_store_dir(app: &AppHandle) -> Result { + let default_data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + let data_dir = crate::data_dir::resolve_data_dir(default_data_dir); + Ok(if crate::data_dir::uses_custom_data_dir() { + data_dir.join("agents") + } else { + dbx_core::connection::default_agent_dir() + }) +} + +fn convert_legacy_driver_store(settings: &mut DesktopSettings, current_plugins_dir: &Path, current_agents_dir: &Path) { + if settings.driver_store_dir.is_none() { + return; + } + if settings.plugin_store_dir.is_none() { + settings.plugin_store_dir = Some(current_plugins_dir.to_string_lossy().to_string()); + } + if settings.agent_store_dir.is_none() { + settings.agent_store_dir = Some(current_agents_dir.to_string_lossy().to_string()); + } + settings.driver_store_dir = None; +} + +fn driver_store_migration_result( + settings: DesktopSettings, + migrated_plugins: bool, + migrated_agents: bool, +) -> DriverStoreMigrationResult { + DriverStoreMigrationResult { + driver_store_dir: settings.driver_store_dir, + plugin_store_dir: settings.plugin_store_dir, + agent_store_dir: settings.agent_store_dir, + migrated_plugins, + migrated_agents, + } +} + +fn migrate_store_directory(current_dir: &Path, target_dir: &Path) -> Result { + std::fs::create_dir_all(target_dir) + .map_err(|e| format!("Failed to create directory {}: {e}", target_dir.display()))?; + let target_canonical = + target_dir.canonicalize().map_err(|e| format!("Invalid path {}: {e}", target_dir.display()))?; + + if !current_dir.exists() { + return Ok(false); + } + + let current_canonical = + current_dir.canonicalize().map_err(|e| format!("Invalid path {}: {e}", current_dir.display()))?; + if current_canonical == target_canonical { + return Ok(false); + } + if target_canonical.starts_with(¤t_canonical) { + return Err(format!( + "Target directory {} cannot be inside current directory {}", + target_dir.display(), + current_dir.display() + )); + } + if target_dir_has_entries(target_dir)? { + return Err(format!( + "Target directory {} is not empty. Please choose an empty directory or remove the existing data first.", + target_dir.display() + )); + } + + migrate_directory(current_dir, target_dir)?; + verify_migration(current_dir, target_dir)?; + if let Err(err) = std::fs::remove_dir_all(current_dir) { + log::warn!("Failed to remove old driver store dir {}: {err}", current_dir.display()); + } + Ok(true) +} + +fn target_dir_has_entries(dir: &Path) -> Result { + let mut entries = std::fs::read_dir(dir).map_err(|e| format!("Failed to read {}: {e}", dir.display()))?; + Ok(entries.next().transpose().map_err(|e| e.to_string())?.is_some()) } /// Recursively copy a directory to a new location. -fn migrate_directory(src: &PathBuf, dst: &PathBuf) -> Result<(), String> { +fn migrate_directory(src: &Path, dst: &Path) -> Result<(), String> { if !src.exists() { return Ok(()); } @@ -178,7 +276,7 @@ fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> Result<() } /// Verify that the migrated directory has the same file count and total size. -fn verify_migration(src: &PathBuf, dst: &PathBuf) -> Result<(), String> { +fn verify_migration(src: &Path, dst: &Path) -> Result<(), String> { let src_info = count_files_recursive(src)?; let dst_info = count_files_recursive(dst)?; if src_info.count != dst_info.count || src_info.total_size != dst_info.total_size { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0e7426bf5..c137ff691 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -261,27 +261,29 @@ pub fn run() { apply_debug_log_level(desktop_settings.debug_logging_enabled); eprintln!("[STARTUP] storage ready in {:?}", t.elapsed()); - let state = if let Some(ref driver_dir) = desktop_settings.driver_store_dir { - let driver_base = std::path::PathBuf::from(driver_dir); + let legacy_driver_base = desktop_settings.driver_store_dir.as_ref().map(std::path::PathBuf::from); + let plugin_dir = desktop_settings + .plugin_store_dir + .as_ref() + .map(std::path::PathBuf::from) + .or_else(|| legacy_driver_base.as_ref().map(|base| base.join("plugins"))) + .unwrap_or_else(|| data_dir.join("plugins")); + let agent_dir = desktop_settings + .agent_store_dir + .as_ref() + .map(std::path::PathBuf::from) + .or_else(|| legacy_driver_base.as_ref().map(|base| base.join("agents"))) + .or_else(|| data_dir::uses_custom_data_dir().then(|| data_dir.join("agents"))); + + let state = if let Some(agent_dir) = agent_dir { Arc::new(AppState::new_with_plugin_and_agent_dir_and_app_version( storage, - driver_base.join("plugins"), - driver_base.join("agents"), - env!("CARGO_PKG_VERSION"), - )) - } else if data_dir::uses_custom_data_dir() { - Arc::new(AppState::new_with_plugin_and_agent_dir_and_app_version( - storage, - data_dir.join("plugins"), - data_dir.join("agents"), + plugin_dir, + agent_dir, env!("CARGO_PKG_VERSION"), )) } else { - Arc::new(AppState::new_with_plugin_dir_and_app_version( - storage, - data_dir.join("plugins"), - env!("CARGO_PKG_VERSION"), - )) + Arc::new(AppState::new_with_plugin_dir_and_app_version(storage, plugin_dir, env!("CARGO_PKG_VERSION"))) }; app.manage(state.clone()); app.manage(commands::saved_sql::SavedSqlStorageState { data_dir: data_dir.clone() }); @@ -337,6 +339,8 @@ pub fn run() { commands::app_settings::load_desktop_settings, commands::app_settings::save_desktop_settings, commands::app_settings::set_driver_store_dir, + commands::app_settings::set_plugin_store_dir, + commands::app_settings::set_agent_store_dir, commands::app_settings::get_driver_store_path, commands::app_settings::load_pinned_tree_node_ids, commands::app_settings::save_pinned_tree_node_ids,