From 2a30747ca59a124f1b99e84cd36edc10aef4b025 Mon Sep 17 00:00:00 2001 From: Kalida Tony Date: Fri, 28 Feb 2025 01:08:30 +0600 Subject: [PATCH 1/2] feat: Add default node storage location preference - Introduced new storage preferences section in app settings - Added ability to set and change default node storage location - Implemented storage location selection with free space information - Updated translations and preload interfaces to support new feature --- assets/locales/en/translation.json | 11 +- src/main/ipc.ts | 9 + src/main/preload.ts | 3 + src/main/state/settings.ts | 10 +- .../Preferences/Preferences.tsx | 212 +++++++++++------- src/renderer/preload.d.ts | 3 + 6 files changed, 166 insertions(+), 82 deletions(-) diff --git a/assets/locales/en/translation.json b/assets/locales/en/translation.json index 1f45cabec..dc796c544 100644 --- a/assets/locales/en/translation.json +++ b/assets/locales/en/translation.json @@ -80,7 +80,7 @@ "PreReleaseUpdatesDescription": "Disabled by default. Enable to update and test pre-release versions of the app. Pre-releases are more likely to have issues.", "YouAreRunningNiceNode": "You are running NiceNode", "NoNotificationsYet": "No notifications yet", - "WellLetYouKnow": "We’ll let you know when something interesting happens!", + "WellLetYouKnow": "We'll let you know when something interesting happens!", "MarkAllAsRead": "Mark all as read", "ClearNotifications": "Clear notifications", "NotificationPreferences": "Notification preferences", @@ -97,7 +97,7 @@ "BlockExplorer": "Block Explorer", "BrowserExtensionId": "Browser Extension ID", "UnableSetWalletConnections": "Unable to set wallet connections for this node. This node is missing configuration values for this feature.", - "WalletDescription": "Connect your browser wallet to this node so you can enjoy greater security, privacy, and read speeds. Enable your favourite browser wallets below to allow access to your node. Don’t forget to add a new network in your wallet with the configuration below.", + "WalletDescription": "Connect your browser wallet to this node so you can enjoy greater security, privacy, and read speeds. Enable your favourite browser wallets below to allow access to your node. Don't forget to add a new network in your wallet with the configuration below.", "UsingLesserKnownWallet": "Using a lesser known wallet?", "SelectBrowser": "If your wallet of choice is not displayed in the list above you can select the browser used for the extension and provide the extension ID to allow access.", "AddRow": "Add Row", @@ -154,7 +154,7 @@ "NodeRequirements": "Node Requirements", "NodeStartCommand": "Node start command (must save changes to take effect)", "ResetToDefaults": "Reset to defaults", - "AddBaseNodeDescription": "Base is a secure, low-cost, developer-friendly Ethereum L2 built to bring the next billion users onchain. It's built on Optimism’s open-source OP Stack.", + "AddBaseNodeDescription": "Base is a secure, low-cost, developer-friendly Ethereum L2 built to bring the next billion users onchain. It's built on Optimism's open-source OP Stack.", "LaunchAVarNode": "Launch a {{nodeName}} Node", "AddNodeDescription": "Support the health of a network protocol by running a node.", "InitialSyncStarted": "Initial sync process started", @@ -180,5 +180,8 @@ "RunningLatestVersion": "You are running the latest version", "SuccessfullyUpdated": "Successfully updated", "Done": "Done", - "Close": "Close" + "Close": "Close", + "Storage": "Storage", + "DefaultNodeStorage": "Default Node Storage Location", + "DefaultNodeStorageDescription": "The default location where node data will be stored. This can be changed per node in node settings." } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index ab16994c9..c9e98b5cf 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -83,6 +83,7 @@ import { import { getSetIsDeveloperModeEnabled } from './state/settings.js'; import store from './state/store'; import { getSystemInfo } from './systemInfo'; +import { setDefaultStorageLocation } from './state/settings'; export const initialize = () => { ipcMain.handle( @@ -285,4 +286,12 @@ export const initialize = () => { ipcMain.handle('checkPorts', (_event, ports: number[]) => { return checkPorts(ports); }); + + // Add to the initialize function + ipcMain.handle( + 'setDefaultStorageLocation', + (_event, storageLocation: string) => { + return setDefaultStorageLocation(storageLocation); + } + ); }; diff --git a/src/main/preload.ts b/src/main/preload.ts index a6902a19c..46ce6466d 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -184,4 +184,7 @@ contextBridge.exposeInMainWorld('electron', { checkPorts: (ports: number[]) => { ipcRenderer.invoke('checkPorts', ports); }, + + setDefaultStorageLocation: (storageLocation: string) => + ipcRenderer.invoke('setDefaultStorageLocation', storageLocation), }); diff --git a/src/main/state/settings.ts b/src/main/state/settings.ts index d622a1010..9e707d9b8 100644 --- a/src/main/state/settings.ts +++ b/src/main/state/settings.ts @@ -24,9 +24,10 @@ const APP_IS_NOTIFICATIONS_ENABLED = 'appIsNotificationsEnabled'; export const APP_IS_EVENT_REPORTING_ENABLED = 'appIsEventReportingEnabled'; const APP_IS_PRE_RELEASE_UPDATES_ENABLED = 'appIsPreReleaseUpdatesEnabled'; export const APP_IS_DEVELOPER_MODE_ENABLED = 'appIsDeveloperModeEnabled'; +const APP_DEFAULT_STORAGE_LOCATION = 'appDefaultStorageLocation'; export type ThemeSetting = 'light' | 'dark' | 'auto'; -export type Settings = { +export interface Settings { [OS_PLATFORM_KEY]?: string; [OS_ARCHITECTURE]?: string; [OS_LANGUAGE_KEY]?: string; @@ -39,7 +40,8 @@ export type Settings = { [APP_IS_EVENT_REPORTING_ENABLED]?: boolean; [APP_IS_PRE_RELEASE_UPDATES_ENABLED]?: boolean; [APP_IS_DEVELOPER_MODE_ENABLED]?: boolean; -}; + [APP_DEFAULT_STORAGE_LOCATION]?: string; +} /** * Called on app launch. @@ -209,6 +211,10 @@ export const getSetIsDeveloperModeEnabled = ( return savedIsDeveloperModeEnabled; }; +export const setDefaultStorageLocation = (storageLocation: string) => { + store.set('appDefaultStorageLocation', storageLocation); +}; + // listen to OS theme updates nativeTheme.on('updated', () => { console.log("nativeTheme.on('updated')"); diff --git a/src/renderer/Presentational/Preferences/Preferences.tsx b/src/renderer/Presentational/Preferences/Preferences.tsx index 8975d201e..0b8cc959e 100644 --- a/src/renderer/Presentational/Preferences/Preferences.tsx +++ b/src/renderer/Presentational/Preferences/Preferences.tsx @@ -1,16 +1,17 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { HorizontalLine } from '../../Generics/redesign/HorizontalLine/HorizontalLine'; -import { Icon } from '../../Generics/redesign/Icon/Icon'; -import LineLabelSettings from '../../Generics/redesign/LabelSetting/LabelSettings'; -import { Toggle } from '../../Generics/redesign/Toggle/Toggle'; -import LanguageSelect from '../../LanguageSelect'; -import AutoDark from '../../assets/images/artwork/auto-dark.png'; -import AutoLight from '../../assets/images/artwork/auto-light.png'; -import DarkDark from '../../assets/images/artwork/dark-dark.png'; -import DarkLight from '../../assets/images/artwork/dark-light.png'; -import LightDark from '../../assets/images/artwork/light-dark.png'; -import LightLight from '../../assets/images/artwork/light-light.png'; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { HorizontalLine } from "../../Generics/redesign/HorizontalLine/HorizontalLine"; +import { Icon } from "../../Generics/redesign/Icon/Icon"; +import LineLabelSettings from "../../Generics/redesign/LabelSetting/LabelSettings"; +import { Toggle } from "../../Generics/redesign/Toggle/Toggle"; +import LanguageSelect from "../../LanguageSelect"; +import AutoDark from "../../assets/images/artwork/auto-dark.png"; +import AutoLight from "../../assets/images/artwork/auto-light.png"; +import DarkDark from "../../assets/images/artwork/dark-dark.png"; +import DarkLight from "../../assets/images/artwork/dark-light.png"; +import LightDark from "../../assets/images/artwork/light-dark.png"; +import LightLight from "../../assets/images/artwork/light-light.png"; +import FolderInput from "../../Generics/redesign/Input/FolderInput"; import { appearanceSection, captionText, @@ -26,17 +27,18 @@ import { themeImage, themeInnerContainer, versionContainer, -} from './preferences.css'; +} from "./preferences.css"; -export type ThemeSetting = 'light' | 'dark' | 'auto'; +export type ThemeSetting = "light" | "dark" | "auto"; export type Preference = - | 'theme' - | 'isOpenOnStartup' - | 'isNotificationsEnabled' - | 'isEventReportingEnabled' - | 'isPreReleaseUpdatesEnabled' - | 'isDeveloperModeEnabled' - | 'language'; + | "theme" + | "isOpenOnStartup" + | "isNotificationsEnabled" + | "isEventReportingEnabled" + | "isPreReleaseUpdatesEnabled" + | "isDeveloperModeEnabled" + | "language" + | "storageLocation"; export interface PreferencesProps { themeSetting?: ThemeSetting; isOpenOnStartup?: boolean; @@ -64,23 +66,40 @@ const Preferences = ({ }: PreferencesProps) => { const { t } = useTranslation(); const [initialThemeSetting] = useState(themeSetting); + const [sNodeStorageLocation, setNodeStorageLocation] = useState(""); + const [ + sNodeStorageLocationFreeStorageGBs, + setNodeStorageLocationFreeStorageGBs, + ] = useState(); + + useEffect(() => { + const fetchData = async () => { + const defaultNodesStorageDetails = + await electron.getNodesDefaultStorageLocation(); + setNodeStorageLocation(defaultNodesStorageDetails.folderPath); + setNodeStorageLocationFreeStorageGBs( + defaultNodesStorageDetails.freeStorageGBs + ); + }; + fetchData(); + }, []); const onClickTheme = (theme: ThemeSetting) => { if (onChange) { - onChange('theme', theme); + onChange("theme", theme); } }; const getThemeThumbnail = (theme: ThemeSetting) => { - const lightTheme = initialThemeSetting === 'light'; + const lightTheme = initialThemeSetting === "light"; switch (theme) { - case 'auto': - return !osDarkMode || initialThemeSetting === 'light' + case "auto": + return !osDarkMode || initialThemeSetting === "light" ? AutoLight : AutoDark; - case 'light': + case "light": return lightTheme ? LightLight : LightDark; - case 'dark': + case "dark": return lightTheme ? DarkLight : DarkDark; default: } @@ -89,20 +108,20 @@ const Preferences = ({ return (
-
{t('Appearance')}
+
{t("Appearance")}
{[ { - theme: 'auto', - label: t('AutoFollowsComputerSetting'), + theme: "auto", + label: t("AutoFollowsComputerSetting"), }, { - theme: 'light', - label: t('LightMode'), + theme: "light", + label: t("LightMode"), }, { - theme: 'dark', - label: t('DarkMode'), + theme: "dark", + label: t("DarkMode"), }, ].map((themeDetails, index) => { const isSelected = themeSetting === themeDetails.theme; @@ -113,7 +132,7 @@ const Preferences = ({ selectedStyle.push(selectedThemeContainer); } const thumbnail = getThemeThumbnail( - themeDetails.theme as ThemeSetting, + themeDetails.theme as ThemeSetting ); return (
onClickTheme(themeDetails.theme as ThemeSetting)} onKeyDown={() => onClickTheme(themeDetails.theme as ThemeSetting)} @@ -138,11 +157,11 @@ const Preferences = ({
)} -
+
{themeDetails.label}
@@ -152,36 +171,36 @@ const Preferences = ({ })}
-
{t('General')}
+
{t("General")}
{ if (onChange) { - onChange('isOpenOnStartup', newValue); + onChange("isOpenOnStartup", newValue); } }} /> ), }, { - label: t('Language'), + label: t("Language"), value: ( { if (onChange) { - onChange('language', newValue); + onChange("language", newValue); } }} /> @@ -194,24 +213,24 @@ const Preferences = ({
- {t('Notifications')} + {t("Notifications")}
{ if (onChange) { - onChange('isNotificationsEnabled', newValue); + onChange("isNotificationsEnabled", newValue); } }} /> @@ -223,29 +242,70 @@ const Preferences = ({ />
-
{t('Privacy')}
+
{t("Privacy")}
{ if (onChange) { - onChange('isEventReportingEnabled', newValue); + onChange("isEventReportingEnabled", newValue); + } + }} + /> + ), + learnMoreLink: "https://impact.nicenode.xyz", + }, + ], + }, + ]} + /> +
+
+
{t("Storage")}
+ + { + const storageLocationDetails = + await electron.openDialogForStorageLocation(); + if (storageLocationDetails) { + setNodeStorageLocation( + storageLocationDetails.folderPath + ); + setNodeStorageLocationFreeStorageGBs( + storageLocationDetails.freeStorageGBs + ); + if (onChange) { + onChange( + "storageLocation", + storageLocationDetails.folderPath + ); + } } }} /> ), - learnMoreLink: 'https://impact.nicenode.xyz', }, ], }, @@ -253,42 +313,42 @@ const Preferences = ({ />
-
{t('Advanced')}
+
{t("Advanced")}
{ if (onChange) { - onChange('isPreReleaseUpdatesEnabled', newValue); + onChange("isPreReleaseUpdatesEnabled", newValue); } }} /> ), }, { - label: `${t('Developer mode')}`, + label: `${t("Developer mode")}`, description: t( - 'Show developer information throuhout the app marked by 👷', + "Show developer information throuhout the app marked by 👷" ), value: ( { if (onChange) { - onChange('isDeveloperModeEnabled', newValue); + onChange("isDeveloperModeEnabled", newValue); } }} /> @@ -300,7 +360,7 @@ const Preferences = ({ />
- {t('YouAreRunningNiceNode')} {version} {import.meta.env.NICENODE_ENV} + {t("YouAreRunningNiceNode")} {version} {import.meta.env.NICENODE_ENV} {import.meta.env.MP_PROJECT_ENV}
diff --git a/src/renderer/preload.d.ts b/src/renderer/preload.d.ts index 67055a649..2a8823462 100644 --- a/src/renderer/preload.d.ts +++ b/src/renderer/preload.d.ts @@ -152,6 +152,9 @@ declare global { // Ports checkPorts(ports: number[]): void; + + // New method + setDefaultStorageLocation(storageLocation: string): void; }; performance: Performance; From acae7db5d2d0ee3c311fe2c3a19b6e0792c0257a Mon Sep 17 00:00:00 2001 From: Kalida Tony Date: Sat, 1 Mar 2025 19:38:39 +0600 Subject: [PATCH 2/2] feat: Add write permission check for default storage location - Implemented write permission validation when selecting storage location - Added error handling and user notification for permission denied scenarios - Updated files to support custom default storage location with fallback - Added new translations for permission-related error messages --- assets/locales/en/translation.json | 5 +++- src/main/dialog.ts | 44 ++++++++++++++++++++++++------ src/main/files.ts | 11 +++++++- src/main/ipc.ts | 2 +- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/assets/locales/en/translation.json b/assets/locales/en/translation.json index dc796c544..489a741ca 100644 --- a/assets/locales/en/translation.json +++ b/assets/locales/en/translation.json @@ -183,5 +183,8 @@ "Close": "Close", "Storage": "Storage", "DefaultNodeStorage": "Default Node Storage Location", - "DefaultNodeStorageDescription": "The default location where node data will be stored. This can be changed per node in node settings." + "DefaultNodeStorageDescription": "The default location where node data will be stored. This can be changed per node in node settings.", + "NoWritePermissions": "Permission Denied", + "NoWritePermissionsMessage": "Cannot write to selected folder", + "NoWritePermissionsDetail": "NiceNode does not have permission to write to the selected folder: {{path}}\n\nPlease select a different folder or check the folder permissions." } diff --git a/src/main/dialog.ts b/src/main/dialog.ts index 75c3c97a4..9e7ff5cce 100644 --- a/src/main/dialog.ts +++ b/src/main/dialog.ts @@ -1,4 +1,5 @@ import { type BrowserWindow, dialog } from 'electron'; +import { access, constants } from 'node:fs/promises'; import type Node from '../common/node'; import type { NodeId } from '../common/node'; @@ -61,6 +62,18 @@ export const openDialogForNodeDataDir = async (nodeId: NodeId) => { return; }; +export const checkWritePermissions = async ( + folderPath: string, +): Promise => { + try { + await access(folderPath, constants.W_OK); + return true; + } catch (err) { + logger.error(`No write permissions for path ${folderPath}:`, err); + return false; + } +}; + export const openDialogForStorageLocation = async (): Promise< CheckStorageDetails | undefined > => { @@ -77,19 +90,32 @@ export const openDialogForStorageLocation = async (): Promise< defaultPath, properties: ['openDirectory'], }); - console.log('dir select result: ', result); + if (result.canceled) { return; } - if (result.filePaths) { - if (result.filePaths.length > 0) { - const folderPath = result.filePaths[0]; - const freeStorageGBs = await getSystemFreeDiskSpace(folderPath); - return { - folderPath, - freeStorageGBs, - }; + + if (result.filePaths && result.filePaths.length > 0) { + const folderPath = result.filePaths[0]; + + // Check write permissions + const hasWritePermissions = await checkWritePermissions(folderPath); + if (!hasWritePermissions) { + // Show error dialog to user + await dialog.showMessageBox(mainWindow, { + type: 'error', + title: t('NoWritePermissions'), + message: t('NoWritePermissionsMessage'), + detail: t('NoWritePermissionsDetail', { path: folderPath }), + }); + return; } + + const freeStorageGBs = await getSystemFreeDiskSpace(folderPath); + return { + folderPath, + freeStorageGBs, + }; } return; diff --git a/src/main/files.ts b/src/main/files.ts index 26b165ea7..9d8f7cccd 100644 --- a/src/main/files.ts +++ b/src/main/files.ts @@ -8,6 +8,7 @@ import { app } from 'electron'; import logger from './logger'; import du from 'du'; +import store from './state/store'; logger.info(`App data dir: ${app.getPath('appData')}`); logger.info(`User data dir: ${app.getPath('userData')}`); @@ -27,7 +28,15 @@ export const getNNDirPath = (): string => { * @returns getNNDirPath + '/nodes' */ export const getNodesDirPath = (): string => { - return path.join(getNNDirPath(), 'nodes'); + // First check if user has set a custom default location + const userDefaultLocation = store.get('appDefaultStorageLocation'); + if (userDefaultLocation) { + return userDefaultLocation; + } + + // Fall back to default app storage location + const userDataPath = app.getPath('userData'); + return path.join(userDataPath, 'nodes'); }; export const checkAndOrCreateDir = async (dirPath: string) => { diff --git a/src/main/ipc.ts b/src/main/ipc.ts index c9e98b5cf..3068dfc4a 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -292,6 +292,6 @@ export const initialize = () => { 'setDefaultStorageLocation', (_event, storageLocation: string) => { return setDefaultStorageLocation(storageLocation); - } + }, ); };