feat: allow user to move jan folder (#1649)

* feat: allow user to move jan folder

Signed-off-by: James <james@jan.ai>

---------

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
NamH 2024-01-22 14:37:46 +07:00 committed by GitHub
parent a3f2a16cb4
commit 4cf47777e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 446 additions and 176 deletions

7
core/.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "auto"
}

View File

@ -7,12 +7,16 @@ export enum AppRoute {
openExternalUrl = 'openExternalUrl',
openAppDirectory = 'openAppDirectory',
openFileExplore = 'openFileExplorer',
selectDirectory = 'selectDirectory',
getAppConfigurations = 'getAppConfigurations',
updateAppConfiguration = 'updateAppConfiguration',
relaunch = 'relaunch',
joinPath = 'joinPath',
baseName = 'baseName',
startServer = 'startServer',
stopServer = 'stopServer',
log = 'log'
log = 'log',
logServer = 'logServer',
}
export enum AppEvent {
@ -55,7 +59,7 @@ export enum FileSystemRoute {
}
export enum FileManagerRoute {
syncFile = 'syncFile',
getUserSpace = 'getUserSpace',
getJanDataFolderPath = 'getJanDataFolderPath',
getResourcePath = 'getResourcePath',
fileStat = 'fileStat',
}

View File

@ -35,10 +35,11 @@ const abortDownload: (fileName: string) => Promise<any> = (fileName) =>
global.core.api?.abortDownload(fileName)
/**
* Gets the user space path.
* @returns {Promise<any>} A Promise that resolves with the user space path.
* Gets Jan's data folder path.
*
* @returns {Promise<string>} A Promise that resolves with Jan's data folder path.
*/
const getUserSpace = (): Promise<string> => global.core.api?.getUserSpace()
const getJanDataFolderPath = (): Promise<string> => global.core.api?.getJanDataFolderPath()
/**
* Opens the file explorer at a specific path.
@ -103,12 +104,12 @@ export {
executeOnMain,
downloadFile,
abortDownload,
getUserSpace,
getJanDataFolderPath,
openFileExplorer,
getResourcePath,
joinPath,
openExternalUrl,
baseName,
log,
FileStat
FileStat,
}

View File

@ -2,13 +2,10 @@ import fs from 'fs'
import { JanApiRouteConfiguration, RouteConfiguration } from './configuration'
import { join } from 'path'
import { ContentType, MessageStatus, Model, ThreadMessage } from './../../../index'
const os = require('os')
const path = join(os.homedir(), 'jan')
import { getJanDataFolderPath } from '../../utils'
export const getBuilder = async (configuration: RouteConfiguration) => {
const directoryPath = join(path, configuration.dirName)
const directoryPath = join(getJanDataFolderPath(), configuration.dirName)
try {
if (!fs.existsSync(directoryPath)) {
console.debug('model folder not found')
@ -72,7 +69,7 @@ export const deleteBuilder = async (configuration: RouteConfiguration, id: strin
}
}
const directoryPath = join(path, configuration.dirName)
const directoryPath = join(getJanDataFolderPath(), configuration.dirName)
try {
const data = await retrieveBuilder(configuration, id)
if (!data) {
@ -94,7 +91,7 @@ export const deleteBuilder = async (configuration: RouteConfiguration, id: strin
}
export const getMessages = async (threadId: string): Promise<ThreadMessage[]> => {
const threadDirPath = join(path, 'threads', threadId)
const threadDirPath = join(getJanDataFolderPath(), 'threads', threadId)
const messageFile = 'messages.jsonl'
try {
const files: string[] = fs.readdirSync(threadDirPath)
@ -155,7 +152,7 @@ export const createThread = async (thread: any) => {
created: Date.now(),
updated: Date.now(),
}
const threadDirPath = join(path, 'threads', updatedThread.id)
const threadDirPath = join(getJanDataFolderPath(), 'threads', updatedThread.id)
const threadJsonPath = join(threadDirPath, threadMetadataFileName)
if (!fs.existsSync(threadDirPath)) {
@ -189,7 +186,7 @@ export const updateThread = async (threadId: string, thread: any) => {
updated: Date.now(),
}
try {
const threadDirPath = join(path, 'threads', updatedThread.id)
const threadDirPath = join(getJanDataFolderPath(), 'threads', updatedThread.id)
const threadJsonPath = join(threadDirPath, threadMetadataFileName)
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2))
@ -231,7 +228,7 @@ export const createMessage = async (threadId: string, message: any) => {
],
}
const threadDirPath = join(path, 'threads', threadId)
const threadDirPath = join(getJanDataFolderPath(), 'threads', threadId)
const threadMessagePath = join(threadDirPath, threadMessagesFileName)
if (!fs.existsSync(threadDirPath)) {
@ -246,9 +243,12 @@ export const createMessage = async (threadId: string, message: any) => {
}
}
export const downloadModel = async (modelId: string, network?: { proxy?: string, ignoreSSL?: boolean }) => {
const strictSSL = !network?.ignoreSSL;
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined;
export const downloadModel = async (
modelId: string,
network?: { proxy?: string; ignoreSSL?: boolean }
) => {
const strictSSL = !network?.ignoreSSL
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined
const model = await retrieveBuilder(JanApiRouteConfiguration.models, modelId)
if (!model || model.object !== 'model') {
return {
@ -256,7 +256,7 @@ export const downloadModel = async (modelId: string, network?: { proxy?: string,
}
}
const directoryPath = join(path, 'models', modelId)
const directoryPath = join(getJanDataFolderPath(), 'models', modelId)
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath)
}
@ -265,7 +265,7 @@ export const downloadModel = async (modelId: string, network?: { proxy?: string,
const modelBinaryPath = join(directoryPath, modelId)
const request = require('request')
const rq = request({url: model.source_url, strictSSL, proxy })
const rq = request({ url: model.source_url, strictSSL, proxy })
const progress = require('request-progress')
progress(rq, {})
.on('progress', function (state: any) {
@ -316,7 +316,7 @@ export const chatCompletions = async (request: any, reply: any) => {
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'Connection': 'keep-alive',
})
const headers: Record<string, any> = {
@ -347,7 +347,7 @@ const getEngineConfiguration = async (engineId: string) => {
if (engineId !== 'openai') {
return undefined
}
const directoryPath = join(path, 'engines')
const directoryPath = join(getJanDataFolderPath(), 'engines')
const filePath = join(directoryPath, `${engineId}.json`)
const data = await fs.readFileSync(filePath, 'utf-8')
return JSON.parse(data)

View File

@ -1,9 +1,9 @@
import { DownloadRoute } from "../../../api";
import { join } from "path";
import { userSpacePath } from "../../extension/manager";
import { DownloadManager } from "../../download";
import { HttpServer } from "../HttpServer";
import { createWriteStream } from "fs";
import { DownloadRoute } from '../../../api'
import { join } from 'path'
import { DownloadManager } from '../../download'
import { HttpServer } from '../HttpServer'
import { createWriteStream } from 'fs'
import { getJanDataFolderPath } from '../../utils'
import { normalizeFilePath } from "../../path";
export const downloadRouter = async (app: HttpServer) => {
@ -13,7 +13,7 @@ export const downloadRouter = async (app: HttpServer) => {
const body = JSON.parse(req.body as any);
const normalizedArgs = body.map((arg: any) => {
if (typeof arg === "string") {
return join(userSpacePath, normalizeFilePath(arg));
return join(getJanDataFolderPath(), normalizeFilePath(arg));
}
return arg;
});
@ -44,7 +44,7 @@ export const downloadRouter = async (app: HttpServer) => {
const body = JSON.parse(req.body as any);
const normalizedArgs = body.map((arg: any) => {
if (typeof arg === "string") {
return join(userSpacePath, normalizeFilePath(arg));
return join(getJanDataFolderPath(), normalizeFilePath(arg));
}
return arg;
});

View File

@ -1,20 +1,20 @@
import { join, extname } from 'path'
import { ExtensionRoute } from '../../../api/index'
import { userSpacePath } from '../../extension/manager'
import { ModuleManager } from '../../module'
import { getActiveExtensions, installExtensions } from '../../extension/store'
import { HttpServer } from '../HttpServer'
import { readdirSync } from 'fs'
import { getJanExtensionsPath } from '../../utils'
export const extensionRouter = async (app: HttpServer) => {
// TODO: Share code between node projects
app.post(`/${ExtensionRoute.getActiveExtensions}`, async (req, res) => {
app.post(`/${ExtensionRoute.getActiveExtensions}`, async (_req, res) => {
const activeExtensions = await getActiveExtensions()
res.status(200).send(activeExtensions)
})
app.post(`/${ExtensionRoute.baseExtensions}`, async (req, res) => {
app.post(`/${ExtensionRoute.baseExtensions}`, async (_req, res) => {
const baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install')
const extensions = readdirSync(baseExtensionPath)
.filter((file) => extname(file) === '.tgz')
@ -23,7 +23,7 @@ export const extensionRouter = async (app: HttpServer) => {
res.status(200).send(extensions)
})
app.post(`/${ExtensionRoute.installExtension}`, async (req, res) => {
app.post(`/${ExtensionRoute.installExtension}`, async (req) => {
const extensions = req.body as any
const installed = await installExtensions(JSON.parse(extensions)[0])
return JSON.parse(JSON.stringify(installed))
@ -32,7 +32,7 @@ export const extensionRouter = async (app: HttpServer) => {
app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => {
const args = JSON.parse(req.body as any)
console.debug(args)
const module = await import(join(userSpacePath, 'extensions', args[0]))
const module = await import(join(getJanExtensionsPath(), args[0]))
ModuleManager.instance.setModule(args[0], module)
const method = args[1]

View File

@ -4,7 +4,7 @@ import { HttpServer } from '../../index'
export const fsRouter = async (app: HttpServer) => {
app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {})
app.post(`/app/${FileManagerRoute.getUserSpace}`, async (request: any, reply: any) => {})
app.post(`/app/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) => {})
app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {})

View File

@ -1,7 +1,7 @@
import { FileSystemRoute } from '../../../api'
import { join } from 'path'
import { HttpServer } from '../HttpServer'
import { userSpacePath } from '../../extension/manager'
import { getJanDataFolderPath } from '../../utils'
export const fsRouter = async (app: HttpServer) => {
const moduleName = 'fs'
@ -14,7 +14,7 @@ export const fsRouter = async (app: HttpServer) => {
return mdl[route](
...body.map((arg: any) =>
typeof arg === 'string' && arg.includes('file:/')
? join(userSpacePath, arg.replace('file:/', ''))
? join(getJanDataFolderPath(), arg.replace('file:/', ''))
: arg,
),
)

View File

@ -103,7 +103,7 @@ export default class Extension {
const pacote = await import('pacote')
await pacote.extract(
this.specifier,
join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''),
join(ExtensionManager.instance.getExtensionsPath() ?? '', this.name ?? ''),
this.installOptions,
)
@ -166,9 +166,9 @@ export default class Extension {
* @returns the latest available version if a new version is available or false if not.
*/
async isUpdateAvailable() {
return import('pacote').then((pacote) => {
if (this.origin) {
return pacote.manifest(this.origin).then((mnf) => {
return import('pacote').then((pacote) => {
if (this.origin) {
return pacote.manifest(this.origin).then((mnf) => {
return mnf.version !== this.version ? mnf.version : false
})
}
@ -179,8 +179,9 @@ export default class Extension {
* Remove extension and refresh renderers.
* @returns {Promise}
*/
async uninstall() {
const extPath = resolve(ExtensionManager.instance.extensionsPath ?? '', this.name ?? '')
async uninstall(): Promise<void> {
const path = ExtensionManager.instance.getExtensionsPath()
const extPath = resolve(path ?? '', this.name ?? '')
await rmdirSync(extPath, { recursive: true })
this.emitUpdate()

View File

@ -35,17 +35,17 @@ async function registerExtensionProtocol() {
let electron: any = undefined
try {
const moduleName = "electron"
const moduleName = 'electron'
electron = await import(moduleName)
} catch (err) {
console.error('Electron is not available')
}
const extensionPath = ExtensionManager.instance.getExtensionsPath()
if (electron) {
return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => {
const entry = request.url.substr('extension://'.length - 1)
const url = normalize(ExtensionManager.instance.extensionsPath + entry)
const url = normalize(extensionPath + entry)
callback({ path: url })
})
}
@ -120,7 +120,7 @@ function loadExtension(ext: any) {
* @returns {extensionManager} A set of functions used to manage the extension lifecycle.
*/
export function getStore() {
if (!ExtensionManager.instance.extensionsPath) {
if (!ExtensionManager.instance.getExtensionsFile()) {
throw new Error(
'The extension path has not yet been set up. Please run useExtensions before accessing the store',
)
@ -133,4 +133,4 @@ export function getStore() {
getActiveExtensions,
removeExtension,
}
}
}

View File

@ -1,44 +1,45 @@
import { join, resolve } from "path";
import { join, resolve } from 'path'
import { existsSync, mkdirSync, writeFileSync } from 'fs'
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { homedir } from "os"
/**
* Manages extension installation and migration.
*/
export const userSpacePath = join(homedir(), "jan");
export class ExtensionManager {
public static instance: ExtensionManager = new ExtensionManager();
public static instance: ExtensionManager = new ExtensionManager()
extensionsPath: string | undefined = join(userSpacePath, "extensions");
private extensionsPath: string | undefined
constructor() {
if (ExtensionManager.instance) {
return ExtensionManager.instance;
return ExtensionManager.instance
}
}
getExtensionsPath(): string | undefined {
return this.extensionsPath
}
setExtensionsPath(extPath: string) {
// Create folder if it does not exist
let extDir;
let extDir
try {
extDir = resolve(extPath);
if (extDir.length < 2) throw new Error();
extDir = resolve(extPath)
if (extDir.length < 2) throw new Error()
if (!existsSync(extDir)) mkdirSync(extDir);
if (!existsSync(extDir)) mkdirSync(extDir)
const extensionsJson = join(extDir, "extensions.json");
if (!existsSync(extensionsJson))
writeFileSync(extensionsJson, "{}");
const extensionsJson = join(extDir, 'extensions.json')
if (!existsSync(extensionsJson)) writeFileSync(extensionsJson, '{}')
this.extensionsPath = extDir;
this.extensionsPath = extDir
} catch (error) {
throw new Error("Invalid path provided to the extensions folder");
throw new Error('Invalid path provided to the extensions folder')
}
}
getExtensionsFile() {
return join(this.extensionsPath ?? "", "extensions.json");
return join(this.extensionsPath ?? '', 'extensions.json')
}
}

View File

@ -6,4 +6,5 @@ export * from './download'
export * from './module'
export * from './api'
export * from './log'
export * from './utils'
export * from './path'

View File

@ -1,22 +1,35 @@
import fs from 'fs'
import util from 'util'
import path from 'path'
import os from 'os'
import { getAppLogPath, getServerLogPath } from './utils'
export const logDir = path.join(os.homedir(), 'jan', 'logs')
export const log = function (message: string, fileName: string = 'app.log') {
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
export const log = function (message: string) {
const appLogPath = getAppLogPath()
if (!message.startsWith('[')) {
message = `[APP]::${message}`
}
message = `${new Date().toISOString()} ${message}`
if (fs.existsSync(logDir)) {
var log_file = fs.createWriteStream(path.join(logDir, fileName), {
if (fs.existsSync(appLogPath)) {
var log_file = fs.createWriteStream(appLogPath, {
flags: 'a',
})
log_file.write(util.format(message) + '\n')
log_file.close()
console.debug(message)
}
}
export const logServer = function (message: string) {
const serverLogPath = getServerLogPath()
if (!message.startsWith('[')) {
message = `[SERVER]::${message}`
}
message = `${new Date().toISOString()} ${message}`
if (fs.existsSync(serverLogPath)) {
var log_file = fs.createWriteStream(serverLogPath, {
flags: 'a',
})
log_file.write(util.format(message) + '\n')

View File

@ -0,0 +1,103 @@
import { AppConfiguration } from "../../types";
import { join } from "path";
import fs from "fs";
import os from "os";
// TODO: move this to core
const configurationFileName = "settings.json";
// TODO: do no specify app name in framework module
const defaultJanDataFolder = join(os.homedir(), "jan");
const defaultAppConfig: AppConfiguration = {
data_folder: defaultJanDataFolder,
};
/**
* Getting App Configurations.
*
* @returns {AppConfiguration} The app configurations.
*/
export const getAppConfigurations = (): AppConfiguration => {
// Retrieve Application Support folder path
// Fallback to user home directory if not found
const configurationFile = getConfigurationFilePath();
if (!fs.existsSync(configurationFile)) {
// create default app config if we don't have one
console.debug(`App config not found, creating default config at ${configurationFile}`);
fs.writeFileSync(configurationFile, JSON.stringify(defaultAppConfig));
return defaultAppConfig;
}
try {
const appConfigurations: AppConfiguration = JSON.parse(
fs.readFileSync(configurationFile, "utf-8"),
);
return appConfigurations;
} catch (err) {
console.error(`Failed to read app config, return default config instead! Err: ${err}`);
return defaultAppConfig;
}
};
const getConfigurationFilePath = () =>
join(
global.core?.appPath() || process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"],
configurationFileName,
);
export const updateAppConfiguration = (configuration: AppConfiguration): Promise<void> => {
const configurationFile = getConfigurationFilePath();
console.debug("updateAppConfiguration, configurationFile: ", configurationFile);
fs.writeFileSync(configurationFile, JSON.stringify(configuration));
return Promise.resolve();
};
/**
* Utility function to get server log path
*
* @returns {string} The log path.
*/
export const getServerLogPath = (): string => {
const appConfigurations = getAppConfigurations();
const logFolderPath = join(appConfigurations.data_folder, "logs");
if (!fs.existsSync(logFolderPath)) {
fs.mkdirSync(logFolderPath, { recursive: true });
}
return join(logFolderPath, "server.log");
};
/**
* Utility function to get app log path
*
* @returns {string} The log path.
*/
export const getAppLogPath = (): string => {
const appConfigurations = getAppConfigurations();
const logFolderPath = join(appConfigurations.data_folder, "logs");
if (!fs.existsSync(logFolderPath)) {
fs.mkdirSync(logFolderPath, { recursive: true });
}
return join(logFolderPath, "app.log");
};
/**
* Utility function to get data folder path
*
* @returns {string} The data folder path.
*/
export const getJanDataFolderPath = (): string => {
const appConfigurations = getAppConfigurations();
return appConfigurations.data_folder;
};
/**
* Utility function to get extension path
*
* @returns {string} The extensions path.
*/
export const getJanExtensionsPath = (): string => {
const appConfigurations = getAppConfigurations();
return join(appConfigurations.data_folder, "extensions");
};

View File

@ -0,0 +1,3 @@
export type AppConfiguration = {
data_folder: string
}

View File

@ -0,0 +1 @@
export * from './appConfigEntity'

View File

@ -5,3 +5,4 @@ export * from './message'
export * from './inference'
export * from './monitoring'
export * from './file'
export * from './config'

View File

@ -34,5 +34,5 @@ module.exports = {
{ name: 'Link', linkAttribute: 'to' },
],
},
ignorePatterns: ['build', 'renderer', 'node_modules'],
ignorePatterns: ['build', 'renderer', 'node_modules', '@global'],
}

10
electron/@global/index.ts Normal file
View File

@ -0,0 +1,10 @@
export {}
declare global {
namespace NodeJS {
interface Global {
core: any
}
}
var core: any | undefined
}

View File

@ -1,10 +1,19 @@
import { app, ipcMain, shell } from 'electron'
import { app, ipcMain, dialog, shell } from 'electron'
import { join, basename } from 'path'
import { WindowManager } from './../managers/window'
import { getResourcePath, userSpacePath } from './../utils/path'
import { AppRoute } from '@janhq/core'
import { ModuleManager, init, log } from '@janhq/core/node'
import { getResourcePath } from './../utils/path'
import { AppRoute, AppConfiguration } from '@janhq/core'
import { ServerConfig, startServer, stopServer } from '@janhq/server'
import {
ModuleManager,
getJanDataFolderPath,
getJanExtensionsPath,
init,
log,
logServer,
getAppConfigurations,
updateAppConfiguration,
} from '@janhq/core/node'
export function handleAppIPCs() {
/**
@ -13,7 +22,7 @@ export function handleAppIPCs() {
* @param _event - The IPC event object.
*/
ipcMain.handle(AppRoute.openAppDirectory, async (_event) => {
shell.openPath(userSpacePath)
shell.openPath(getJanDataFolderPath())
})
/**
@ -76,7 +85,7 @@ export function handleAppIPCs() {
* @param _event - The IPC event object.
* @param url - The URL to reload.
*/
ipcMain.handle(AppRoute.relaunch, async (_event, url) => {
ipcMain.handle(AppRoute.relaunch, async (_event) => {
ModuleManager.instance.clearImportedModules()
if (app.isPackaged) {
@ -85,7 +94,7 @@ export function handleAppIPCs() {
} else {
for (const modulePath in ModuleManager.instance.requiredModules) {
delete require.cache[
require.resolve(join(userSpacePath, 'extensions', modulePath))
require.resolve(join(getJanExtensionsPath(), modulePath))
]
}
init({
@ -94,7 +103,7 @@ export function handleAppIPCs() {
return true
},
// Path to install extension to
extensionsPath: join(userSpacePath, 'extensions'),
extensionsPath: getJanExtensionsPath(),
})
WindowManager.instance.currentWindow?.reload()
}
@ -103,7 +112,41 @@ export function handleAppIPCs() {
/**
* Log message to log file.
*/
ipcMain.handle(AppRoute.log, async (_event, message, fileName) =>
log(message, fileName)
ipcMain.handle(AppRoute.log, async (_event, message) => log(message))
/**
* Log message to log file.
*/
ipcMain.handle(AppRoute.logServer, async (_event, message) =>
logServer(message)
)
ipcMain.handle(AppRoute.selectDirectory, async () => {
const mainWindow = WindowManager.instance.currentWindow
if (!mainWindow) {
console.error('No main window found')
return
}
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Select a folder',
buttonLabel: 'Select Folder',
properties: ['openDirectory', 'createDirectory'],
})
if (canceled) {
return
} else {
return filePaths[0]
}
})
ipcMain.handle(AppRoute.getAppConfigurations, async () =>
getAppConfigurations()
)
ipcMain.handle(
AppRoute.updateAppConfiguration,
async (_event, appConfiguration: AppConfiguration) => {
await updateAppConfiguration(appConfiguration)
}
)
}

View File

@ -1,11 +1,11 @@
import { app, ipcMain } from 'electron'
import { resolve, join } from 'path'
import { ipcMain } from 'electron'
import { resolve } from 'path'
import { WindowManager } from './../managers/window'
import request from 'request'
import { createWriteStream, renameSync } from 'fs'
import { DownloadEvent, DownloadRoute } from '@janhq/core'
const progress = require('request-progress')
import { DownloadManager, normalizeFilePath } from '@janhq/core/node'
import { DownloadManager, getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
export function handleDownloaderIPCs() {
/**
@ -61,11 +61,11 @@ export function handleDownloaderIPCs() {
const proxy = network?.proxy?.startsWith('http')
? network.proxy
: undefined
const userDataPath = join(app.getPath('home'), 'jan')
if (typeof fileName === 'string') {
fileName = normalizeFilePath(fileName)
}
const destination = resolve(userDataPath, fileName)
const destination = resolve(getJanDataFolderPath(), fileName)
const rq = request({ url, strictSSL, proxy })
// Put request to download manager instance

View File

@ -7,10 +7,11 @@ import {
getExtension,
removeExtension,
getActiveExtensions,
ModuleManager
ModuleManager,
getJanExtensionsPath,
} from '@janhq/core/node'
import { getResourcePath, userSpacePath } from './../utils/path'
import { getResourcePath } from './../utils/path'
import { ExtensionRoute } from '@janhq/core'
export function handleExtensionIPCs() {
@ -27,7 +28,7 @@ export function handleExtensionIPCs() {
ExtensionRoute.invokeExtensionFunc,
async (_event, modulePath, method, ...args) => {
const module = require(
/* webpackIgnore: true */ join(userSpacePath, 'extensions', modulePath)
/* webpackIgnore: true */ join(getJanExtensionsPath(), modulePath)
)
ModuleManager.instance.setModule(modulePath, module)

View File

@ -2,12 +2,11 @@ import { ipcMain } from 'electron'
// @ts-ignore
import reflect from '@alumna/reflect'
import { FileManagerRoute } from '@janhq/core'
import { userSpacePath, getResourcePath } from './../utils/path'
import { FileManagerRoute, FileStat } from '@janhq/core'
import { getResourcePath } from './../utils/path'
import fs from 'fs'
import { join } from 'path'
import { FileStat } from '@janhq/core'
import { normalizeFilePath } from '@janhq/core/node'
import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
/**
* Handles file system extensions operations.
@ -28,10 +27,10 @@ export function handleFileMangerIPCs() {
}
)
// Handles the 'getUserSpace' IPC event. This event is triggered to get the user space path.
// Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path.
ipcMain.handle(
FileManagerRoute.getUserSpace,
(): Promise<string> => Promise.resolve(userSpacePath)
FileManagerRoute.getJanDataFolderPath,
(): Promise<string> => Promise.resolve(getJanDataFolderPath())
)
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
@ -45,7 +44,7 @@ export function handleFileMangerIPCs() {
async (_event, path: string): Promise<FileStat | undefined> => {
const normalizedPath = normalizeFilePath(path)
const fullPath = join(userSpacePath, normalizedPath)
const fullPath = join(getJanDataFolderPath(), normalizedPath)
const isExist = fs.existsSync(fullPath)
if (!isExist) return undefined

View File

@ -1,9 +1,9 @@
import { ipcMain } from 'electron'
import { FileSystemRoute } from '@janhq/core'
import { userSpacePath } from '../utils/path'
import { join } from 'path'
import { normalizeFilePath } from '@janhq/core/node'
import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
/**
* Handles file system operations.
*/
@ -16,7 +16,7 @@ export function handleFsIPCs() {
...args.map((arg) =>
typeof arg === 'string' &&
(arg.includes(`file:/`) || arg.includes(`file:\\`))
? join(userSpacePath, normalizeFilePath(arg))
? join(getJanDataFolderPath(), normalizeFilePath(arg))
: arg
)
)

View File

@ -1,7 +1,5 @@
import { app, BrowserWindow } from 'electron'
import { join } from 'path'
import { setupMenu } from './utils/menu'
import { createUserSpace } from './utils/path'
/**
* Managers
**/
@ -21,12 +19,16 @@ import { handleFsIPCs } from './handlers/fs'
/**
* Utils
**/
import { setupMenu } from './utils/menu'
import { createUserSpace } from './utils/path'
import { migrateExtensions } from './utils/migration'
import { cleanUpAndQuit } from './utils/clean'
import { setupExtensions } from './utils/extension'
import { setupCore } from './utils/setup'
app
.whenReady()
.then(setupCore)
.then(createUserSpace)
.then(migrateExtensions)
.then(setupExtensions)
@ -94,9 +96,8 @@ function handleIPCs() {
}
/*
** Suppress Node error messages
*/
** Suppress Node error messages
*/
process.on('uncaughtException', function (err) {
// TODO: Write error to log file in #1447
log(`Error: ${err}`)
})

View File

@ -1,13 +1,12 @@
import { init, userSpacePath } from '@janhq/core/node'
import path from 'path'
import { getJanExtensionsPath, init } from '@janhq/core/node'
export const setupExtensions = () => {
export const setupExtensions = async () => {
init({
// Function to check from the main process that user wants to install a extension
confirmInstall: async (_extensions: string[]) => {
return true
},
// Path to install extension to
extensionsPath: path.join(userSpacePath, 'extensions'),
extensionsPath: getJanExtensionsPath(),
})
}

View File

@ -1,9 +1,9 @@
import { app } from 'electron'
import { join } from 'path'
import { rmdir } from 'fs'
import Store from 'electron-store'
import { userSpacePath } from './path'
import { getJanExtensionsPath } from '@janhq/core/node'
/**
* Migrates the extensions by deleting the `extensions` directory in the user data path.
* If the `migrated_version` key in the `Store` object does not match the current app version,
@ -15,9 +15,8 @@ export function migrateExtensions() {
const store = new Store()
if (store.get('migrated_version') !== app.getVersion()) {
console.debug('start migration:', store.get('migrated_version'))
const fullPath = join(userSpacePath, 'extensions')
rmdir(fullPath, { recursive: true }, function (err) {
rmdir(getJanExtensionsPath(), { recursive: true }, function (err) {
if (err) console.error(err)
store.set('migrated_version', app.getVersion())
console.debug('migrate extensions done')

View File

@ -1,13 +1,22 @@
import { join } from 'path'
import { app } from 'electron'
import { mkdir } from 'fs-extra'
import { existsSync } from 'fs'
import { getJanDataFolderPath } from '@janhq/core/node'
export async function createUserSpace(): Promise<void> {
return mkdir(userSpacePath).catch(() => {})
const janDataFolderPath = getJanDataFolderPath()
if (!existsSync(janDataFolderPath)) {
try {
await mkdir(janDataFolderPath)
} catch (err) {
console.error(
`Unable to create Jan data folder at ${janDataFolderPath}: ${err}`
)
}
}
}
export const userSpacePath = join(app.getPath('home'), 'jan')
export function getResourcePath() {
let appPath = join(app.getAppPath(), '..', 'app.asar.unpacked')

9
electron/utils/setup.ts Normal file
View File

@ -0,0 +1,9 @@
import { app } from 'electron'
export const setupCore = async () => {
// Setup core api for main process
global.core = {
// Define appPath function for app to retrieve app path globaly
appPath: () => app.getPath('userData')
}
}

View File

@ -4,11 +4,10 @@ import { ChildProcessWithoutNullStreams, spawn } from "child_process";
import tcpPortUsed from "tcp-port-used";
import fetchRT from "fetch-retry";
import osUtils from "os-utils";
import { log } from "@janhq/core/node";
import { log, getJanDataFolderPath } from "@janhq/core/node";
import { getNitroProcessInfo, updateNvidiaInfo } from "./nvidia";
import { Model, InferenceEngine, ModelSettingParams } from "@janhq/core";
import { executableNitroFile } from "./execute";
import { homedir } from "os";
// Polyfill fetch with retry
const fetchRetry = fetchRT(fetch);
@ -86,7 +85,7 @@ async function runModel(
}
currentModelFile = wrapper.modelFullPath;
const janRoot = path.join(homedir(), "jan");
const janRoot = await getJanDataFolderPath();
if (!currentModelFile.includes(janRoot)) {
currentModelFile = path.join(janRoot, currentModelFile);
}

View File

@ -1,7 +1,7 @@
import { writeFileSync, existsSync, readFileSync } from "fs";
import { exec } from "child_process";
import path from "path";
import { homedir } from "os";
import { getJanDataFolderPath } from "@janhq/core/node";
/**
* Default GPU settings
@ -25,8 +25,7 @@ const DEFALT_SETTINGS = {
* Path to the settings file
**/
export const NVIDIA_INFO_FILE = path.join(
homedir(),
"jan",
getJanDataFolderPath(),
"settings",
"settings.json"
);
@ -40,7 +39,7 @@ let nitroProcessInfo: NitroProcessInfo | undefined = undefined;
* Nitro process info
*/
export interface NitroProcessInfo {
isRunning: boolean
isRunning: boolean;
}
/**

View File

@ -3,11 +3,11 @@ import {
downloadFile,
abortDownload,
getResourcePath,
getUserSpace,
InferenceEngine,
joinPath,
ModelExtension,
Model,
getJanDataFolderPath,
} from '@janhq/core'
/**
@ -39,7 +39,6 @@ export default class JanModelExtension extends ModelExtension {
private async copyModelsToHomeDir() {
try {
// Check for migration conditions
if (
localStorage.getItem(`${EXTENSION_NAME}-version`) === VERSION &&
@ -53,8 +52,8 @@ export default class JanModelExtension extends ModelExtension {
const resourePath = await getResourcePath()
const srcPath = await joinPath([resourePath, 'models'])
const userSpace = await getUserSpace()
const destPath = await joinPath([userSpace, 'models'])
const janDataFolderPath = await getJanDataFolderPath()
const destPath = await joinPath([janDataFolderPath, 'models'])
await fs.syncFile(srcPath, destPath)

View File

@ -1,25 +1,23 @@
const os = require("os");
const nodeOsUtils = require("node-os-utils");
const getResourcesInfo = () =>
new Promise((resolve) => {
nodeOsUtils.mem.used()
.then(ramUsedInfo => {
const totalMemory = ramUsedInfo.totalMemMb * 1024 * 1024;
const usedMemory = ramUsedInfo.usedMemMb * 1024 * 1024;
const response = {
mem: {
totalMemory,
usedMemory,
},
};
resolve(response);
})
nodeOsUtils.mem.used().then((ramUsedInfo) => {
const totalMemory = ramUsedInfo.totalMemMb * 1024 * 1024;
const usedMemory = ramUsedInfo.usedMemMb * 1024 * 1024;
const response = {
mem: {
totalMemory,
usedMemory,
},
};
resolve(response);
});
});
const getCurrentLoad = () =>
new Promise((resolve) => {
nodeOsUtils.cpu.usage().then(cpuPercentage =>{
nodeOsUtils.cpu.usage().then((cpuPercentage) => {
const response = {
cpu: {
usage: cpuPercentage,

View File

@ -1,9 +1,12 @@
import fastify from "fastify";
import dotenv from "dotenv";
import { log, v1Router } from "@janhq/core/node";
import path from "path";
import os from "os";
import {
getServerLogPath,
v1Router,
logServer,
getJanExtensionsPath,
} from "@janhq/core/node";
import { join } from "path";
// Load environment variables
dotenv.config();
@ -11,7 +14,6 @@ dotenv.config();
// Define default settings
const JAN_API_HOST = process.env.JAN_API_HOST || "127.0.0.1";
const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || "1337");
const serverLogPath = path.join(os.homedir(), "jan", "logs", "server.log");
// Initialize server settings
let server: any | undefined = undefined;
@ -40,7 +42,7 @@ export interface ServerConfig {
/**
* Function to start the server
* @param configs - Server configurations
* @param configs - Server configurations
*/
export const startServer = async (configs?: ServerConfig) => {
// Update server settings
@ -48,12 +50,12 @@ export const startServer = async (configs?: ServerConfig) => {
hostSetting = configs?.host ?? JAN_API_HOST;
portSetting = configs?.port ?? JAN_API_PORT;
corsEnbaled = configs?.isCorsEnabled ?? true;
const serverLogPath = getServerLogPath();
// Start the server
try {
// Log server start
if (isVerbose)
log(`[API]::Debug: Starting JAN API server...`, "server.log");
if (isVerbose) logServer(`Debug: Starting JAN API server...`);
// Initialize Fastify server with logging
server = fastify({
@ -78,7 +80,7 @@ export const startServer = async (configs?: ServerConfig) => {
// Register Swagger UI
await server.register(require("@fastify/swagger-ui"), {
routePrefix: "/",
baseDir: configs?.baseDir ?? path.join(__dirname, "../..", "./docs/openapi"),
baseDir: configs?.baseDir ?? join(__dirname, "../..", "./docs/openapi"),
uiConfig: {
docExpansion: "full",
deepLinking: false,
@ -92,9 +94,7 @@ export const startServer = async (configs?: ServerConfig) => {
await server.register(
(childContext: any, _: any, done: any) => {
childContext.register(require("@fastify/static"), {
root:
process.env.EXTENSION_ROOT ||
path.join(require("os").homedir(), "jan", "extensions"),
root: getJanExtensionsPath(),
wildcard: false,
});
@ -115,13 +115,13 @@ export const startServer = async (configs?: ServerConfig) => {
.then(() => {
// Log server listening
if (isVerbose)
log(
`[API]::Debug: JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`
logServer(
`Debug: JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`
);
});
} catch (e) {
// Log any errors
if (isVerbose) log(`[API]::Error: ${e}`);
if (isVerbose) logServer(`Error: ${e}`);
}
};
@ -131,11 +131,11 @@ export const startServer = async (configs?: ServerConfig) => {
export const stopServer = async () => {
try {
// Log server stop
if (isVerbose) log(`[API]::Debug: Server stopped`, "server.log");
if (isVerbose) logServer(`Debug: Server stopped`);
// Stop the server
await server.close();
} catch (e) {
// Log any errors
if (isVerbose) log(`[API]::Error: ${e}`);
if (isVerbose) logServer(`Error: ${e}`);
}
};

View File

@ -1,7 +1,6 @@
import { useEffect, useState } from 'react'
import { ExtensionTypeEnum } from '@janhq/core'
import { MonitoringExtension } from '@janhq/core'
import { ExtensionTypeEnum, MonitoringExtension } from '@janhq/core'
import { useSetAtom } from 'jotai'

View File

@ -1,4 +1,4 @@
import { getUserSpace, openFileExplorer, joinPath } from '@janhq/core'
import { openFileExplorer, joinPath, getJanDataFolderPath } from '@janhq/core'
import { useAtomValue } from 'jotai'
import { selectedModelAtom } from '@/containers/DropdownListSidebar'
@ -18,7 +18,7 @@ export const usePath = () => {
return
}
const userSpace = await getUserSpace()
const userSpace = await getJanDataFolderPath()
let filePath = undefined
const assistantId = activeThread.assistants[0]?.assistant_id
switch (type) {
@ -51,7 +51,7 @@ export const usePath = () => {
return
}
const userSpace = await getUserSpace()
const userSpace = await getJanDataFolderPath()
let filePath = undefined
const assistantId = activeThread.assistants[0]?.assistant_id
switch (type) {

View File

@ -1,4 +1,9 @@
import { fs, joinPath, openFileExplorer, getUserSpace } from '@janhq/core'
import {
fs,
joinPath,
openFileExplorer,
getJanDataFolderPath,
} from '@janhq/core'
export const useServerLog = () => {
const getServerLog = async () => {
@ -12,8 +17,8 @@ export const useServerLog = () => {
return logs
}
const openServerLog = async () => {
const userSpace = await getUserSpace()
const fullPath = await joinPath([userSpace, 'logs', 'server.log'])
const janDataFolderPath = await getJanDataFolderPath()
const fullPath = await joinPath([janDataFolderPath, 'logs', 'server.log'])
return openFileExplorer(fullPath)
}

View File

@ -9,7 +9,7 @@ import {
ChangeEvent,
} from 'react'
import { fs } from '@janhq/core'
import { fs, AppConfiguration } from '@janhq/core'
import { Switch, Button, Input } from '@janhq/uikit'
import ShortcutModal from '@/containers/ShortcutModal'
@ -46,6 +46,17 @@ const Advanced = () => {
[setPartialProxy, setProxy]
)
// TODO: remove me later.
const [currentPath, setCurrentPath] = useState('')
useEffect(() => {
window.core?.api
?.getAppConfigurations()
?.then((appConfig: AppConfiguration) => {
setCurrentPath(appConfig.data_folder)
})
}, [])
useEffect(() => {
readSettings().then((settings) => {
setGpuEnabled(settings.run_mode === 'gpu')
@ -62,6 +73,35 @@ const Advanced = () => {
})
}
const onJanVaultDirectoryClick = async () => {
const destFolder = await window.core?.api?.selectDirectory()
if (destFolder) {
console.debug(`Destination folder selected: ${destFolder}`)
try {
const appConfiguration: AppConfiguration =
await window.core?.api?.getAppConfigurations()
const currentJanDataFolder = appConfiguration.data_folder
if (currentJanDataFolder === destFolder) {
console.debug(
`Destination folder is the same as current folder. Ignore..`
)
return
}
appConfiguration.data_folder = destFolder
await fs.syncFile(currentJanDataFolder, destFolder)
await window.core?.api?.updateAppConfiguration(appConfiguration)
console.debug(
`File sync finished from ${currentJanDataFolder} to ${destFolder}`
)
await window.core?.api?.relaunch()
} catch (e) {
console.error(`Error: ${e}`)
}
}
}
return (
<div className="block w-full">
{/* CPU / GPU switching */}
@ -192,6 +232,31 @@ const Advanced = () => {
Clear
</Button>
</div>
{experimentalFeature && (
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
<div className="w-4/5 flex-shrink-0 space-y-1.5">
<div className="flex gap-x-2">
<h6 className="text-sm font-semibold capitalize">
Jan Data Folder
</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
Where messages, model configurations, and other user data is
placed.
</p>
<p className="whitespace-pre-wrap leading-relaxed text-gray-500">
{`${currentPath}`}
</p>
</div>
<Button
size="sm"
themes="secondary"
onClick={onJanVaultDirectoryClick}
>
Select
</Button>
</div>
)}
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
<div className="w-4/5 flex-shrink-0 space-y-1.5">
<div className="flex gap-x-2">