diff --git a/core/rollup.config.ts b/core/rollup.config.ts index d78130a4de..98cb1f2a44 100644 --- a/core/rollup.config.ts +++ b/core/rollup.config.ts @@ -54,7 +54,8 @@ export default [ 'url', 'http', 'os', - 'util' + 'util', + 'child_process' ], watch: { include: 'src/node/**', diff --git a/core/src/api/index.ts b/core/src/api/index.ts index f4ec3cd7e1..0a160df393 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -1,15 +1,22 @@ /** - * App Route APIs + * Native Route APIs * @description Enum of all the routes exposed by the app */ -export enum AppRoute { +export enum NativeRoute { openExternalUrl = 'openExternalUrl', openAppDirectory = 'openAppDirectory', openFileExplore = 'openFileExplorer', selectDirectory = 'selectDirectory', + relaunch = 'relaunch', +} + +/** + * App Route APIs + * @description Enum of all the routes exposed by the app + */ +export enum AppRoute { getAppConfigurations = 'getAppConfigurations', updateAppConfiguration = 'updateAppConfiguration', - relaunch = 'relaunch', joinPath = 'joinPath', isSubdirectory = 'isSubdirectory', baseName = 'baseName', @@ -69,6 +76,10 @@ export enum FileManagerRoute { export type ApiFunction = (...args: any[]) => any +export type NativeRouteFunctions = { + [K in NativeRoute]: ApiFunction +} + export type AppRouteFunctions = { [K in AppRoute]: ApiFunction } @@ -97,7 +108,8 @@ export type FileManagerRouteFunctions = { [K in FileManagerRoute]: ApiFunction } -export type APIFunctions = AppRouteFunctions & +export type APIFunctions = NativeRouteFunctions & + AppRouteFunctions & AppEventFunctions & DownloadRouteFunctions & DownloadEventFunctions & @@ -105,11 +117,16 @@ export type APIFunctions = AppRouteFunctions & FileSystemRouteFunctions & FileManagerRoute -export const APIRoutes = [ +export const CoreRoutes = [ ...Object.values(AppRoute), ...Object.values(DownloadRoute), ...Object.values(ExtensionRoute), ...Object.values(FileSystemRoute), ...Object.values(FileManagerRoute), ] + +export const APIRoutes = [ + ...CoreRoutes, + ...Object.values(NativeRoute), +] export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)] diff --git a/core/src/index.ts b/core/src/index.ts index a56b6f0e13..3505797b19 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -38,3 +38,10 @@ export * from './extension' * @module */ export * from './extensions/index' + +/** + * Declare global object + */ +declare global { + var core: any | undefined +} diff --git a/core/src/node/api/common/adapter.ts b/core/src/node/api/common/adapter.ts new file mode 100644 index 0000000000..b1013f6d2b --- /dev/null +++ b/core/src/node/api/common/adapter.ts @@ -0,0 +1,37 @@ +import { AppRoute, DownloadRoute, ExtensionRoute, FileManagerRoute, FileSystemRoute } from '../../../api' +import { Downloader } from '../processors/download' +import { FileSystem } from '../processors/fs' +import { Extension } from '../processors/extension' +import { FSExt } from '../processors/fsExt' +import { App } from '../processors/app' + +export class RequestAdapter { + downloader: Downloader + fileSystem: FileSystem + extension: Extension + fsExt: FSExt + app: App + + constructor(observer?: Function) { + this.downloader = new Downloader(observer) + this.fileSystem = new FileSystem() + this.extension = new Extension() + this.fsExt = new FSExt() + this.app = new App() + } + + // TODO: Clearer Factory pattern here + process(route: string, ...args: any) { + if (route in DownloadRoute) { + return this.downloader.process(route, ...args) + } else if (route in FileSystemRoute) { + return this.fileSystem.process(route, ...args) + } else if (route in ExtensionRoute) { + return this.extension.process(route, ...args) + } else if (route in FileManagerRoute) { + return this.fsExt.process(route, ...args) + } else if (route in AppRoute) { + return this.app.process(route, ...args) + } + } +} diff --git a/core/src/node/api/common/handler.ts b/core/src/node/api/common/handler.ts new file mode 100644 index 0000000000..4a39ae52a6 --- /dev/null +++ b/core/src/node/api/common/handler.ts @@ -0,0 +1,23 @@ +import { CoreRoutes } from '../../../api' +import { RequestAdapter } from './adapter' + +export type Handler = (route: string, args: any) => any + +export class RequestHandler { + handler: Handler + adataper: RequestAdapter + + constructor(handler: Handler, observer?: Function) { + this.handler = handler + this.adataper = new RequestAdapter(observer) + } + + handle() { + CoreRoutes.map((route) => { + this.handler(route, async (...args: any[]) => { + const values = await this.adataper.process(route, ...args) + return values + }) + }) + } +} diff --git a/core/src/node/api/index.ts b/core/src/node/api/index.ts index 4c3041ba3f..ab0c516569 100644 --- a/core/src/node/api/index.ts +++ b/core/src/node/api/index.ts @@ -1,2 +1,3 @@ export * from './HttpServer' -export * from './routes' +export * from './restful/v1' +export * from './common/handler' diff --git a/core/src/node/api/processors/Processor.ts b/core/src/node/api/processors/Processor.ts new file mode 100644 index 0000000000..8ef0c6e191 --- /dev/null +++ b/core/src/node/api/processors/Processor.ts @@ -0,0 +1,3 @@ +export abstract class Processor { + abstract process(key: string, ...args: any[]): any +} diff --git a/core/src/node/api/processors/app.ts b/core/src/node/api/processors/app.ts new file mode 100644 index 0000000000..a4b1a5a066 --- /dev/null +++ b/core/src/node/api/processors/app.ts @@ -0,0 +1,97 @@ +import { basename, isAbsolute, join, relative } from 'path' + +import { AppRoute } from '../../../api' +import { Processor } from './Processor' +import { getAppConfigurations as appConfiguration, updateAppConfiguration } from '../../helper' +import { log as writeLog, logServer as writeServerLog } from '../../helper/log' +import { appResourcePath } from '../../helper/path' + +export class App implements Processor { + observer?: Function + + constructor(observer?: Function) { + this.observer = observer + } + + process(key: string, ...args: any[]): any { + const instance = this as any + const func = instance[key] + return func(...args) + } + + /** + * Joins multiple paths together, respect to the current OS. + */ + joinPath(args: any[]) { + return join(...args) + } + + /** + * Checks if the given path is a subdirectory of the given directory. + * + * @param _event - The IPC event object. + * @param from - The path to check. + * @param to - The directory to check against. + * + * @returns {Promise} - A promise that resolves with the result. + */ + isSubdirectory(from: any, to: any) { + const rel = relative(from, to) + const isSubdir = rel && !rel.startsWith('..') && !isAbsolute(rel) + + if (isSubdir === '') return false + else return isSubdir + } + + /** + * Retrieve basename from given path, respect to the current OS. + */ + baseName(args: any) { + return basename(args) + } + + /** + * Log message to log file. + */ + log(args: any) { + writeLog(args) + } + + /** + * Log message to log file. + */ + logServer(args: any) { + writeServerLog(args) + } + + getAppConfigurations() { + return appConfiguration() + } + + async updateAppConfiguration(args: any) { + await updateAppConfiguration(args) + } + + /** + * Start Jan API Server. + */ + async startServer(args?: any) { + const { startServer } = require('@janhq/server') + return startServer({ + host: args?.host, + port: args?.port, + isCorsEnabled: args?.isCorsEnabled, + isVerboseEnabled: args?.isVerboseEnabled, + schemaPath: join(await appResourcePath(), 'docs', 'openapi', 'jan.yaml'), + baseDir: join(await appResourcePath(), 'docs', 'openapi'), + }) + } + + /** + * Stop Jan API Server. + */ + stopServer() { + const { stopServer } = require('@janhq/server') + return stopServer() + } +} diff --git a/core/src/node/api/processors/download.ts b/core/src/node/api/processors/download.ts new file mode 100644 index 0000000000..8d58ac852c --- /dev/null +++ b/core/src/node/api/processors/download.ts @@ -0,0 +1,105 @@ +import { resolve, sep } from 'path' +import { DownloadEvent } from '../../../api' +import { normalizeFilePath } from '../../helper/path' +import { getJanDataFolderPath } from '../../helper' +import { DownloadManager } from '../../helper/download' +import { createWriteStream, renameSync } from 'fs' +import { Processor } from './Processor' +import { DownloadState } from '../../../types' + +export class Downloader implements Processor { + observer?: Function + + constructor(observer?: Function) { + this.observer = observer + } + + process(key: string, ...args: any[]): any { + const instance = this as any + const func = instance[key] + return func(this.observer, ...args) + } + + downloadFile(observer: any, url: string, localPath: string, network: any) { + const request = require('request') + const progress = require('request-progress') + + const strictSSL = !network?.ignoreSSL + const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined + if (typeof localPath === 'string') { + localPath = normalizeFilePath(localPath) + } + const array = localPath.split(sep) + const fileName = array.pop() ?? '' + const modelId = array.pop() ?? '' + + const destination = resolve(getJanDataFolderPath(), localPath) + const rq = request({ url, strictSSL, proxy }) + + // Put request to download manager instance + DownloadManager.instance.setRequest(localPath, rq) + + // Downloading file to a temp file first + const downloadingTempFile = `${destination}.download` + + progress(rq, {}) + .on('progress', (state: any) => { + const downloadState: DownloadState = { + ...state, + modelId, + fileName, + downloadState: 'downloading', + } + console.log('progress: ', downloadState) + observer?.(DownloadEvent.onFileDownloadUpdate, downloadState) + DownloadManager.instance.downloadProgressMap[modelId] = downloadState + }) + .on('error', (error: Error) => { + const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId] + const downloadState: DownloadState = { + ...currentDownloadState, + downloadState: 'error', + } + if (currentDownloadState) { + DownloadManager.instance.downloadProgressMap[modelId] = downloadState + } + + observer?.(DownloadEvent.onFileDownloadError, downloadState) + }) + .on('end', () => { + const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId] + if (currentDownloadState && DownloadManager.instance.networkRequests[localPath]) { + // Finished downloading, rename temp file to actual file + renameSync(downloadingTempFile, destination) + const downloadState: DownloadState = { + ...currentDownloadState, + downloadState: 'end', + } + observer?.(DownloadEvent.onFileDownloadSuccess, downloadState) + DownloadManager.instance.downloadProgressMap[modelId] = downloadState + } + }) + .pipe(createWriteStream(downloadingTempFile)) + } + + abortDownload(observer: any, fileName: string) { + const rq = DownloadManager.instance.networkRequests[fileName] + if (rq) { + DownloadManager.instance.networkRequests[fileName] = undefined + rq?.abort() + } else { + observer?.(DownloadEvent.onFileDownloadError, { + fileName, + error: 'aborted', + }) + } + } + + resumeDownload(observer: any, fileName: any) { + DownloadManager.instance.networkRequests[fileName]?.resume() + } + + pauseDownload(observer: any, fileName: any) { + DownloadManager.instance.networkRequests[fileName]?.pause() + } +} diff --git a/core/src/node/api/processors/extension.ts b/core/src/node/api/processors/extension.ts new file mode 100644 index 0000000000..95dcd19d03 --- /dev/null +++ b/core/src/node/api/processors/extension.ts @@ -0,0 +1,89 @@ +import { readdirSync } from 'fs' +import { join, extname } from 'path' + +import { ExtensionRoute } from '../../../api' +import { Processor } from './Processor' +import { ModuleManager } from '../../helper/module' +import { getJanExtensionsPath as getPath } from '../../helper' +import { + getActiveExtensions as getExtensions, + getExtension, + removeExtension, + installExtensions, +} from '../../extension/store' +import { appResourcePath } from '../../helper/path' + +export class Extension implements Processor { + observer?: Function + + constructor(observer?: Function) { + this.observer = observer + } + + process(key: string, ...args: any[]): any { + const instance = this as any + const func = instance[key] + return func(...args) + } + + invokeExtensionFunc(modulePath: string, method: string, ...params: any[]) { + const module = require(join(getPath(), modulePath)) + ModuleManager.instance.setModule(modulePath, module) + + if (typeof module[method] === 'function') { + return module[method](...params) + } else { + console.debug(module[method]) + console.error(`Function "${method}" does not exist in the module.`) + } + } + + /** + * Returns the paths of the base extensions. + * @returns An array of paths to the base extensions. + */ + async baseExtensions() { + const baseExtensionPath = join(await appResourcePath(), 'pre-install') + return readdirSync(baseExtensionPath) + .filter((file) => extname(file) === '.tgz') + .map((file) => join(baseExtensionPath, file)) + } + + /**MARK: Extension Manager handlers */ + async installExtension(extensions: any) { + // Install and activate all provided extensions + const installed = await installExtensions(extensions) + return JSON.parse(JSON.stringify(installed)) + } + + // Register IPC route to uninstall a extension + async uninstallExtension(extensions: any) { + // Uninstall all provided extensions + for (const ext of extensions) { + const extension = getExtension(ext) + await extension.uninstall() + if (extension.name) removeExtension(extension.name) + } + + // Reload all renderer pages if needed + return true + } + + // Register IPC route to update a extension + async updateExtension(extensions: any) { + // Update all provided extensions + const updated: any[] = [] + for (const ext of extensions) { + const extension = getExtension(ext) + const res = await extension.update() + if (res) updated.push(extension) + } + + // Reload all renderer pages if needed + return JSON.parse(JSON.stringify(updated)) + } + + getActiveExtensions() { + return JSON.parse(JSON.stringify(getExtensions())) + } +} diff --git a/core/src/node/api/processors/fs.ts b/core/src/node/api/processors/fs.ts new file mode 100644 index 0000000000..93a5f19057 --- /dev/null +++ b/core/src/node/api/processors/fs.ts @@ -0,0 +1,25 @@ +import { join } from 'path' +import { normalizeFilePath } from '../../helper/path' +import { getJanDataFolderPath } from '../../helper' +import { Processor } from './Processor' + +export class FileSystem implements Processor { + observer?: Function + private static moduleName = 'fs' + + constructor(observer?: Function) { + this.observer = observer + } + + process(route: string, ...args: any[]): any { + return import(FileSystem.moduleName).then((mdl) => + mdl[route]( + ...args.map((arg: any) => + typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) + ? join(getJanDataFolderPath(), normalizeFilePath(arg)) + : arg + ) + ) + ) + } +} diff --git a/core/src/node/api/processors/fsExt.ts b/core/src/node/api/processors/fsExt.ts new file mode 100644 index 0000000000..71e07ae57b --- /dev/null +++ b/core/src/node/api/processors/fsExt.ts @@ -0,0 +1,78 @@ +import { join } from 'path' +import fs from 'fs' +import { FileManagerRoute } from '../../../api' +import { appResourcePath, normalizeFilePath } from '../../helper/path' +import { getJanDataFolderPath, getJanDataFolderPath as getPath } from '../../helper' +import { Processor } from './Processor' +import { FileStat } from '../../../types' + +export class FSExt implements Processor { + observer?: Function + + constructor(observer?: Function) { + this.observer = observer + } + + process(key: string, ...args: any): any { + const instance = this as any + const func = instance[key] + return func(...args) + } + + // Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path. + syncFile(src: string, dest: string) { + const reflect = require('@alumna/reflect') + return reflect({ + src, + dest, + recursive: true, + delete: false, + overwrite: true, + errorOnExist: false, + }) + } + + // Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path. + getJanDataFolderPath() { + return Promise.resolve(getPath()) + } + + // Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path. + getResourcePath() { + return appResourcePath() + } + + // Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path. + getUserHomePath() { + return process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME'] + } + + // handle fs is directory here + fileStat(path: string) { + const normalizedPath = normalizeFilePath(path) + + const fullPath = join(getJanDataFolderPath(), normalizedPath) + const isExist = fs.existsSync(fullPath) + if (!isExist) return undefined + + const isDirectory = fs.lstatSync(fullPath).isDirectory() + const size = fs.statSync(fullPath).size + + const fileStat: FileStat = { + isDirectory, + size, + } + + return fileStat + } + + writeBlob(path: string, data: any) { + try { + const normalizedPath = normalizeFilePath(path) + const dataBuffer = Buffer.from(data, 'base64') + fs.writeFileSync(join(getJanDataFolderPath(), normalizedPath), dataBuffer) + } catch (err) { + console.error(`writeFile ${path} result: ${err}`) + } + } +} diff --git a/core/src/node/api/restful/app/download.ts b/core/src/node/api/restful/app/download.ts new file mode 100644 index 0000000000..b5919659b1 --- /dev/null +++ b/core/src/node/api/restful/app/download.ts @@ -0,0 +1,23 @@ +import { DownloadRoute } from '../../../../api' +import { DownloadManager } from '../../../helper/download' +import { HttpServer } from '../../HttpServer' + +export const downloadRouter = async (app: HttpServer) => { + app.get(`/download/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => { + const modelId = req.params.modelId + + console.debug(`Getting download progress for model ${modelId}`) + console.debug( + `All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}` + ) + + // check if null DownloadManager.instance.downloadProgressMap + if (!DownloadManager.instance.downloadProgressMap[modelId]) { + return res.status(404).send({ + message: 'Download progress not found', + }) + } else { + return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId]) + } + }) +} diff --git a/core/src/node/api/restful/app/handlers.ts b/core/src/node/api/restful/app/handlers.ts new file mode 100644 index 0000000000..43c3f7add9 --- /dev/null +++ b/core/src/node/api/restful/app/handlers.ts @@ -0,0 +1,13 @@ +import { HttpServer } from '../../HttpServer' +import { Handler, RequestHandler } from '../../common/handler' + +export function handleRequests(app: HttpServer) { + const restWrapper: Handler = (route: string, listener: (...args: any[]) => any) => { + app.post(`/app/${route}`, async (request: any, reply: any) => { + const args = JSON.parse(request.body) as any[] + reply.send(JSON.stringify(await listener(...args))) + }) + } + const handler = new RequestHandler(restWrapper) + handler.handle() +} diff --git a/core/src/node/api/routes/common.ts b/core/src/node/api/restful/common.ts similarity index 58% rename from core/src/node/api/routes/common.ts rename to core/src/node/api/restful/common.ts index 8887755fe1..b87bc946de 100644 --- a/core/src/node/api/routes/common.ts +++ b/core/src/node/api/restful/common.ts @@ -1,22 +1,24 @@ -import { AppRoute } from '../../../api' import { HttpServer } from '../HttpServer' -import { basename, join } from 'path' import { chatCompletions, deleteBuilder, downloadModel, getBuilder, retrieveBuilder, -} from '../common/builder' + createMessage, + createThread, + getMessages, + retrieveMesasge, + updateThread, +} from './helper/builder' -import { JanApiRouteConfiguration } from '../common/configuration' -import { startModel, stopModel } from '../common/startStopModel' +import { JanApiRouteConfiguration } from './helper/configuration' +import { startModel, stopModel } from './helper/startStopModel' import { ModelSettingParams } from '../../../types' -import { getJanDataFolderPath } from '../../utils' -import { normalizeFilePath } from '../../path' export const commonRouter = async (app: HttpServer) => { // Common Routes + // Read & Delete :: Threads | Models | Assistants Object.keys(JanApiRouteConfiguration).forEach((key) => { app.get(`/${key}`, async (_request) => getBuilder(JanApiRouteConfiguration[key])) @@ -29,7 +31,24 @@ export const commonRouter = async (app: HttpServer) => { ) }) - // Download Model Routes + // Threads + app.post(`/threads/`, async (req, res) => createThread(req.body)) + + app.get(`/threads/:threadId/messages`, async (req, res) => getMessages(req.params.threadId)) + + app.get(`/threads/:threadId/messages/:messageId`, async (req, res) => + retrieveMesasge(req.params.threadId, req.params.messageId) + ) + + app.post(`/threads/:threadId/messages`, async (req, res) => + createMessage(req.params.threadId as any, req.body as any) + ) + + app.patch(`/threads/:threadId`, async (request: any) => + updateThread(request.params.threadId, request.body) + ) + + // Models app.get(`/models/download/:modelId`, async (request: any) => downloadModel(request.params.modelId, { ignoreSSL: request.query.ignoreSSL === 'true', @@ -48,24 +67,6 @@ export const commonRouter = async (app: HttpServer) => { app.put(`/models/:modelId/stop`, async (request: any) => stopModel(request.params.modelId)) - // Chat Completion Routes + // Chat Completion app.post(`/chat/completions`, async (request: any, reply: any) => chatCompletions(request, reply)) - - // App Routes - app.post(`/app/${AppRoute.joinPath}`, async (request: any, reply: any) => { - const args = JSON.parse(request.body) as any[] - - const paths = args[0].map((arg: string) => - typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) - ? join(getJanDataFolderPath(), normalizeFilePath(arg)) - : arg - ) - - reply.send(JSON.stringify(join(...paths))) - }) - - app.post(`/app/${AppRoute.baseName}`, async (request: any, reply: any) => { - const args = JSON.parse(request.body) as any[] - reply.send(JSON.stringify(basename(args[0]))) - }) } diff --git a/core/src/node/api/common/builder.ts b/core/src/node/api/restful/helper/builder.ts similarity index 99% rename from core/src/node/api/common/builder.ts rename to core/src/node/api/restful/helper/builder.ts index 5c99cf4d8e..a8124a74ad 100644 --- a/core/src/node/api/common/builder.ts +++ b/core/src/node/api/restful/helper/builder.ts @@ -1,10 +1,11 @@ import fs from 'fs' import { JanApiRouteConfiguration, RouteConfiguration } from './configuration' import { join } from 'path' -import { ContentType, MessageStatus, Model, ThreadMessage } from './../../../index' -import { getEngineConfiguration, getJanDataFolderPath } from '../../utils' +import { ContentType, MessageStatus, Model, ThreadMessage } from '../../../../index' +import { getEngineConfiguration, getJanDataFolderPath } from '../../../helper' import { DEFAULT_CHAT_COMPLETION_URL } from './consts' +// TODO: Refactor these export const getBuilder = async (configuration: RouteConfiguration) => { const directoryPath = join(getJanDataFolderPath(), configuration.dirName) try { diff --git a/core/src/node/api/common/configuration.ts b/core/src/node/api/restful/helper/configuration.ts similarity index 100% rename from core/src/node/api/common/configuration.ts rename to core/src/node/api/restful/helper/configuration.ts diff --git a/core/src/node/api/common/consts.ts b/core/src/node/api/restful/helper/consts.ts similarity index 100% rename from core/src/node/api/common/consts.ts rename to core/src/node/api/restful/helper/consts.ts diff --git a/core/src/node/api/common/startStopModel.ts b/core/src/node/api/restful/helper/startStopModel.ts similarity index 99% rename from core/src/node/api/common/startStopModel.ts rename to core/src/node/api/restful/helper/startStopModel.ts index 0d4934e1c0..0e6972b0bf 100644 --- a/core/src/node/api/common/startStopModel.ts +++ b/core/src/node/api/restful/helper/startStopModel.ts @@ -1,9 +1,9 @@ import fs from 'fs' import { join } from 'path' -import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../utils' -import { logServer } from '../../log' +import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../../helper' +import { logServer } from '../../../helper/log' import { ChildProcessWithoutNullStreams, spawn } from 'child_process' -import { Model, ModelSettingParams, PromptTemplate } from '../../../types' +import { Model, ModelSettingParams, PromptTemplate } from '../../../../types' import { LOCAL_HOST, NITRO_DEFAULT_PORT, diff --git a/core/src/node/api/restful/v1.ts b/core/src/node/api/restful/v1.ts new file mode 100644 index 0000000000..474b1f3550 --- /dev/null +++ b/core/src/node/api/restful/v1.ts @@ -0,0 +1,16 @@ +import { HttpServer } from '../HttpServer' +import { commonRouter } from './common' +import { downloadRouter } from './app/download' +import { handleRequests } from './app/handlers' + +export const v1Router = async (app: HttpServer) => { + // MARK: Public API Routes + app.register(commonRouter) + + // MARK: Internal Application Routes + handleRequests(app) + + // Expanded route for tracking download progress + // TODO: Replace by Observer Wrapper (ZeroMQ / Vanilla Websocket) + app.register(downloadRouter) +} diff --git a/core/src/node/api/routes/download.ts b/core/src/node/api/routes/download.ts deleted file mode 100644 index cc95fe1d4f..0000000000 --- a/core/src/node/api/routes/download.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { DownloadRoute } from '../../../api' -import { join, sep } from 'path' -import { DownloadManager } from '../../download' -import { HttpServer } from '../HttpServer' -import { createWriteStream } from 'fs' -import { getJanDataFolderPath } from '../../utils' -import { normalizeFilePath } from '../../path' -import { DownloadState } from '../../../types' - -export const downloadRouter = async (app: HttpServer) => { - app.get(`/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => { - const modelId = req.params.modelId - - console.debug(`Getting download progress for model ${modelId}`) - console.debug( - `All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}` - ) - - // check if null DownloadManager.instance.downloadProgressMap - if (!DownloadManager.instance.downloadProgressMap[modelId]) { - return res.status(404).send({ - message: 'Download progress not found', - }) - } else { - return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId]) - } - }) - - app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => { - const strictSSL = !(req.query.ignoreSSL === 'true') - const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined - const body = JSON.parse(req.body as any) - const normalizedArgs = body.map((arg: any) => { - if (typeof arg === 'string' && arg.startsWith('file:')) { - return join(getJanDataFolderPath(), normalizeFilePath(arg)) - } - return arg - }) - - const localPath = normalizedArgs[1] - const array = localPath.split(sep) - const fileName = array.pop() ?? '' - const modelId = array.pop() ?? '' - console.debug('downloadFile', normalizedArgs, fileName, modelId) - - const request = require('request') - const progress = require('request-progress') - - const rq = request({ url: normalizedArgs[0], strictSSL, proxy }) - progress(rq, {}) - .on('progress', function (state: any) { - const downloadProps: DownloadState = { - ...state, - modelId, - fileName, - downloadState: 'downloading', - } - console.debug(`Download ${modelId} onProgress`, downloadProps) - DownloadManager.instance.downloadProgressMap[modelId] = downloadProps - }) - .on('error', function (err: Error) { - console.debug(`Download ${modelId} onError`, err.message) - - const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId] - if (currentDownloadState) { - DownloadManager.instance.downloadProgressMap[modelId] = { - ...currentDownloadState, - downloadState: 'error', - } - } - }) - .on('end', function () { - console.debug(`Download ${modelId} onEnd`) - - const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId] - if (currentDownloadState) { - if (currentDownloadState.downloadState === 'downloading') { - // if the previous state is downloading, then set the state to end (success) - DownloadManager.instance.downloadProgressMap[modelId] = { - ...currentDownloadState, - downloadState: 'end', - } - } - } - }) - .pipe(createWriteStream(normalizedArgs[1])) - - DownloadManager.instance.setRequest(localPath, rq) - res.status(200).send({ message: 'Download started' }) - }) - - app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => { - const body = JSON.parse(req.body as any) - const normalizedArgs = body.map((arg: any) => { - if (typeof arg === 'string' && arg.startsWith('file:')) { - return join(getJanDataFolderPath(), normalizeFilePath(arg)) - } - return arg - }) - - const localPath = normalizedArgs[0] - const fileName = localPath.split(sep).pop() ?? '' - const rq = DownloadManager.instance.networkRequests[fileName] - DownloadManager.instance.networkRequests[fileName] = undefined - rq?.abort() - if (rq) { - res.status(200).send({ message: 'Download aborted' }) - } else { - res.status(404).send({ message: 'Download not found' }) - } - }) -} diff --git a/core/src/node/api/routes/extension.ts b/core/src/node/api/routes/extension.ts deleted file mode 100644 index 02bc54eb37..0000000000 --- a/core/src/node/api/routes/extension.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { join, extname } from 'path' -import { ExtensionRoute } from '../../../api/index' -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) => { - const activeExtensions = await getActiveExtensions() - res.status(200).send(activeExtensions) - }) - - app.post(`/${ExtensionRoute.baseExtensions}`, async (_req, res) => { - const baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install') - const extensions = readdirSync(baseExtensionPath) - .filter((file) => extname(file) === '.tgz') - .map((file) => join(baseExtensionPath, file)) - - res.status(200).send(extensions) - }) - - 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)) - }) - - app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => { - const args = JSON.parse(req.body as any) - console.debug(args) - const module = await import(join(getJanExtensionsPath(), args[0])) - - ModuleManager.instance.setModule(args[0], module) - const method = args[1] - if (typeof module[method] === 'function') { - // remove first item from args - const newArgs = args.slice(2) - console.log(newArgs) - return module[method](...args.slice(2)) - } else { - console.debug(module[method]) - console.error(`Function "${method}" does not exist in the module.`) - } - }) -} diff --git a/core/src/node/api/routes/fileManager.ts b/core/src/node/api/routes/fileManager.ts deleted file mode 100644 index b4c73dda14..0000000000 --- a/core/src/node/api/routes/fileManager.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { FileManagerRoute } from '../../../api' -import { HttpServer } from '../../index' -import { join } from 'path' - -export const fileManagerRouter = async (app: HttpServer) => { - app.post(`/fs/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => { - const reflect = require('@alumna/reflect') - const args = JSON.parse(request.body) - return reflect({ - src: args[0], - dest: args[1], - recursive: true, - delete: false, - overwrite: true, - errorOnExist: false, - }) - }) - - app.post(`/fs/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) => - global.core.appPath() - ) - - app.post(`/fs/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => - join(global.core.appPath(), '../../..') - ) - - app.post(`/app/${FileManagerRoute.getUserHomePath}`, async (request: any, reply: any) => {}) - app.post(`/fs/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {}) -} diff --git a/core/src/node/api/routes/fs.ts b/core/src/node/api/routes/fs.ts deleted file mode 100644 index 9535418a02..0000000000 --- a/core/src/node/api/routes/fs.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { FileManagerRoute, FileSystemRoute } from '../../../api' -import { join } from 'path' -import { HttpServer } from '../HttpServer' -import { getJanDataFolderPath } from '../../utils' -import { normalizeFilePath } from '../../path' -import { writeFileSync } from 'fs' - -export const fsRouter = async (app: HttpServer) => { - const moduleName = 'fs' - // Generate handlers for each fs route - Object.values(FileSystemRoute).forEach((route) => { - app.post(`/${route}`, async (req, res) => { - const body = JSON.parse(req.body as any) - try { - const result = await import(moduleName).then((mdl) => { - return mdl[route]( - ...body.map((arg: any) => - typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) - ? join(getJanDataFolderPath(), normalizeFilePath(arg)) - : arg - ) - ) - }) - res.status(200).send(result) - } catch (ex) { - console.log(ex) - } - }) - }) - app.post(`/${FileManagerRoute.writeBlob}`, async (request: any, reply: any) => { - try { - const args = JSON.parse(request.body) as any[] - console.log('writeBlob:', args[0]) - const dataBuffer = Buffer.from(args[1], 'base64') - writeFileSync(args[0], dataBuffer) - } catch (err) { - console.error(`writeFile ${request.body} result: ${err}`) - } - }) -} diff --git a/core/src/node/api/routes/index.ts b/core/src/node/api/routes/index.ts deleted file mode 100644 index e6edc62f7c..0000000000 --- a/core/src/node/api/routes/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './download' -export * from './extension' -export * from './fs' -export * from './thread' -export * from './common' -export * from './v1' diff --git a/core/src/node/api/routes/thread.ts b/core/src/node/api/routes/thread.ts deleted file mode 100644 index 4066d27165..0000000000 --- a/core/src/node/api/routes/thread.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { HttpServer } from '../HttpServer' -import { - createMessage, - createThread, - getMessages, - retrieveMesasge, - updateThread, -} from '../common/builder' - -export const threadRouter = async (app: HttpServer) => { - // create thread - app.post(`/`, async (req, res) => createThread(req.body)) - - app.get(`/:threadId/messages`, async (req, res) => getMessages(req.params.threadId)) - - // retrieve message - app.get(`/:threadId/messages/:messageId`, async (req, res) => - retrieveMesasge(req.params.threadId, req.params.messageId), - ) - - // create message - app.post(`/:threadId/messages`, async (req, res) => - createMessage(req.params.threadId as any, req.body as any), - ) - - // modify thread - app.patch(`/:threadId`, async (request: any) => - updateThread(request.params.threadId, request.body), - ) -} diff --git a/core/src/node/api/routes/v1.ts b/core/src/node/api/routes/v1.ts deleted file mode 100644 index 301c41ac04..0000000000 --- a/core/src/node/api/routes/v1.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { HttpServer } from '../HttpServer' -import { commonRouter } from './common' -import { threadRouter } from './thread' -import { fsRouter } from './fs' -import { extensionRouter } from './extension' -import { downloadRouter } from './download' -import { fileManagerRouter } from './fileManager' - -export const v1Router = async (app: HttpServer) => { - // MARK: External Routes - app.register(commonRouter) - app.register(threadRouter, { - prefix: '/threads', - }) - - // MARK: Internal Application Routes - app.register(fsRouter, { - prefix: '/fs', - }) - app.register(fileManagerRouter) - - app.register(extensionRouter, { - prefix: '/extension', - }) - app.register(downloadRouter, { - prefix: '/download', - }) -} diff --git a/core/src/node/extension/store.ts b/core/src/node/extension/store.ts index 84b1f9caf3..b92b3fd62f 100644 --- a/core/src/node/extension/store.ts +++ b/core/src/node/extension/store.ts @@ -94,7 +94,7 @@ export function persistExtensions() { * @returns {Promise.>} New extension * @alias extensionManager.installExtensions */ -export async function installExtensions(extensions: any, store = true) { +export async function installExtensions(extensions: any) { const installed: Extension[] = []; for (const ext of extensions) { // Set install options and activation based on input type @@ -104,11 +104,12 @@ export async function installExtensions(extensions: any, store = true) { // Install and possibly activate extension const extension = new Extension(...spec); + if(!extension.origin) { continue } await extension._install(); if (activate) extension.setActive(true); // Add extension to store if needed - if (store) addExtension(extension); + addExtension(extension); installed.push(extension); } diff --git a/core/src/node/utils/index.ts b/core/src/node/helper/config.ts similarity index 93% rename from core/src/node/utils/index.ts rename to core/src/node/helper/config.ts index 4bcbf13b17..a47875e68a 100644 --- a/core/src/node/utils/index.ts +++ b/core/src/node/helper/config.ts @@ -2,7 +2,7 @@ import { AppConfiguration, SystemResourceInfo } from '../../types' import { join } from 'path' import fs from 'fs' import os from 'os' -import { log, logServer } from '../log' +import { log, logServer } from './log' import childProcess from 'child_process' // TODO: move this to core @@ -56,34 +56,6 @@ export const updateAppConfiguration = (configuration: AppConfiguration): Promise 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 * @@ -146,18 +118,6 @@ const exec = async (command: string): Promise => { }) } -export const getSystemResourceInfo = async (): Promise => { - const cpu = await physicalCpuCount() - const message = `[NITRO]::CPU informations - ${cpu}` - log(message) - logServer(message) - - return { - numCpuPhysicalCore: cpu, - memAvailable: 0, // TODO: this should not be 0 - } -} - export const getEngineConfiguration = async (engineId: string) => { if (engineId !== 'openai') { return undefined @@ -167,3 +127,31 @@ export const getEngineConfiguration = async (engineId: string) => { const data = fs.readFileSync(filePath, 'utf-8') return JSON.parse(data) } + +/** + * 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') +} diff --git a/core/src/node/download.ts b/core/src/node/helper/download.ts similarity index 94% rename from core/src/node/download.ts rename to core/src/node/helper/download.ts index b3f2844409..b9fb88bb5c 100644 --- a/core/src/node/download.ts +++ b/core/src/node/helper/download.ts @@ -1,4 +1,4 @@ -import { DownloadState } from '../types' +import { DownloadState } from '../../types' /** * Manages file downloads and network requests. diff --git a/core/src/node/helper/index.ts b/core/src/node/helper/index.ts new file mode 100644 index 0000000000..6fc54fc6b1 --- /dev/null +++ b/core/src/node/helper/index.ts @@ -0,0 +1,6 @@ +export * from './config' +export * from './download' +export * from './log' +export * from './module' +export * from './path' +export * from './resource' diff --git a/core/src/node/log.ts b/core/src/node/helper/log.ts similarity index 93% rename from core/src/node/log.ts rename to core/src/node/helper/log.ts index 6f2c2f80f3..8ff1969434 100644 --- a/core/src/node/log.ts +++ b/core/src/node/helper/log.ts @@ -1,6 +1,6 @@ import fs from 'fs' import util from 'util' -import { getAppLogPath, getServerLogPath } from './utils' +import { getAppLogPath, getServerLogPath } from './config' export const log = (message: string) => { const path = getAppLogPath() diff --git a/core/src/node/module.ts b/core/src/node/helper/module.ts similarity index 100% rename from core/src/node/module.ts rename to core/src/node/helper/module.ts diff --git a/core/src/node/helper/path.ts b/core/src/node/helper/path.ts new file mode 100644 index 0000000000..b07f297f4b --- /dev/null +++ b/core/src/node/helper/path.ts @@ -0,0 +1,35 @@ +import { join } from "path" + +/** + * Normalize file path + * Remove all file protocol prefix + * @param path + * @returns + */ +export function normalizeFilePath(path: string): string { + return path.replace(/^(file:[\\/]+)([^:\s]+)$/, '$2') +} + +export async function appResourcePath(): Promise { + let electron: any = undefined + + try { + const moduleName = 'electron' + electron = await import(moduleName) + } catch (err) { + console.error('Electron is not available') + } + + // electron + if (electron && electron.protocol) { + let appPath = join(electron.app.getAppPath(), '..', 'app.asar.unpacked') + + if (!electron.app.isPackaged) { + // for development mode + appPath = join(electron.app.getAppPath()) + } + return appPath + } + // server + return join(global.core.appPath(), '../../..') +} diff --git a/core/src/node/helper/resource.ts b/core/src/node/helper/resource.ts new file mode 100644 index 0000000000..8e3cf96aa8 --- /dev/null +++ b/core/src/node/helper/resource.ts @@ -0,0 +1,15 @@ +import { SystemResourceInfo } from "../../types" +import { physicalCpuCount } from "./config" +import { log, logServer } from "./log" + +export const getSystemResourceInfo = async (): Promise => { + const cpu = await physicalCpuCount() + const message = `[NITRO]::CPU informations - ${cpu}` + log(message) + logServer(message) + + return { + numCpuPhysicalCore: cpu, + memAvailable: 0, // TODO: this should not be 0 + } +} diff --git a/core/src/node/index.ts b/core/src/node/index.ts index 10385ecfcc..31f2f076e9 100644 --- a/core/src/node/index.ts +++ b/core/src/node/index.ts @@ -2,9 +2,5 @@ export * from './extension/index' export * from './extension/extension' export * from './extension/manager' export * from './extension/store' -export * from './download' -export * from './module' export * from './api' -export * from './log' -export * from './utils' -export * from './path' +export * from './helper' diff --git a/core/src/node/path.ts b/core/src/node/path.ts deleted file mode 100644 index adbc38c6c1..0000000000 --- a/core/src/node/path.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Normalize file path - * Remove all file protocol prefix - * @param path - * @returns - */ -export function normalizeFilePath(path: string): string { - return path.replace(/^(file:[\\/]+)([^:\s]+)$/, "$2"); -} diff --git a/core/src/types/file/index.ts b/core/src/types/file/index.ts index 57d687d2f2..cc7274a28f 100644 --- a/core/src/types/file/index.ts +++ b/core/src/types/file/index.ts @@ -5,7 +5,7 @@ export type FileStat = { export type DownloadState = { modelId: string - filename: string + fileName: string time: DownloadTime speed: number percent: number diff --git a/core/tests/node/path.test.ts b/core/tests/node/path.test.ts index 9f8a557bb0..5390df1193 100644 --- a/core/tests/node/path.test.ts +++ b/core/tests/node/path.test.ts @@ -1,4 +1,4 @@ -import { normalizeFilePath } from "../../src/node/path"; +import { normalizeFilePath } from "../../src/node/helper/path"; describe("Test file normalize", () => { test("returns no file protocol prefix on Unix", async () => { diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts deleted file mode 100644 index c1f431ef3c..0000000000 --- a/electron/handlers/app.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { app, ipcMain, dialog, shell } from 'electron' -import { join, basename, relative as getRelative, isAbsolute } from 'path' -import { WindowManager } from './../managers/window' -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() { - /** - * Handles the "openAppDirectory" IPC message by opening the app's user data directory. - * The `shell.openPath` method is used to open the directory in the user's default file explorer. - * @param _event - The IPC event object. - */ - ipcMain.handle(AppRoute.openAppDirectory, async (_event) => { - shell.openPath(getJanDataFolderPath()) - }) - - /** - * Opens a URL in the user's default browser. - * @param _event - The IPC event object. - * @param url - The URL to open. - */ - ipcMain.handle(AppRoute.openExternalUrl, async (_event, url) => { - shell.openExternal(url) - }) - - /** - * Opens a URL in the user's default browser. - * @param _event - The IPC event object. - * @param url - The URL to open. - */ - ipcMain.handle(AppRoute.openFileExplore, async (_event, url) => { - shell.openPath(url) - }) - - /** - * Joins multiple paths together, respect to the current OS. - */ - ipcMain.handle(AppRoute.joinPath, async (_event, paths: string[]) => - join(...paths) - ) - - /** - * Checks if the given path is a subdirectory of the given directory. - * - * @param _event - The IPC event object. - * @param from - The path to check. - * @param to - The directory to check against. - * - * @returns {Promise} - A promise that resolves with the result. - */ - ipcMain.handle( - AppRoute.isSubdirectory, - async (_event, from: string, to: string) => { - const relative = getRelative(from, to) - const isSubdir = - relative && !relative.startsWith('..') && !isAbsolute(relative) - - if (isSubdir === '') return false - else return isSubdir - } - ) - - /** - * Retrieve basename from given path, respect to the current OS. - */ - ipcMain.handle(AppRoute.baseName, async (_event, path: string) => - basename(path) - ) - - /** - * Start Jan API Server. - */ - ipcMain.handle(AppRoute.startServer, async (_event, configs?: ServerConfig) => - startServer({ - host: configs?.host, - port: configs?.port, - isCorsEnabled: configs?.isCorsEnabled, - isVerboseEnabled: configs?.isVerboseEnabled, - schemaPath: app.isPackaged - ? join(getResourcePath(), 'docs', 'openapi', 'jan.yaml') - : undefined, - baseDir: app.isPackaged - ? join(getResourcePath(), 'docs', 'openapi') - : undefined, - }) - ) - - /** - * Stop Jan API Server. - */ - ipcMain.handle(AppRoute.stopServer, stopServer) - - /** - * Relaunches the app in production - reload window in development. - * @param _event - The IPC event object. - * @param url - The URL to reload. - */ - ipcMain.handle(AppRoute.relaunch, async (_event) => { - ModuleManager.instance.clearImportedModules() - - if (app.isPackaged) { - app.relaunch() - app.exit() - } else { - for (const modulePath in ModuleManager.instance.requiredModules) { - delete require.cache[ - require.resolve(join(getJanExtensionsPath(), modulePath)) - ] - } - 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: getJanExtensionsPath(), - }) - WindowManager.instance.currentWindow?.reload() - } - }) - - /** - * Log message to log file. - */ - 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) - } - ) -} diff --git a/electron/handlers/common.ts b/electron/handlers/common.ts new file mode 100644 index 0000000000..5a54a92bdf --- /dev/null +++ b/electron/handlers/common.ts @@ -0,0 +1,25 @@ +import { Handler, RequestHandler } from '@janhq/core/node' +import { ipcMain } from 'electron' +import { WindowManager } from '../managers/window' + +export function injectHandler() { + const ipcWrapper: Handler = ( + route: string, + listener: (...args: any[]) => any + ) => { + return ipcMain.handle(route, async (event, ...args: any[]) => { + return listener(...args) + }) + } + + const handler = new RequestHandler( + ipcWrapper, + (channel: string, args: any) => { + return WindowManager.instance.currentWindow?.webContents.send( + channel, + args + ) + } + ) + handler.handle() +} diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts deleted file mode 100644 index 85261847b2..0000000000 --- a/electron/handlers/download.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { ipcMain } from 'electron' -import { resolve, sep } 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, - getJanDataFolderPath, - normalizeFilePath, -} from '@janhq/core/node' - -export function handleDownloaderIPCs() { - /** - * Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName. - * @param _event - The IPC event object. - * @param fileName - The name of the file being downloaded. - */ - ipcMain.handle(DownloadRoute.pauseDownload, async (_event, fileName) => { - DownloadManager.instance.networkRequests[fileName]?.pause() - }) - - /** - * Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName. - * @param _event - The IPC event object. - * @param fileName - The name of the file being downloaded. - */ - ipcMain.handle(DownloadRoute.resumeDownload, async (_event, fileName) => { - DownloadManager.instance.networkRequests[fileName]?.resume() - }) - - /** - * Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName. - * The network request associated with the fileName is then removed from the networkRequests object. - * @param _event - The IPC event object. - * @param fileName - The name of the file being downloaded. - */ - ipcMain.handle(DownloadRoute.abortDownload, async (_event, fileName) => { - const rq = DownloadManager.instance.networkRequests[fileName] - if (rq) { - DownloadManager.instance.networkRequests[fileName] = undefined - rq?.abort() - } else { - WindowManager?.instance.currentWindow?.webContents.send( - DownloadEvent.onFileDownloadError, - { - fileName, - error: 'aborted', - } - ) - } - }) - - /** - * Downloads a file from a given URL. - * @param _event - The IPC event object. - * @param url - The URL to download the file from. - * @param fileName - The name to give the downloaded file. - */ - ipcMain.handle( - DownloadRoute.downloadFile, - async (_event, url, localPath, network) => { - const strictSSL = !network?.ignoreSSL - const proxy = network?.proxy?.startsWith('http') - ? network.proxy - : undefined - if (typeof localPath === 'string') { - localPath = normalizeFilePath(localPath) - } - const array = localPath.split(sep) - const fileName = array.pop() ?? '' - const modelId = array.pop() ?? '' - - const destination = resolve(getJanDataFolderPath(), localPath) - const rq = request({ url, strictSSL, proxy }) - - // Put request to download manager instance - DownloadManager.instance.setRequest(localPath, rq) - - // Downloading file to a temp file first - const downloadingTempFile = `${destination}.download` - - progress(rq, {}) - .on('progress', function (state: any) { - WindowManager?.instance.currentWindow?.webContents.send( - DownloadEvent.onFileDownloadUpdate, - { - ...state, - fileName, - modelId, - } - ) - }) - .on('error', function (error: Error) { - WindowManager?.instance.currentWindow?.webContents.send( - DownloadEvent.onFileDownloadError, - { - fileName, - modelId, - error, - } - ) - }) - .on('end', function () { - if (DownloadManager.instance.networkRequests[localPath]) { - // Finished downloading, rename temp file to actual file - renameSync(downloadingTempFile, destination) - - WindowManager?.instance.currentWindow?.webContents.send( - DownloadEvent.onFileDownloadSuccess, - { - fileName, - modelId, - } - ) - DownloadManager.instance.setRequest(localPath, undefined) - } else { - WindowManager?.instance.currentWindow?.webContents.send( - DownloadEvent.onFileDownloadError, - { - fileName, - modelId, - error: 'aborted', - } - ) - } - }) - .pipe(createWriteStream(downloadingTempFile)) - } - ) -} diff --git a/electron/handlers/extension.ts b/electron/handlers/extension.ts deleted file mode 100644 index 763c4cdecb..0000000000 --- a/electron/handlers/extension.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { ipcMain, webContents } from 'electron' -import { readdirSync } from 'fs' -import { join, extname } from 'path' - -import { - installExtensions, - getExtension, - removeExtension, - getActiveExtensions, - ModuleManager, - getJanExtensionsPath, -} from '@janhq/core/node' - -import { getResourcePath } from './../utils/path' -import { ExtensionRoute } from '@janhq/core' - -export function handleExtensionIPCs() { - /**MARK: General handlers */ - /** - * Invokes a function from a extension module in main node process. - * @param _event - The IPC event object. - * @param modulePath - The path to the extension module. - * @param method - The name of the function to invoke. - * @param args - The arguments to pass to the function. - * @returns The result of the invoked function. - */ - ipcMain.handle( - ExtensionRoute.invokeExtensionFunc, - async (_event, modulePath, method, ...args) => { - const module = require( - /* webpackIgnore: true */ join(getJanExtensionsPath(), modulePath) - ) - ModuleManager.instance.setModule(modulePath, module) - - if (typeof module[method] === 'function') { - return module[method](...args) - } else { - console.debug(module[method]) - console.error(`Function "${method}" does not exist in the module.`) - } - } - ) - - /** - * Returns the paths of the base extensions. - * @param _event - The IPC event object. - * @returns An array of paths to the base extensions. - */ - ipcMain.handle(ExtensionRoute.baseExtensions, async (_event) => { - const baseExtensionPath = join(getResourcePath(), 'pre-install') - return readdirSync(baseExtensionPath) - .filter((file) => extname(file) === '.tgz') - .map((file) => join(baseExtensionPath, file)) - }) - - /**MARK: Extension Manager handlers */ - ipcMain.handle(ExtensionRoute.installExtension, async (e, extensions) => { - // Install and activate all provided extensions - const installed = await installExtensions(extensions) - return JSON.parse(JSON.stringify(installed)) - }) - - // Register IPC route to uninstall a extension - ipcMain.handle( - ExtensionRoute.uninstallExtension, - async (e, extensions, reload) => { - // Uninstall all provided extensions - for (const ext of extensions) { - const extension = getExtension(ext) - await extension.uninstall() - if (extension.name) removeExtension(extension.name) - } - - // Reload all renderer pages if needed - reload && webContents.getAllWebContents().forEach((wc) => wc.reload()) - return true - } - ) - - // Register IPC route to update a extension - ipcMain.handle( - ExtensionRoute.updateExtension, - async (e, extensions, reload) => { - // Update all provided extensions - const updated: any[] = [] - for (const ext of extensions) { - const extension = getExtension(ext) - const res = await extension.update() - if (res) updated.push(extension) - } - - // Reload all renderer pages if needed - if (updated.length && reload) - webContents.getAllWebContents().forEach((wc) => wc.reload()) - - return JSON.parse(JSON.stringify(updated)) - } - ) - - // Register IPC route to get the list of active extensions - ipcMain.handle(ExtensionRoute.getActiveExtensions, () => { - return JSON.parse(JSON.stringify(getActiveExtensions())) - }) -} diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts deleted file mode 100644 index 15c371d34c..0000000000 --- a/electron/handlers/fileManager.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ipcMain, app } from 'electron' -// @ts-ignore -import reflect from '@alumna/reflect' - -import { FileManagerRoute, FileStat } from '@janhq/core' -import { getResourcePath } from './../utils/path' -import fs from 'fs' -import { join } from 'path' -import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node' - -/** - * Handles file system extensions operations. - */ -export function handleFileMangerIPCs() { - // Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path. - ipcMain.handle( - FileManagerRoute.syncFile, - async (_event, src: string, dest: string) => { - return reflect({ - src, - dest, - recursive: true, - delete: false, - overwrite: true, - errorOnExist: false, - }) - } - ) - - // Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path. - ipcMain.handle( - FileManagerRoute.getJanDataFolderPath, - (): Promise => Promise.resolve(getJanDataFolderPath()) - ) - - // Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path. - ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) => - getResourcePath() - ) - - // Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path. - ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) => - app.getPath('home') - ) - - // handle fs is directory here - ipcMain.handle( - FileManagerRoute.fileStat, - async (_event, path: string): Promise => { - const normalizedPath = normalizeFilePath(path) - - const fullPath = join(getJanDataFolderPath(), normalizedPath) - const isExist = fs.existsSync(fullPath) - if (!isExist) return undefined - - const isDirectory = fs.lstatSync(fullPath).isDirectory() - const size = fs.statSync(fullPath).size - - const fileStat: FileStat = { - isDirectory, - size, - } - - return fileStat - } - ) - - ipcMain.handle( - FileManagerRoute.writeBlob, - async (_event, path: string, data: string): Promise => { - try { - const normalizedPath = normalizeFilePath(path) - const dataBuffer = Buffer.from(data, 'base64') - fs.writeFileSync( - join(getJanDataFolderPath(), normalizedPath), - dataBuffer - ) - } catch (err) { - console.error(`writeFile ${path} result: ${err}`) - } - } - ) -} diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts deleted file mode 100644 index 8ac575cb25..0000000000 --- a/electron/handlers/fs.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ipcMain } from 'electron' - -import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node' -import { FileSystemRoute } from '@janhq/core' -import { join } from 'path' -/** - * Handles file system operations. - */ -export function handleFsIPCs() { - const moduleName = 'fs' - Object.values(FileSystemRoute).forEach((route) => { - ipcMain.handle(route, async (event, ...args) => { - return import(moduleName).then((mdl) => - mdl[route]( - ...args.map((arg) => - typeof arg === 'string' && - (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) - ? join(getJanDataFolderPath(), normalizeFilePath(arg)) - : arg - ) - ) - ) - }) - }) -} diff --git a/electron/handlers/native.ts b/electron/handlers/native.ts new file mode 100644 index 0000000000..14ead07bd3 --- /dev/null +++ b/electron/handlers/native.ts @@ -0,0 +1,86 @@ +import { app, ipcMain, dialog, shell } from 'electron' +import { join } from 'path' +import { WindowManager } from '../managers/window' +import { + ModuleManager, + getJanDataFolderPath, + getJanExtensionsPath, + init, +} from '@janhq/core/node' +import { NativeRoute } from '@janhq/core' + +export function handleAppIPCs() { + /** + * Handles the "openAppDirectory" IPC message by opening the app's user data directory. + * The `shell.openPath` method is used to open the directory in the user's default file explorer. + * @param _event - The IPC event object. + */ + ipcMain.handle(NativeRoute.openAppDirectory, async (_event) => { + shell.openPath(getJanDataFolderPath()) + }) + + /** + * Opens a URL in the user's default browser. + * @param _event - The IPC event object. + * @param url - The URL to open. + */ + ipcMain.handle(NativeRoute.openExternalUrl, async (_event, url) => { + shell.openExternal(url) + }) + + /** + * Opens a URL in the user's default browser. + * @param _event - The IPC event object. + * @param url - The URL to open. + */ + ipcMain.handle(NativeRoute.openFileExplore, async (_event, url) => { + shell.openPath(url) + }) + + /** + * Relaunches the app in production - reload window in development. + * @param _event - The IPC event object. + * @param url - The URL to reload. + */ + ipcMain.handle(NativeRoute.relaunch, async (_event) => { + ModuleManager.instance.clearImportedModules() + + if (app.isPackaged) { + app.relaunch() + app.exit() + } else { + for (const modulePath in ModuleManager.instance.requiredModules) { + delete require.cache[ + require.resolve(join(getJanExtensionsPath(), modulePath)) + ] + } + 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: getJanExtensionsPath(), + }) + WindowManager.instance.currentWindow?.reload() + } + }) + + ipcMain.handle(NativeRoute.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] + } + }) +} diff --git a/electron/main.ts b/electron/main.ts index 5eeb19fe92..884c0e55fe 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -9,12 +9,9 @@ import { log } from '@janhq/core/node' /** * IPC Handlers **/ -import { handleDownloaderIPCs } from './handlers/download' -import { handleExtensionIPCs } from './handlers/extension' -import { handleFileMangerIPCs } from './handlers/fileManager' -import { handleAppIPCs } from './handlers/app' +import { injectHandler } from './handlers/common' import { handleAppUpdates } from './handlers/update' -import { handleFsIPCs } from './handlers/fs' +import { handleAppIPCs } from './handlers/native' /** * Utils @@ -92,11 +89,11 @@ function createMainWindow() { * Handles various IPC messages from the renderer process. */ function handleIPCs() { - handleFsIPCs() - handleDownloaderIPCs() - handleExtensionIPCs() + // Inject core handlers for IPCs + injectHandler() + + // Handle native IPCs handleAppIPCs() - handleFileMangerIPCs() } /* diff --git a/electron/utils/menu.ts b/electron/utils/menu.ts index 7721b7c78b..4825991ee5 100644 --- a/electron/utils/menu.ts +++ b/electron/utils/menu.ts @@ -1,8 +1,7 @@ // @ts-nocheck -import { app, Menu, dialog, shell } from 'electron' +import { app, Menu, shell } from 'electron' const isMac = process.platform === 'darwin' import { autoUpdater } from 'electron-updater' -import { compareSemanticVersions } from './versionDiff' const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [ { diff --git a/electron/utils/path.ts b/electron/utils/path.ts index 4e47cc312b..4438156bcb 100644 --- a/electron/utils/path.ts +++ b/electron/utils/path.ts @@ -1,5 +1,3 @@ -import { join } from 'path' -import { app } from 'electron' import { mkdir } from 'fs-extra' import { existsSync } from 'fs' import { getJanDataFolderPath } from '@janhq/core/node' @@ -16,13 +14,3 @@ export async function createUserSpace(): Promise { } } } - -export function getResourcePath() { - let appPath = join(app.getAppPath(), '..', 'app.asar.unpacked') - - if (!app.isPackaged) { - // for development mode - appPath = join(__dirname, '..', '..') - } - return appPath -} diff --git a/electron/utils/versionDiff.ts b/electron/utils/versionDiff.ts deleted file mode 100644 index 25934e87f0..0000000000 --- a/electron/utils/versionDiff.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const compareSemanticVersions = (a: string, b: string) => { - - // 1. Split the strings into their parts. - const a1 = a.split('.'); - const b1 = b.split('.'); - // 2. Contingency in case there's a 4th or 5th version - const len = Math.min(a1.length, b1.length); - // 3. Look through each version number and compare. - for (let i = 0; i < len; i++) { - const a2 = +a1[ i ] || 0; - const b2 = +b1[ i ] || 0; - - if (a2 !== b2) { - return a2 > b2 ? 1 : -1; - } - } - - // 4. We hit this if the all checked versions so far are equal - // - return b1.length - a1.length; -}; \ No newline at end of file diff --git a/web/services/restService.ts b/web/services/restService.ts index 6b749fd718..5841d88944 100644 --- a/web/services/restService.ts +++ b/web/services/restService.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { AppRoute, + CoreRoutes, DownloadRoute, ExtensionRoute, FileManagerRoute, @@ -15,16 +16,7 @@ export function openExternalUrl(url: string) { } // Define API routes based on different route types -export const APIRoutes = [ - ...Object.values(AppRoute).map((r) => ({ path: 'app', route: r })), - ...Object.values(DownloadRoute).map((r) => ({ path: `download`, route: r })), - ...Object.values(ExtensionRoute).map((r) => ({ - path: `extension`, - route: r, - })), - ...Object.values(FileSystemRoute).map((r) => ({ path: `fs`, route: r })), - ...Object.values(FileManagerRoute).map((r) => ({ path: `fs`, route: r })), -] +export const APIRoutes = [...CoreRoutes.map((r) => ({ path: `app`, route: r }))] // Define the restAPI object with methods for each API route export const restAPI = {