feat: change data folder (#3309)

This commit is contained in:
Louis 2024-08-08 22:54:25 +07:00 committed by GitHub
parent b348110fb7
commit b43242b9b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 145 additions and 78 deletions

View File

@ -35,6 +35,9 @@ export enum NativeRoute {
syncModelFileToCortex = 'syncModelFileToCortex', syncModelFileToCortex = 'syncModelFileToCortex',
openAppLog = 'openAppLog', openAppLog = 'openAppLog',
appDataFolder = 'appDataFolder',
changeDataFolder = 'changeDataFolder',
isDirectoryEmpty = 'isDirectoryEmpty',
} }
export enum AppEvent { export enum AppEvent {

View File

@ -1,4 +1,8 @@
export type AppConfiguration = { export type AppConfiguration = {
data_folder: string dataFolderPath: string,
quick_ask: boolean quickAsk: boolean,
cortexCppHost: string,
cortexCppPort: number,
apiServerHost: string,
apiServerPort: number,
} }

View File

@ -5,10 +5,11 @@ import {
NativeRoute, NativeRoute,
SelectFileProp, SelectFileProp,
SelectFileOption, SelectFileOption,
AppConfiguration,
} from '@janhq/core/node' } from '@janhq/core/node'
import { menu } from '../utils/menu' import { menu } from '../utils/menu'
import { join } from 'path' import { join } from 'path'
import { getAppConfigurations, getJanDataFolderPath } from './../utils/path' import { getAppConfigurations, getJanDataFolderPath, legacyDataPath, updateAppConfiguration } from './../utils/path'
import { import {
readdirSync, readdirSync,
writeFileSync, writeFileSync,
@ -16,8 +17,7 @@ import {
existsSync, existsSync,
mkdirSync, mkdirSync,
} from 'fs' } from 'fs'
import { dump } from 'js-yaml' import { dump, load } from 'js-yaml'
const isMac = process.platform === 'darwin' const isMac = process.platform === 'darwin'
export function handleAppIPCs() { export function handleAppIPCs() {
@ -209,7 +209,7 @@ export function handleAppIPCs() {
ipcMain.handle(NativeRoute.openAppLog, async (_event): Promise<void> => { ipcMain.handle(NativeRoute.openAppLog, async (_event): Promise<void> => {
const configuration = getAppConfigurations() const configuration = getAppConfigurations()
const dataFolder = configuration.data_folder const dataFolder = configuration.dataFolderPath
try { try {
const errorMessage = await shell.openPath(join(dataFolder)) const errorMessage = await shell.openPath(join(dataFolder))
@ -224,11 +224,14 @@ export function handleAppIPCs() {
}) })
ipcMain.handle(NativeRoute.syncModelFileToCortex, async (_event) => { 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) const allModelFolders = readdirSync(janModelFolderPath)
// Latest app configs
const configration = getAppConfigurations() const configration = getAppConfigurations()
const destinationFolderPath = join(configration.data_folder, 'models') const destinationFolderPath = join(configration.dataFolderPath, 'models')
if (!existsSync(destinationFolderPath)) mkdirSync(destinationFolderPath) if (!existsSync(destinationFolderPath)) mkdirSync(destinationFolderPath)
@ -332,7 +335,7 @@ export function handleAppIPCs() {
ipcMain.handle( ipcMain.handle(
NativeRoute.getAllMessagesAndThreads, NativeRoute.getAllMessagesAndThreads,
async (_event): Promise<any> => { async (_event): Promise<any> => {
const janThreadFolderPath = join(getJanDataFolderPath(), 'threads') const janThreadFolderPath = join(legacyDataPath(), 'threads')
// check if exist // check if exist
if (!existsSync(janThreadFolderPath)) { if (!existsSync(janThreadFolderPath)) {
return { return {
@ -382,7 +385,7 @@ export function handleAppIPCs() {
ipcMain.handle( ipcMain.handle(
NativeRoute.getAllLocalModels, NativeRoute.getAllLocalModels,
async (_event): Promise<boolean> => { async (_event): Promise<boolean> => {
const janModelsFolderPath = join(getJanDataFolderPath(), 'models') const janModelsFolderPath = join(legacyDataPath(), 'models')
if (!existsSync(janModelsFolderPath)) { if (!existsSync(janModelsFolderPath)) {
console.debug('No local models found') console.debug('No local models found')
@ -408,4 +411,50 @@ export function handleAppIPCs() {
return hasLocalModels 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
})
} }

View File

@ -89,7 +89,7 @@ app
.then(() => killProcessesOnPort(cortexJsPort)) .then(() => killProcessesOnPort(cortexJsPort))
.then(() => { .then(() => {
const appConfiguration = getAppConfigurations() const appConfiguration = getAppConfigurations()
const janDataFolder = appConfiguration.data_folder const janDataFolder = appConfiguration.dataFolderPath
start('jan', host, cortexJsPort, cortexCppPort, janDataFolder) start('jan', host, cortexJsPort, cortexCppPort, janDataFolder)
}) })

View File

@ -8,7 +8,7 @@ class TrayManager {
createSystemTray = () => { createSystemTray = () => {
// Feature Toggle for Quick Ask // Feature Toggle for Quick Ask
if (!getAppConfigurations().quick_ask) return if (!getAppConfigurations().quickAsk) return
if (this.currentTray) { if (this.currentTray) {
return return

View File

@ -73,7 +73,7 @@ class WindowManager {
windowManager.mainWindow?.on('close', function (evt) { windowManager.mainWindow?.on('close', function (evt) {
// Feature Toggle for Quick Ask // Feature Toggle for Quick Ask
if (!getAppConfigurations().quick_ask) return if (!getAppConfigurations().quickAsk) return
if (!isAppQuitting) { if (!isAppQuitting) {
evt.preventDefault() evt.preventDefault()

View File

@ -3,9 +3,8 @@
* @module preload * @module preload
*/ */
import { APIEvents, APIRoutes, AppConfiguration } from '@janhq/core/node' import { APIEvents, APIRoutes } from '@janhq/core/node'
import { contextBridge, ipcRenderer } from 'electron' import { contextBridge, ipcRenderer } from 'electron'
import { readdirSync } from 'fs'
const interfaces: { [key: string]: (...args: any[]) => any } = {} const interfaces: { [key: string]: (...args: any[]) => any } = {}
@ -25,32 +24,7 @@ APIEvents.forEach((method) => {
interfaces[method] = (handler: any) => ipcRenderer.on(method, handler) 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' // Expose the 'interfaces' object in the main world under the name 'electronAPI'
// This allows the renderer process to access these methods directly // This allows the renderer process to access these methods directly

View File

@ -3,14 +3,19 @@ import { existsSync, writeFileSync, readFileSync } from 'fs'
import { join } from 'path' import { join } from 'path'
import { AppConfiguration } from '@janhq/core/node' import { AppConfiguration } from '@janhq/core/node'
import os from 'os' import os from 'os'
import { dump, load } from 'js-yaml'
const configurationFileName = 'settings.json' const configurationFileName = '.janrc'
const defaultJanDataFolder = join(os.homedir(), 'jan') const defaultJanDataFolder = join(os.homedir(), 'jan')
const defaultAppConfig: AppConfiguration = { const defaultAppConfig: AppConfiguration = {
data_folder: defaultJanDataFolder, dataFolderPath: defaultJanDataFolder,
quick_ask: false, quickAsk: true,
cortexCppHost: '127.0.0.1',
cortexCppPort: 3940,
apiServerHost: '127.0.0.1',
apiServerPort: 1338
} }
export async function createUserSpace(): Promise<void> { export async function createUserSpace(): Promise<void> {
@ -66,15 +71,14 @@ export const getAppConfigurations = (): AppConfiguration => {
console.debug( console.debug(
`App config not found, creating default config at ${configurationFile}` `App config not found, creating default config at ${configurationFile}`
) )
writeFileSync(configurationFile, JSON.stringify(defaultAppConfig)) writeFileSync(configurationFile, dump(defaultAppConfig))
return defaultAppConfig return defaultAppConfig
} }
try { try {
const appConfigurations: AppConfiguration = JSON.parse( const configYaml = readFileSync(configurationFile, 'utf-8')
readFileSync(configurationFile, 'utf-8') const appConfigurations = load(configYaml) as AppConfiguration
) console.debug('app config', appConfigurations)
console.debug('app config', JSON.stringify(appConfigurations))
return appConfigurations return appConfigurations
} catch (err) { } catch (err) {
console.error( console.error(
@ -84,12 +88,15 @@ export const getAppConfigurations = (): AppConfiguration => {
} }
} }
const getConfigurationFilePath = () => // Get configuration file path of the application
join( const getConfigurationFilePath = () => {
global.core?.appPath() || const homeDir = os.homedir();
process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME'], const configPath = join(
configurationFileName homeDir,
) configurationFileName,
);
return configPath
}
export const updateAppConfiguration = ( export const updateAppConfiguration = (
configuration: AppConfiguration configuration: AppConfiguration
@ -100,7 +107,7 @@ export const updateAppConfiguration = (
configurationFile configurationFile
) )
writeFileSync(configurationFile, JSON.stringify(configuration)) writeFileSync(configurationFile, dump(configuration))
return Promise.resolve() return Promise.resolve()
} }
@ -110,6 +117,21 @@ export const updateAppConfiguration = (
* @returns {string} The data folder path. * @returns {string} The data folder path.
*/ */
export const getJanDataFolderPath = (): string => { export const getJanDataFolderPath = (): string => {
const appConfigurations = getAppConfigurations() return getAppConfigurations().dataFolderPath
return appConfigurations.data_folder }
// 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
} }

View File

@ -5,7 +5,7 @@ import { windowManager } from '../managers/window'
const quickAskHotKey = 'CommandOrControl+J' const quickAskHotKey = 'CommandOrControl+J'
export function registerGlobalShortcuts() { export function registerGlobalShortcuts() {
if (!getAppConfigurations().quick_ask) return if (!getAppConfigurations().quickAsk) return
const ret = registerShortcut(quickAskHotKey, (selectedText: string) => { const ret = registerShortcut(quickAskHotKey, (selectedText: string) => {
// Feature Toggle for Quick Ask // Feature Toggle for Quick Ask
if (!windowManager.isQuickAskWindowVisible()) { if (!windowManager.isQuickAskWindowVisible()) {

View File

@ -2,8 +2,6 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { AppConfiguration } from '@janhq/core'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import ClipboardListener from '@/containers/Providers/ClipboardListener' import ClipboardListener from '@/containers/Providers/ClipboardListener'
@ -29,10 +27,8 @@ export default function RootLayout() {
}, []) }, [])
useEffect(() => { useEffect(() => {
window.core?.api window.electronAPI?.appDataFolder()?.then((path: string) => {
?.getAppConfigurations() setJanDataFolderPath(path)
?.then((appConfig: AppConfiguration) => {
setJanDataFolderPath(appConfig.data_folder)
}) })
}, [setJanDataFolderPath]) }, [setJanDataFolderPath])

View File

@ -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 { Button, Input } from '@janhq/joi'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtom, useSetAtom } from 'jotai'
import { PencilIcon, FolderOpenIcon } from 'lucide-react' import { PencilIcon, FolderOpenIcon } from 'lucide-react'
import Loader from '@/containers/Loader' import Loader from '@/containers/Loader'
@ -29,8 +31,18 @@ const DataFolder = () => {
const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom) const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom)
const showDestNotEmptyConfirm = useSetAtom(showDestNotEmptyConfirmAtom) 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 [destinationPath, setDestinationPath] = useState(undefined)
const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
useEffect(() => {
getAppDataFolder()
}, [getAppDataFolder])
const onChangeFolderClick = useCallback(async () => { const onChangeFolderClick = useCallback(async () => {
const destFolder = await window.core?.api?.selectDirectory() const destFolder = await window.core?.api?.selectDirectory()
@ -41,14 +53,18 @@ const DataFolder = () => {
return return
} }
// const appConfiguration: AppConfiguration = const currentJanDataFolder = await window.electronAPI?.appDataFolder()
// await window.core?.api?.getAppConfigurations()
// const currentJanDataFolder = appConfiguration.data_folder
// if (await isSubdirectory(currentJanDataFolder, destFolder)) { const relativePath = relative(currentJanDataFolder, destFolder)
// setShowSameDirectory(true)
// return if (
// } relativePath &&
!relativePath.startsWith('..') &&
!isAbsolute(relativePath)
) {
setShowSameDirectory(true)
return
}
const isEmpty: boolean = const isEmpty: boolean =
await window.core?.api?.isDirectoryEmpty(destFolder) await window.core?.api?.isDirectoryEmpty(destFolder)
@ -106,7 +122,9 @@ const DataFolder = () => {
<FolderOpenIcon <FolderOpenIcon
size={16} size={16}
className="absolute right-2 top-1/2 z-10 -translate-y-1/2 cursor-pointer" className="absolute right-2 top-1/2 z-10 -translate-y-1/2 cursor-pointer"
onClick={() => window.core?.api?.openAppDirectory()} onClick={() =>
window.electronAPI?.openFileExplorer(janDataFolderPath)
}
/> />
</div> </div>
<Button <Button

View File

@ -13,6 +13,7 @@ import { toaster } from '@/containers/Toast'
import useModelStop from '@/hooks/useModelStop' import useModelStop from '@/hooks/useModelStop'
import { useSettings } from '@/hooks/useSettings' import { useSettings } from '@/hooks/useSettings'
import DataFolder from './DataFolder'
import CopyOverInstructionItem from './components/CopyOverInstruction' import CopyOverInstructionItem from './components/CopyOverInstruction'
import DataMigration from './components/DataMigration' import DataMigration from './components/DataMigration'
@ -79,7 +80,7 @@ const Advanced = () => {
) => { ) => {
const appConfiguration: AppConfiguration = const appConfiguration: AppConfiguration =
await window.core?.api?.getAppConfigurations() await window.core?.api?.getAppConfigurations()
appConfiguration.quick_ask = e appConfiguration.quickAsk = e
await window.core?.api?.updateAppConfiguration(appConfiguration) await window.core?.api?.updateAppConfiguration(appConfiguration)
if (relaunch) window.core?.api?.relaunch() if (relaunch) window.core?.api?.relaunch()
} }
@ -365,7 +366,7 @@ const Advanced = () => {
{/* </div> */} {/* </div> */}
{/* )} */} {/* )} */}
{/* <DataFolder /> */} <DataFolder />
{/* Proxy */} {/* Proxy */}
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row"> <div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">