feat(settings): split driver store paths

This commit is contained in:
t8y2 2026-06-13 17:27:58 +08:00
parent 3bd0f50ff7
commit 0b9aef1ca8
15 changed files with 403 additions and 157 deletions

View File

@ -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<DriverStoreDirKind | null>(null);
const currentDriverStorePath = ref<DriverStorePathInfo | null>(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) => {
<div v-if="!isWeb" class="rounded-xl border bg-muted/20 p-4 space-y-3">
<div class="text-sm font-medium">{{ t("driverStore.driverStoreDir") }}</div>
<p class="text-xs text-muted-foreground">{{ t("driverStore.driverStoreDirDescription") }}</p>
<div class="flex items-center gap-2">
<Tooltip>
<TooltipTrigger as-child>
<div class="min-w-0 flex-1 rounded-md border bg-background px-3 py-2 text-xs font-mono truncate">
{{ driverStoreDirDisplay }}
<div class="space-y-2.5">
<div v-for="row in driverStorePathRows" :key="row.kind" class="rounded-lg border bg-background/50 p-3">
<div class="mb-2 flex items-start justify-between gap-3">
<div class="min-w-0 text-xs leading-5">
<span class="font-medium">{{ row.label }}</span>
<span class="ml-2 text-[11px] text-muted-foreground">{{ row.description }}</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" class="max-w-100 break-all text-xs">
{{ driverStoreDirDisplay }}
</TooltipContent>
</Tooltip>
<Button variant="outline" size="sm" class="shrink-0 gap-1" :disabled="driverStoreDirMigrating" @click="chooseDriverStoreDir">
<FolderSync class="h-3.5 w-3.5" />
{{ t("driverStore.driverStoreDirChange") }}
</Button>
<Button v-if="driverStoreDir" variant="ghost" size="sm" class="shrink-0 gap-1 text-muted-foreground" :disabled="driverStoreDirMigrating" @click="resetDriverStoreDir">
{{ t("driverStore.driverStoreDirReset") }}
</Button>
<Loader2 v-if="driverStoreDirMigrating === row.kind" class="mt-0.5 h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground" />
</div>
<div class="flex items-center gap-2">
<Tooltip>
<TooltipTrigger as-child>
<div class="min-w-0 flex-1 rounded-md border bg-background px-3 py-2 text-xs font-mono truncate">
{{ row.display }}
</div>
</TooltipTrigger>
<TooltipContent side="bottom" class="max-w-100 break-all text-xs">
{{ row.display }}
</TooltipContent>
</Tooltip>
<Button variant="outline" size="sm" class="shrink-0 gap-1" :disabled="Boolean(driverStoreDirMigrating)" @click="chooseDriverStoreDir(row.kind)">
<FolderSync class="h-3.5 w-3.5" />
{{ t("driverStore.driverStoreDirChange") }}
</Button>
<Button v-if="row.custom" variant="ghost" size="sm" class="shrink-0 gap-1 text-muted-foreground" :disabled="Boolean(driverStoreDirMigrating)" @click="resetDriverStoreDir(row.kind)">
{{ t("driverStore.driverStoreDirReset") }}
</Button>
</div>
</div>
</div>
<p v-if="driverStoreDirMigrating" class="text-xs text-muted-foreground flex items-center gap-1.5">
<Loader2 class="h-3 w-3 animate-spin" />
{{ t("driverStore.driverStoreDirMigrating") }}
{{ t("driverStore.driverStoreDirMigrating", { target: driverStoreTargetLabel(driverStoreDirMigrating) }) }}
</p>
</div>

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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: "导出数据库",

View File

@ -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: "匯出資料庫",

View File

@ -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");

View File

@ -851,7 +851,7 @@ export async function loadAiConfig(): Promise<AiConfig | null> {
}
export async function loadDesktopSettings(): Promise<DesktopSettings> {
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<void> {
@ -860,6 +860,8 @@ export async function saveDesktopSettings(_settings: DesktopSettings): Promise<v
export interface DriverStoreMigrationResult {
driver_store_dir: string | null;
plugin_store_dir: string | null;
agent_store_dir: string | null;
migrated_plugins: boolean;
migrated_agents: boolean;
}
@ -868,8 +870,18 @@ export async function setDriverStoreDir(_newDir: string | null): Promise<DriverS
throw new Error("Not available in web mode");
}
export async function setPluginStoreDir(_newDir: string | null): Promise<DriverStoreMigrationResult> {
throw new Error("Not available in web mode");
}
export async function setAgentStoreDir(_newDir: string | null): Promise<DriverStoreMigrationResult> {
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;
}

View File

@ -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<vo
export interface DriverStoreMigrationResult {
driver_store_dir: string | null;
plugin_store_dir: string | null;
agent_store_dir: string | null;
migrated_plugins: boolean;
migrated_agents: boolean;
}
@ -342,8 +346,18 @@ export async function setDriverStoreDir(newDir: string | null): Promise<DriverSt
return invoke("set_driver_store_dir", { newDir });
}
export async function setPluginStoreDir(newDir: string | null): Promise<DriverStoreMigrationResult> {
return invoke("set_plugin_store_dir", { newDir });
}
export async function setAgentStoreDir(newDir: string | null): Promise<DriverStoreMigrationResult> {
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;
}

View File

@ -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<DesktopSettings> | null | undefined): DesktopSettings {
@ -61,6 +65,8 @@ function normalizeDesktopSettings(settings: Partial<DesktopSettings> | 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,
};
}

View File

@ -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")
}

View File

@ -38,6 +38,10 @@ pub struct DesktopSettings {
pub saved_sql_sync_dir: Option<String>,
#[serde(default)]
pub driver_store_dir: Option<String>,
#[serde(default)]
pub plugin_store_dir: Option<String>,
#[serde(default)]
pub agent_store_dir: Option<String>,
}
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())
}
);
}

View File

@ -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<String>,
pub plugin_store_dir: Option<String>,
pub agent_store_dir: Option<String>,
pub migrated_plugins: bool,
pub migrated_agents: bool,
}
@ -53,6 +58,8 @@ pub async fn load_native_debug_logs(app: AppHandle) -> Result<String, String> {
#[derive(Debug, Clone, serde::Serialize)]
pub struct DriverStorePathInfo {
pub driver_store_dir: Option<String>,
pub plugin_store_dir: Option<String>,
pub agent_store_dir: Option<String>,
pub plugins_dir: String,
pub agents_dir: String,
}
@ -62,6 +69,8 @@ pub async fn get_driver_store_path(state: State<'_, Arc<AppState>>) -> Result<Dr
let settings = state.storage.load_desktop_settings().await.unwrap_or_default();
Ok(DriverStorePathInfo {
driver_store_dir: settings.driver_store_dir,
plugin_store_dir: settings.plugin_store_dir,
agent_store_dir: settings.agent_store_dir,
plugins_dir: state.plugins.root_dir().to_string_lossy().to_string(),
agents_dir: state.agent_manager.base_dir().to_string_lossy().to_string(),
})
@ -69,91 +78,180 @@ pub async fn get_driver_store_path(state: State<'_, Arc<AppState>>) -> Result<Dr
#[tauri::command]
pub async fn set_driver_store_dir(
app: AppHandle,
state: State<'_, Arc<AppState>>,
new_dir: Option<String>,
) -> Result<DriverStoreMigrationResult, String> {
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(&current_plugins_dir, &target_plugins_dir)?;
let migrated_agents = migrate_store_directory(&current_agents_dir, &target_agents_dir)?;
// Migrate plugins directory
let migrated_plugins = if current_plugins_dir.exists() {
migrate_directory(&current_plugins_dir, &target_plugins)?;
true
} else {
false
};
// Migrate agents directory
let migrated_agents = if current_agents_dir.exists() {
migrate_directory(&current_agents_dir, &target_agents)?;
true
} else {
false
};
// Verify migration
if migrated_plugins {
verify_migration(&current_plugins_dir, &target_plugins)?;
}
if migrated_agents {
verify_migration(&current_agents_dir, &target_agents)?;
}
// Delete old data after successful verification
if migrated_plugins {
if let Err(err) = std::fs::remove_dir_all(&current_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(&current_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<AppState>>,
new_dir: Option<String>,
) -> Result<DriverStoreMigrationResult, String> {
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(&current_plugins_dir, &target_plugins_dir)?;
let mut settings = state.storage.load_desktop_settings().await.unwrap_or_default();
convert_legacy_driver_store(&mut settings, &current_plugins_dir, &current_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<AppState>>,
new_dir: Option<String>,
) -> Result<DriverStoreMigrationResult, String> {
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(&current_agents_dir, &target_agents_dir)?;
let mut settings = state.storage.load_desktop_settings().await.unwrap_or_default();
convert_legacy_driver_store(&mut settings, &current_plugins_dir, &current_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<String>) -> Option<String> {
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<PathBuf, String> {
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<PathBuf, String> {
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<bool, String> {
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(&current_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<bool, String> {
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 {

View File

@ -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,