diff --git a/core/src/api/index.ts b/core/src/api/index.ts index a232c4090..0adc8b7e2 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -12,6 +12,7 @@ export enum AppRoute { updateAppConfiguration = 'updateAppConfiguration', relaunch = 'relaunch', joinPath = 'joinPath', + isSubdirectory = 'isSubdirectory', baseName = 'baseName', startServer = 'startServer', stopServer = 'stopServer', diff --git a/core/src/core.ts b/core/src/core.ts index aa545e10e..24053e55c 100644 --- a/core/src/core.ts +++ b/core/src/core.ts @@ -22,7 +22,11 @@ const executeOnMain: (extension: string, method: string, ...args: any[]) => Prom * @param {object} network - Optional object to specify proxy/whether to ignore SSL certificates. * @returns {Promise} A promise that resolves when the file is downloaded. */ -const downloadFile: (url: string, fileName: string, network?: { proxy?: string, ignoreSSL?: boolean }) => Promise = (url, fileName, network) => { +const downloadFile: ( + url: string, + fileName: string, + network?: { proxy?: string; ignoreSSL?: boolean } +) => Promise = (url, fileName, network) => { return global.core?.api?.downloadFile(url, fileName, network) } @@ -87,6 +91,17 @@ const getResourcePath: () => Promise = () => global.core.api?.getResourc const log: (message: string, fileName?: string) => void = (message, fileName) => global.core.api?.log(message, fileName) +/** + * Check whether the path is a subdirectory of another path. + * + * @param from - The path to check. + * @param to - The path to check against. + * + * @returns {Promise} - A promise that resolves with a boolean indicating whether the path is a subdirectory. + */ +const isSubdirectory: (from: string, to: string) => Promise = (from: string, to: string) => + global.core.api?.isSubdirectory(from, to) + /** * Register extension point function type definition */ @@ -94,7 +109,7 @@ export type RegisterExtensionPoint = ( extensionName: string, extensionId: string, method: Function, - priority?: number, + priority?: number ) => void /** @@ -111,5 +126,6 @@ export { openExternalUrl, baseName, log, + isSubdirectory, FileStat, } diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index bdb70047a..c1f431ef3 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -1,5 +1,5 @@ import { app, ipcMain, dialog, shell } from 'electron' -import { join, basename } from 'path' +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' @@ -50,6 +50,27 @@ export function handleAppIPCs() { 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. */ diff --git a/web/containers/Layout/index.tsx b/web/containers/Layout/index.tsx index e7bde49c0..77a1fe971 100644 --- a/web/containers/Layout/index.tsx +++ b/web/containers/Layout/index.tsx @@ -12,7 +12,8 @@ import TopBar from '@/containers/Layout/TopBar' import { MainViewState } from '@/constants/screens' import { useMainViewState } from '@/hooks/useMainViewState' -import { SUCCESS_SET_NEW_DESTINATION } from '@/hooks/useVaultDirectory' + +import { SUCCESS_SET_NEW_DESTINATION } from '@/screens/Settings/Advanced/DataFolder' const BaseLayout = (props: PropsWithChildren) => { const { children } = props diff --git a/web/hooks/useVaultDirectory.ts b/web/hooks/useVaultDirectory.ts deleted file mode 100644 index 9d7adf2ab..000000000 --- a/web/hooks/useVaultDirectory.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useEffect, useState } from 'react' - -import { fs, AppConfiguration } from '@janhq/core' - -export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination' - -export function useVaultDirectory() { - const [isSameDirectory, setIsSameDirectory] = useState(false) - const [isDirectoryConfirm, setIsDirectoryConfirm] = useState(false) - const [isErrorSetNewDest, setIsErrorSetNewDest] = useState(false) - const [currentPath, setCurrentPath] = useState('') - const [newDestinationPath, setNewDestinationPath] = useState('') - - useEffect(() => { - window.core?.api - ?.getAppConfigurations() - ?.then((appConfig: AppConfiguration) => { - setCurrentPath(appConfig.data_folder) - }) - }, []) - - const setNewDestination = async () => { - const destFolder = await window.core?.api?.selectDirectory() - setNewDestinationPath(destFolder) - - 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..` - ) - setIsSameDirectory(true) - setIsDirectoryConfirm(false) - return - } else { - setIsSameDirectory(false) - setIsDirectoryConfirm(true) - } - setIsErrorSetNewDest(false) - } catch (e) { - console.error(`Error: ${e}`) - setIsErrorSetNewDest(true) - } - } - } - - const applyNewDestination = async () => { - try { - const appConfiguration: AppConfiguration = - await window.core?.api?.getAppConfigurations() - const currentJanDataFolder = appConfiguration.data_folder - - appConfiguration.data_folder = newDestinationPath - - await fs.syncFile(currentJanDataFolder, newDestinationPath) - await window.core?.api?.updateAppConfiguration(appConfiguration) - console.debug( - `File sync finished from ${currentPath} to ${newDestinationPath}` - ) - - setIsErrorSetNewDest(false) - localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true') - await window.core?.api?.relaunch() - } catch (e) { - console.error(`Error: ${e}`) - setIsErrorSetNewDest(true) - } - } - - return { - setNewDestination, - newDestinationPath, - applyNewDestination, - isSameDirectory, - setIsDirectoryConfirm, - isDirectoryConfirm, - setIsSameDirectory, - currentPath, - isErrorSetNewDest, - setIsErrorSetNewDest, - } -} diff --git a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx index 3729dc0d8..84646e735 100644 --- a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx +++ b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx @@ -16,7 +16,6 @@ export const showChangeFolderErrorAtom = atom(false) const ModalErrorSetDestGlobal = () => { const [show, setShow] = useAtom(showChangeFolderErrorAtom) - return ( diff --git a/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx b/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx index 8b2d90c61..1909e6428 100644 --- a/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx +++ b/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx @@ -15,7 +15,11 @@ import { atom, useAtom } from 'jotai' export const showSamePathModalAtom = atom(false) -const ModalSameDirectory = () => { +type Props = { + onChangeFolderClick: () => void +} + +const ModalSameDirectory = ({ onChangeFolderClick }: Props) => { const [show, setShow] = useAtom(showSamePathModalAtom) return ( @@ -34,7 +38,14 @@ const ModalSameDirectory = () => { - diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 4b242f235..5abd5390b 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -1,13 +1,13 @@ import { Fragment, useCallback, useEffect, useState } from 'react' -import { fs, AppConfiguration } from '@janhq/core' +import { fs, AppConfiguration, isSubdirectory } from '@janhq/core' import { Button, Input } from '@janhq/uikit' import { useSetAtom } from 'jotai' import { PencilIcon, FolderOpenIcon } from 'lucide-react' import Loader from '@/containers/Loader' -import { SUCCESS_SET_NEW_DESTINATION } from '@/hooks/useVaultDirectory' +export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination' import ModalChangeDirectory, { showDirectoryConfirmModalAtom, @@ -43,6 +43,15 @@ const DataFolder = () => { return } + const appConfiguration: AppConfiguration = + await window.core?.api?.getAppConfigurations() + const currentJanDataFolder = appConfiguration.data_folder + + if (await isSubdirectory(currentJanDataFolder, destFolder)) { + setShowSameDirectory(true) + return + } + setDestinationPath(destFolder) setShowDirectoryConfirm(true) }, [janDataFolderPath, setShowSameDirectory, setShowDirectoryConfirm]) @@ -67,6 +76,7 @@ const DataFolder = () => { await window.core?.api?.relaunch() } catch (e) { console.error(`Error: ${e}`) + setShowLoader(false) setShowChangeFolderError(true) } }, [destinationPath, setShowChangeFolderError]) @@ -107,7 +117,7 @@ const DataFolder = () => { - + { const [activeStaticMenu, setActiveStaticMenu] = useState('My Models') const [menus, setMenus] = useState([])