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',
openAppLog = 'openAppLog',
appDataFolder = 'appDataFolder',
changeDataFolder = 'changeDataFolder',
isDirectoryEmpty = 'isDirectoryEmpty',
}
export enum AppEvent {

View File

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

View File

@ -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<void> => {
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<any> => {
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<boolean> => {
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
})
}

View File

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

View File

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

View File

@ -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()

View File

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

View File

@ -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<void> {
@ -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
}

View File

@ -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()) {

View File

@ -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(() => {

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 { 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 = () => {
<FolderOpenIcon
size={16}
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>
<Button

View File

@ -13,6 +13,7 @@ import { toaster } from '@/containers/Toast'
import useModelStop from '@/hooks/useModelStop'
import { useSettings } from '@/hooks/useSettings'
import DataFolder from './DataFolder'
import CopyOverInstructionItem from './components/CopyOverInstruction'
import DataMigration from './components/DataMigration'
@ -79,7 +80,7 @@ const Advanced = () => {
) => {
const appConfiguration: AppConfiguration =
await window.core?.api?.getAppConfigurations()
appConfiguration.quick_ask = e
appConfiguration.quickAsk = e
await window.core?.api?.updateAppConfiguration(appConfiguration)
if (relaunch) window.core?.api?.relaunch()
}
@ -365,7 +366,7 @@ const Advanced = () => {
{/* </div> */}
{/* )} */}
{/* <DataFolder /> */}
<DataFolder />
{/* 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">