feat: change data folder (#3309)
This commit is contained in:
parent
b348110fb7
commit
b43242b9b2
@ -35,6 +35,9 @@ export enum NativeRoute {
|
||||
syncModelFileToCortex = 'syncModelFileToCortex',
|
||||
|
||||
openAppLog = 'openAppLog',
|
||||
appDataFolder = 'appDataFolder',
|
||||
changeDataFolder = 'changeDataFolder',
|
||||
isDirectoryEmpty = 'isDirectoryEmpty',
|
||||
}
|
||||
|
||||
export enum AppEvent {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user