From b43242b9b24352c7f90995eccab753dede679616 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 8 Aug 2024 22:54:25 +0700 Subject: [PATCH] feat: change data folder (#3309) --- core/src/types/api/index.ts | 3 + core/src/types/config/appConfigEntity.ts | 8 ++- electron/handlers/native.ts | 65 ++++++++++++++++--- electron/main.ts | 2 +- electron/managers/tray.ts | 2 +- electron/managers/window.ts | 2 +- electron/preload.ts | 30 +-------- electron/utils/path.ts | 56 +++++++++++----- electron/utils/shortcut.ts | 2 +- web/app/search/layout.tsx | 10 +-- .../Settings/Advanced/DataFolder/index.tsx | 40 ++++++++---- web/screens/Settings/Advanced/index.tsx | 5 +- 12 files changed, 146 insertions(+), 79 deletions(-) diff --git a/core/src/types/api/index.ts b/core/src/types/api/index.ts index 141794f493..267441f4ab 100644 --- a/core/src/types/api/index.ts +++ b/core/src/types/api/index.ts @@ -35,6 +35,9 @@ export enum NativeRoute { syncModelFileToCortex = 'syncModelFileToCortex', openAppLog = 'openAppLog', + appDataFolder = 'appDataFolder', + changeDataFolder = 'changeDataFolder', + isDirectoryEmpty = 'isDirectoryEmpty', } export enum AppEvent { diff --git a/core/src/types/config/appConfigEntity.ts b/core/src/types/config/appConfigEntity.ts index 1402aeca12..6180303c1c 100644 --- a/core/src/types/config/appConfigEntity.ts +++ b/core/src/types/config/appConfigEntity.ts @@ -1,4 +1,8 @@ export type AppConfiguration = { - data_folder: string - quick_ask: boolean + dataFolderPath: string, + quickAsk: boolean, + cortexCppHost: string, + cortexCppPort: number, + apiServerHost: string, + apiServerPort: number, } diff --git a/electron/handlers/native.ts b/electron/handlers/native.ts index 2d7226c52f..0457c0a3e0 100644 --- a/electron/handlers/native.ts +++ b/electron/handlers/native.ts @@ -5,10 +5,11 @@ import { NativeRoute, SelectFileProp, SelectFileOption, + AppConfiguration, } from '@janhq/core/node' import { menu } from '../utils/menu' import { join } from 'path' -import { getAppConfigurations, getJanDataFolderPath } from './../utils/path' +import { getAppConfigurations, getJanDataFolderPath, legacyDataPath, updateAppConfiguration } from './../utils/path' import { readdirSync, writeFileSync, @@ -16,8 +17,7 @@ import { existsSync, mkdirSync, } from 'fs' -import { dump } from 'js-yaml' - +import { dump, load } from 'js-yaml' const isMac = process.platform === 'darwin' export function handleAppIPCs() { @@ -209,7 +209,7 @@ export function handleAppIPCs() { ipcMain.handle(NativeRoute.openAppLog, async (_event): Promise => { const configuration = getAppConfigurations() - const dataFolder = configuration.data_folder + const dataFolder = configuration.dataFolderPath try { const errorMessage = await shell.openPath(join(dataFolder)) @@ -224,11 +224,14 @@ export function handleAppIPCs() { }) ipcMain.handle(NativeRoute.syncModelFileToCortex, async (_event) => { - const janModelFolderPath = join(getJanDataFolderPath(), 'models') + + // Read models from legacy data folder + const janModelFolderPath = join(legacyDataPath(), 'models') const allModelFolders = readdirSync(janModelFolderPath) + // Latest app configs const configration = getAppConfigurations() - const destinationFolderPath = join(configration.data_folder, 'models') + const destinationFolderPath = join(configration.dataFolderPath, 'models') if (!existsSync(destinationFolderPath)) mkdirSync(destinationFolderPath) @@ -332,7 +335,7 @@ export function handleAppIPCs() { ipcMain.handle( NativeRoute.getAllMessagesAndThreads, async (_event): Promise => { - const janThreadFolderPath = join(getJanDataFolderPath(), 'threads') + const janThreadFolderPath = join(legacyDataPath(), 'threads') // check if exist if (!existsSync(janThreadFolderPath)) { return { @@ -382,7 +385,7 @@ export function handleAppIPCs() { ipcMain.handle( NativeRoute.getAllLocalModels, async (_event): Promise => { - const janModelsFolderPath = join(getJanDataFolderPath(), 'models') + const janModelsFolderPath = join(legacyDataPath(), 'models') if (!existsSync(janModelsFolderPath)) { console.debug('No local models found') @@ -408,4 +411,50 @@ export function handleAppIPCs() { return hasLocalModels } ) + ipcMain.handle(NativeRoute.appDataFolder, () => { + return getJanDataFolderPath() + }) + + ipcMain.handle(NativeRoute.changeDataFolder, async (_event, path) => { + const appConfiguration: AppConfiguration = getAppConfigurations() + const currentJanDataFolder = appConfiguration.dataFolderPath + + appConfiguration.dataFolderPath = path + + const reflect = require('@alumna/reflect') + const { err } = await reflect({ + src: currentJanDataFolder, + dest: path, + recursive: true, + delete: false, + overwrite: true, + errorOnExist: false, + }) + if (err) { + console.error(err) + throw err + } + + // Migrate models + const janModelsPath = join(path, 'models') + if (existsSync(janModelsPath)) { + const modelYamls = readdirSync(janModelsPath).filter((x) => + x.endsWith('.yaml') || x.endsWith('.yml') + ) + for(const yaml of modelYamls) { + const modelPath = join(janModelsPath, yaml) + const model = load(readFileSync(modelPath, 'utf-8')) as any + if('files' in model && Array.isArray(model.files) && model.files.length > 0) { + model.files[0] = model.files[0].replace(currentJanDataFolder, path) + } + writeFileSync(modelPath, dump(model)) + } + } + await updateAppConfiguration(appConfiguration) + }) + + ipcMain.handle(NativeRoute.isDirectoryEmpty, async (_event, path) => { + const dirChildren = readdirSync(path) + return dirChildren.filter((x) => x !== '.DS_Store').length === 0 + }) } diff --git a/electron/main.ts b/electron/main.ts index ef916bb7f3..e928177b5d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -89,7 +89,7 @@ app .then(() => killProcessesOnPort(cortexJsPort)) .then(() => { const appConfiguration = getAppConfigurations() - const janDataFolder = appConfiguration.data_folder + const janDataFolder = appConfiguration.dataFolderPath start('jan', host, cortexJsPort, cortexCppPort, janDataFolder) }) diff --git a/electron/managers/tray.ts b/electron/managers/tray.ts index fad55294f4..470499238d 100644 --- a/electron/managers/tray.ts +++ b/electron/managers/tray.ts @@ -8,7 +8,7 @@ class TrayManager { createSystemTray = () => { // Feature Toggle for Quick Ask - if (!getAppConfigurations().quick_ask) return + if (!getAppConfigurations().quickAsk) return if (this.currentTray) { return diff --git a/electron/managers/window.ts b/electron/managers/window.ts index be2d0a7b95..d837505aa7 100644 --- a/electron/managers/window.ts +++ b/electron/managers/window.ts @@ -73,7 +73,7 @@ class WindowManager { windowManager.mainWindow?.on('close', function (evt) { // Feature Toggle for Quick Ask - if (!getAppConfigurations().quick_ask) return + if (!getAppConfigurations().quickAsk) return if (!isAppQuitting) { evt.preventDefault() diff --git a/electron/preload.ts b/electron/preload.ts index 647afa5b19..7378add11f 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -3,9 +3,8 @@ * @module preload */ -import { APIEvents, APIRoutes, AppConfiguration } from '@janhq/core/node' +import { APIEvents, APIRoutes } from '@janhq/core/node' import { contextBridge, ipcRenderer } from 'electron' -import { readdirSync } from 'fs' const interfaces: { [key: string]: (...args: any[]) => any } = {} @@ -25,32 +24,7 @@ APIEvents.forEach((method) => { interfaces[method] = (handler: any) => ipcRenderer.on(method, handler) }) -interfaces['changeDataFolder'] = async (path) => { - const appConfiguration: AppConfiguration = await ipcRenderer.invoke( - 'getAppConfigurations' - ) - const currentJanDataFolder = appConfiguration.data_folder - appConfiguration.data_folder = path - const reflect = require('@alumna/reflect') - const { err } = await reflect({ - src: currentJanDataFolder, - dest: path, - recursive: true, - delete: false, - overwrite: true, - errorOnExist: false, - }) - if (err) { - console.error(err) - throw err - } - await ipcRenderer.invoke('updateAppConfiguration', appConfiguration) -} - -interfaces['isDirectoryEmpty'] = async (path) => { - const dirChildren = await readdirSync(path) - return dirChildren.filter((x) => x !== '.DS_Store').length === 0 -} + // Expose the 'interfaces' object in the main world under the name 'electronAPI' // This allows the renderer process to access these methods directly diff --git a/electron/utils/path.ts b/electron/utils/path.ts index a6ff9d4755..46ece1e079 100644 --- a/electron/utils/path.ts +++ b/electron/utils/path.ts @@ -3,14 +3,19 @@ import { existsSync, writeFileSync, readFileSync } from 'fs' import { join } from 'path' import { AppConfiguration } from '@janhq/core/node' import os from 'os' +import { dump, load } from 'js-yaml' -const configurationFileName = 'settings.json' +const configurationFileName = '.janrc' const defaultJanDataFolder = join(os.homedir(), 'jan') const defaultAppConfig: AppConfiguration = { - data_folder: defaultJanDataFolder, - quick_ask: false, + dataFolderPath: defaultJanDataFolder, + quickAsk: true, + cortexCppHost: '127.0.0.1', + cortexCppPort: 3940, + apiServerHost: '127.0.0.1', + apiServerPort: 1338 } export async function createUserSpace(): Promise { @@ -66,15 +71,14 @@ export const getAppConfigurations = (): AppConfiguration => { console.debug( `App config not found, creating default config at ${configurationFile}` ) - writeFileSync(configurationFile, JSON.stringify(defaultAppConfig)) + writeFileSync(configurationFile, dump(defaultAppConfig)) return defaultAppConfig } try { - const appConfigurations: AppConfiguration = JSON.parse( - readFileSync(configurationFile, 'utf-8') - ) - console.debug('app config', JSON.stringify(appConfigurations)) + const configYaml = readFileSync(configurationFile, 'utf-8') + const appConfigurations = load(configYaml) as AppConfiguration + console.debug('app config', appConfigurations) return appConfigurations } catch (err) { console.error( @@ -84,12 +88,15 @@ export const getAppConfigurations = (): AppConfiguration => { } } -const getConfigurationFilePath = () => - join( - global.core?.appPath() || - process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME'], - configurationFileName - ) +// Get configuration file path of the application +const getConfigurationFilePath = () => { + const homeDir = os.homedir(); + const configPath = join( + homeDir, + configurationFileName, + ); + return configPath +} export const updateAppConfiguration = ( configuration: AppConfiguration @@ -100,7 +107,7 @@ export const updateAppConfiguration = ( configurationFile ) - writeFileSync(configurationFile, JSON.stringify(configuration)) + writeFileSync(configurationFile, dump(configuration)) return Promise.resolve() } @@ -110,6 +117,21 @@ export const updateAppConfiguration = ( * @returns {string} The data folder path. */ export const getJanDataFolderPath = (): string => { - const appConfigurations = getAppConfigurations() - return appConfigurations.data_folder + return getAppConfigurations().dataFolderPath +} + +// This is to support pulling legacy configs for migration purpose +export const legacyConfigs = () => { + const legacyConfigFilePath = join( + process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME'] ?? '', + 'settings.json' + ) + const legacyConfigs = JSON.parse(readFileSync(legacyConfigFilePath, 'utf-8')) as any + + return legacyConfigs +} + +// This is to support pulling legacy data path for migration purpose +export const legacyDataPath = () => { + return legacyConfigs().data_path } diff --git a/electron/utils/shortcut.ts b/electron/utils/shortcut.ts index 45aa7c8a29..7f4d4c4f70 100644 --- a/electron/utils/shortcut.ts +++ b/electron/utils/shortcut.ts @@ -5,7 +5,7 @@ import { windowManager } from '../managers/window' const quickAskHotKey = 'CommandOrControl+J' export function registerGlobalShortcuts() { - if (!getAppConfigurations().quick_ask) return + if (!getAppConfigurations().quickAsk) return const ret = registerShortcut(quickAskHotKey, (selectedText: string) => { // Feature Toggle for Quick Ask if (!windowManager.isQuickAskWindowVisible()) { diff --git a/web/app/search/layout.tsx b/web/app/search/layout.tsx index a27aa0430a..3b7a280a3f 100644 --- a/web/app/search/layout.tsx +++ b/web/app/search/layout.tsx @@ -2,8 +2,6 @@ import { useEffect } from 'react' -import { AppConfiguration } from '@janhq/core' - import { useSetAtom } from 'jotai' import ClipboardListener from '@/containers/Providers/ClipboardListener' @@ -29,11 +27,9 @@ export default function RootLayout() { }, []) useEffect(() => { - window.core?.api - ?.getAppConfigurations() - ?.then((appConfig: AppConfiguration) => { - setJanDataFolderPath(appConfig.data_folder) - }) + window.electronAPI?.appDataFolder()?.then((path: string) => { + setJanDataFolderPath(path) + }) }, [setJanDataFolderPath]) useEffect(() => { diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 37555c487d..bbbcc6b8f0 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -1,7 +1,9 @@ -import { Fragment, useCallback, useState } from 'react' +import { isAbsolute, relative } from 'path' + +import { Fragment, useCallback, useEffect, useState } from 'react' import { Button, Input } from '@janhq/joi' -import { useAtomValue, useSetAtom } from 'jotai' +import { useAtom, useSetAtom } from 'jotai' import { PencilIcon, FolderOpenIcon } from 'lucide-react' import Loader from '@/containers/Loader' @@ -29,8 +31,18 @@ const DataFolder = () => { const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom) const showDestNotEmptyConfirm = useSetAtom(showDestNotEmptyConfirmAtom) + const [janDataFolderPath, setJanDataFolderPath] = useAtom( + janDataFolderPathAtom + ) + const getAppDataFolder = useCallback(async () => { + return window.electronAPI?.appDataFolder().then(setJanDataFolderPath) + }, [setJanDataFolderPath]) + const [destinationPath, setDestinationPath] = useState(undefined) - const janDataFolderPath = useAtomValue(janDataFolderPathAtom) + + useEffect(() => { + getAppDataFolder() + }, [getAppDataFolder]) const onChangeFolderClick = useCallback(async () => { const destFolder = await window.core?.api?.selectDirectory() @@ -41,14 +53,18 @@ const DataFolder = () => { return } - // const appConfiguration: AppConfiguration = - // await window.core?.api?.getAppConfigurations() - // const currentJanDataFolder = appConfiguration.data_folder + const currentJanDataFolder = await window.electronAPI?.appDataFolder() - // if (await isSubdirectory(currentJanDataFolder, destFolder)) { - // setShowSameDirectory(true) - // return - // } + const relativePath = relative(currentJanDataFolder, destFolder) + + if ( + relativePath && + !relativePath.startsWith('..') && + !isAbsolute(relativePath) + ) { + setShowSameDirectory(true) + return + } const isEmpty: boolean = await window.core?.api?.isDirectoryEmpty(destFolder) @@ -106,7 +122,9 @@ const DataFolder = () => { window.core?.api?.openAppDirectory()} + onClick={() => + window.electronAPI?.openFileExplorer(janDataFolderPath) + } />