diff --git a/src/apps/main/interface.d.ts b/src/apps/main/interface.d.ts index 4c0a8a1093..57c8b8999a 100644 --- a/src/apps/main/interface.d.ts +++ b/src/apps/main/interface.d.ts @@ -16,6 +16,7 @@ import { StoredValues } from './config/service.types'; import { AppStore } from './config'; import { ConfigTheme } from '../shared/types/Theme'; import { UserNotification } from '../../infra/drive-server/out/dto'; +import { ChooseSyncRootResult } from './virtual-root-folder/service'; /** This interface and declare global will replace the preload.d.ts. * The thing is that instead of that, we will gradually will be declaring the interface here as we generate tests @@ -151,7 +152,7 @@ export interface IElectronAPI { cancelScan: () => Promise; }; getVirtualDriveRoot(): Promise; - chooseSyncRootWithDialog(): Promise; + chooseSyncRootWithDialog(): Promise; getBackupErrorByFolder(folderId: number): Promise; getLastBackupHadIssues(): Promise; onBackupFatalErrorsChanged(fn: (backupErrors: Array) => void): () => void; diff --git a/src/apps/main/virtual-root-folder/service.test.ts b/src/apps/main/virtual-root-folder/service.test.ts index 7324c23656..5c6a416ff3 100644 --- a/src/apps/main/virtual-root-folder/service.test.ts +++ b/src/apps/main/virtual-root-folder/service.test.ts @@ -1,6 +1,7 @@ import { dialog } from 'electron'; import configStore, { type AppStore } from '../config'; import eventBus from '../event-bus'; +import * as validateRootFolderChangeModule from './validate-root-folder-change'; import { chooseSyncRootWithDialog, getRootVirtualDrive } from './service'; vi.mock('electron', () => ({ @@ -36,10 +37,17 @@ vi.mock('../../shared/fs/ensure-folder-exists', () => ({ ensureFolderExists: vi.fn(), })); +vi.mock('./validate-root-folder-change', () => ({ + validateRootFolderChange: vi.fn(), + isPermissionError: vi.fn(), +})); + describe('service', () => { const configGetMock = vi.mocked(configStore.get); const configSetMock = vi.mocked(configStore.set); const eventBusEmitMock = vi.mocked(eventBus.emit); + const validateRootFolderChangeMock = vi.mocked(validateRootFolderChangeModule.validateRootFolderChange); + const isPermissionErrorMock = vi.mocked(validateRootFolderChangeModule.isPermissionError); beforeEach(() => { const state = new Map([ @@ -51,6 +59,9 @@ describe('service', () => { configSetMock.mockImplementation((key, value) => { state.set(key, value); }); + + validateRootFolderChangeMock.mockResolvedValue(null); + isPermissionErrorMock.mockReturnValue(false); }); it('should fallback to default root folder when no saved path exists', () => { @@ -90,10 +101,91 @@ describe('service', () => { const selectedPath = await chooseSyncRootWithDialog(); - expect(selectedPath).toBe('/new/root/Internxt Drive/'); + expect(selectedPath).toStrictEqual({ status: 'success', path: '/new/root/Internxt Drive/' }); expect(eventBusEmitMock).toHaveBeenCalledWith('SYNC_ROOT_CHANGED', { oldPath: '/old/root/Internxt Drive/', newPath: '/new/root/Internxt Drive/', }); }); + + it('should return cancelled result when user dismisses folder picker', async () => { + vi.mocked(dialog.showOpenDialog).mockResolvedValue({ + canceled: true, + filePaths: [], + } as Awaited>); + + const selectedPath = await chooseSyncRootWithDialog(); + + expect(selectedPath).toStrictEqual({ status: 'cancelled' }); + expect(validateRootFolderChangeMock).not.toHaveBeenCalled(); + expect(eventBusEmitMock).not.toHaveBeenCalled(); + }); + + it('should return validation error from validateRootFolderChange', async () => { + vi.mocked(dialog.showOpenDialog).mockResolvedValue({ + canceled: false, + filePaths: ['/media/user/usb'], + } as Awaited>); + validateRootFolderChangeMock.mockResolvedValueOnce({ + status: 'error', + code: 'REMOVABLE_DEVICE', + }); + + const selectedPath = await chooseSyncRootWithDialog(); + + expect(selectedPath).toStrictEqual({ status: 'error', code: 'REMOVABLE_DEVICE' }); + expect(eventBusEmitMock).not.toHaveBeenCalled(); + }); + + it('should not emit SYNC_ROOT_CHANGED when resulting mount path does not change', async () => { + const state = new Map([ + ['virtualDriveRoot', '/old/root/'], + ['lastSavedListing', ''], + ]); + + configGetMock.mockImplementation((key) => state.get(key) as AppStore[typeof key]); + configSetMock.mockImplementation((key, value) => { + state.set(key, value); + }); + + vi.mocked(dialog.showOpenDialog).mockResolvedValue({ + canceled: false, + filePaths: ['/old/root'], + } as Awaited>); + + const selectedPath = await chooseSyncRootWithDialog(); + + expect(selectedPath).toStrictEqual({ status: 'success', path: '/old/root/Internxt Drive/' }); + expect(eventBusEmitMock).not.toHaveBeenCalled(); + }); + + it('should return INSUFFICIENT_PERMISSION when an error is caught and identified as permission error', async () => { + vi.mocked(dialog.showOpenDialog).mockResolvedValue({ + canceled: false, + filePaths: ['/restricted/path'], + } as Awaited>); + + validateRootFolderChangeMock.mockRejectedValueOnce(new Error('permission denied')); + isPermissionErrorMock.mockReturnValueOnce(true); + + const selectedPath = await chooseSyncRootWithDialog(); + + expect(selectedPath).toStrictEqual({ status: 'error', code: 'INSUFFICIENT_PERMISSION' }); + expect(eventBusEmitMock).not.toHaveBeenCalled(); + }); + + it('should return UNKNOWN when an unknown error is caught', async () => { + vi.mocked(dialog.showOpenDialog).mockResolvedValue({ + canceled: false, + filePaths: ['/problematic/path'], + } as Awaited>); + + validateRootFolderChangeMock.mockRejectedValueOnce(new Error('unexpected failure')); + isPermissionErrorMock.mockReturnValueOnce(false); + + const selectedPath = await chooseSyncRootWithDialog(); + + expect(selectedPath).toStrictEqual({ status: 'error', code: 'UNKNOWN' }); + expect(eventBusEmitMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/apps/main/virtual-root-folder/service.ts b/src/apps/main/virtual-root-folder/service.ts index de61218a03..f4f44ae4df 100644 --- a/src/apps/main/virtual-root-folder/service.ts +++ b/src/apps/main/virtual-root-folder/service.ts @@ -6,10 +6,16 @@ import eventBus from '../event-bus'; import { exec } from 'node:child_process'; import { ensureFolderExists } from '../../shared/fs/ensure-folder-exists'; import { PATHS } from '../../../core/electron/paths'; +import { ChooseSyncRootFailureCode, isPermissionError, validateRootFolderChange } from './validate-root-folder-change'; const VIRTUAL_DRIVE_FOLDER = PATHS.ROOT_DRIVE_FOLDER; const VIRTUAL_DRIVE_FOLDER_NAME = PATHS.VIRTUAL_DRIVE_FOLDER_NAME; +export type ChooseSyncRootResult = + | { status: 'cancelled' } + | { status: 'success'; path: string } + | { status: 'error'; code: ChooseSyncRootFailureCode }; + export async function clearDirectory(pathname: string): Promise { try { await fs.rm(pathname, { recursive: true }); @@ -57,12 +63,24 @@ export function getRootVirtualDrive(): string { return normalizePathname(mountPath); } -export async function chooseSyncRootWithDialog(): Promise { +export async function chooseSyncRootWithDialog(): Promise { const previousPath = getRootVirtualDrive(); const result = await dialog.showOpenDialog({ properties: ['openDirectory'] }); - if (!result.canceled) { + if (result.canceled) { + return { status: 'cancelled' }; + } + + try { const chosenPath = result.filePaths[0]; + const validationResult = await validateRootFolderChange({ + pathname: chosenPath, + virtualDriveFolderName: VIRTUAL_DRIVE_FOLDER_NAME, + }); + + if (validationResult) { + return validationResult; + } setupRootFolder(chosenPath); const nextPath = getRootVirtualDrive(); @@ -71,10 +89,14 @@ export async function chooseSyncRootWithDialog(): Promise { eventBus.emit('SYNC_ROOT_CHANGED', { oldPath: previousPath, newPath: nextPath }); } - return nextPath; - } + return { status: 'success', path: nextPath }; + } catch (error) { + if (isPermissionError(error)) { + return { status: 'error', code: 'INSUFFICIENT_PERMISSION' }; + } - return null; + return { status: 'error', code: 'UNKNOWN' }; + } } export async function openVirtualDriveRootFolder() { diff --git a/src/apps/main/virtual-root-folder/validate-root-folder-change.test.ts b/src/apps/main/virtual-root-folder/validate-root-folder-change.test.ts new file mode 100644 index 0000000000..1763b35083 --- /dev/null +++ b/src/apps/main/virtual-root-folder/validate-root-folder-change.test.ts @@ -0,0 +1,131 @@ +import fs from 'node:fs/promises'; +import { + isPermissionError, + OTHER_CLOUD_PROVIDER_KEYWORDS, + REMOVABLE_PATH_PREFIXES, + validateRootFolderChange, +} from './validate-root-folder-change'; + +vi.mock('node:fs/promises', () => ({ + default: { + access: vi.fn(), + mkdir: vi.fn(), + }, +})); + +describe('validate-root-folder-change', () => { + const fsAccessMock = vi.mocked(fs.access); + const fsMkdirMock = vi.mocked(fs.mkdir); + + beforeEach(() => { + fsAccessMock.mockResolvedValue(undefined); + fsMkdirMock.mockResolvedValue(undefined); + }); + + it('should expose removable path prefixes', () => { + expect(REMOVABLE_PATH_PREFIXES).toMatchObject(['/media/', '/run/media/', '/mnt/']); + }); + + it('should expose known cloud provider keywords', () => { + expect(OTHER_CLOUD_PROVIDER_KEYWORDS).toContain('dropbox'); + expect(OTHER_CLOUD_PROVIDER_KEYWORDS).toContain('google drive'); + expect(OTHER_CLOUD_PROVIDER_KEYWORDS).toContain('onedrive'); + }); + + it.each(['/media/user/usb', '/run/media/user/ssd', '/mnt/external'])( + 'should return REMOVABLE_DEVICE for removable path %s', + async (pathname) => { + const result = await validateRootFolderChange({ + pathname, + virtualDriveFolderName: 'Internxt Drive', + }); + + expect(result).toStrictEqual({ status: 'error', code: 'REMOVABLE_DEVICE' }); + expect(fsAccessMock).not.toHaveBeenCalled(); + expect(fsMkdirMock).not.toHaveBeenCalled(); + }, + ); + + it('should return OTHER_CLOUD_PROVIDER when pathname contains cloud provider keyword', async () => { + const result = await validateRootFolderChange({ + pathname: '/home/user/Dropbox/Work', + virtualDriveFolderName: 'Internxt Drive', + }); + + expect(result).toStrictEqual({ status: 'error', code: 'OTHER_CLOUD_PROVIDER' }); + expect(fsAccessMock).not.toHaveBeenCalled(); + expect(fsMkdirMock).not.toHaveBeenCalled(); + }); + + it('should match cloud provider keyword case-insensitively', async () => { + const result = await validateRootFolderChange({ + pathname: '/home/user/Google Drive/My Folder', + virtualDriveFolderName: 'Internxt Drive', + }); + + expect(result).toStrictEqual({ status: 'error', code: 'OTHER_CLOUD_PROVIDER' }); + }); + + it.each(['EACCES', 'EPERM'])('should return INSUFFICIENT_PERMISSION when access fails with %s', async (code) => { + fsAccessMock.mockRejectedValueOnce(Object.assign(new Error('permission denied'), { code })); + + const result = await validateRootFolderChange({ + pathname: '/home/user/folder', + virtualDriveFolderName: 'Internxt Drive', + }); + + expect(result).toStrictEqual({ status: 'error', code: 'INSUFFICIENT_PERMISSION' }); + }); + + it('should return INSUFFICIENT_PERMISSION when mount folder creation fails with permission error', async () => { + fsMkdirMock.mockRejectedValueOnce(Object.assign(new Error('permission denied'), { code: 'EACCES' })); + + const result = await validateRootFolderChange({ + pathname: '/home/user/folder', + virtualDriveFolderName: 'Internxt Drive', + }); + + expect(result).toStrictEqual({ status: 'error', code: 'INSUFFICIENT_PERMISSION' }); + }); + + it('should throw when fs fails with unknown error', async () => { + const unknownError = Object.assign(new Error('disk failure'), { code: 'EIO' }); + fsAccessMock.mockRejectedValueOnce(unknownError); + + await expect( + validateRootFolderChange({ + pathname: '/home/user/folder', + virtualDriveFolderName: 'Internxt Drive', + }), + ).rejects.toThrow('disk failure'); + }); + + it('should return null when pathname is valid and accessible', async () => { + const result = await validateRootFolderChange({ + pathname: '/home/user/folder', + virtualDriveFolderName: 'Internxt Drive', + }); + + expect(result).toBe(null); + expect(fsAccessMock).toHaveBeenCalledTimes(2); + expect(fsMkdirMock).toHaveBeenCalledWith('/home/user/folder/Internxt Drive', { recursive: true }); + }); + + it.each(['EACCES', 'EPERM'])('isPermissionError should return true for %s', (code) => { + const result = isPermissionError(Object.assign(new Error('permission denied'), { code })); + + expect(result).toBe(true); + }); + + it('isPermissionError should return false for unknown code', () => { + const result = isPermissionError(Object.assign(new Error('other'), { code: 'ENOENT' })); + + expect(result).toBe(false); + }); + + it('isPermissionError should return false for non-objects', () => { + expect(isPermissionError(null)).toBe(false); + expect(isPermissionError(undefined)).toBe(false); + expect(isPermissionError('error')).toBe(false); + }); +}); diff --git a/src/apps/main/virtual-root-folder/validate-root-folder-change.ts b/src/apps/main/virtual-root-folder/validate-root-folder-change.ts new file mode 100644 index 0000000000..b17db3b59e --- /dev/null +++ b/src/apps/main/virtual-root-folder/validate-root-folder-change.ts @@ -0,0 +1,111 @@ +import { constants } from 'node:fs'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +export const REMOVABLE_PATH_PREFIXES = ['/media/', '/run/media/', '/mnt/']; + +export const OTHER_CLOUD_PROVIDER_KEYWORDS = [ + 'dropbox', + 'google drive', + 'onedrive', + 'icloud', + 'mega', + 'pcloud', + 'nextcloud', + 'owncloud', + 'syncthing', + 'proton drive', +]; + +export type ChooseSyncRootFailureCode = + | 'REMOVABLE_DEVICE' + | 'OTHER_CLOUD_PROVIDER' + | 'INSUFFICIENT_PERMISSION' + | 'UNKNOWN'; + +type ValidateRootFolderChangePops = { + pathname: string; + virtualDriveFolderName: string; +}; + +type ValidationErrorResult = { + status: 'error'; + code: ChooseSyncRootFailureCode; +}; + +export async function validateRootFolderChange({ + pathname, + virtualDriveFolderName, +}: ValidateRootFolderChangePops): Promise { + const resolvedPath = path.resolve(pathname); + + if (isPathInsideRemovableDevice(resolvedPath)) { + return { status: 'error', code: 'REMOVABLE_DEVICE' }; + } + + if (isPathInsideOtherCloudProvider(resolvedPath)) { + return { status: 'error', code: 'OTHER_CLOUD_PROVIDER' }; + } + + const hasPermissions = await canAccessFolderForMount({ + basePath: resolvedPath, + virtualDriveFolderName, + }); + + if (!hasPermissions) { + return { status: 'error', code: 'INSUFFICIENT_PERMISSION' }; + } + + return null; +} + +export function isPermissionError(error: unknown) { + if (!error || typeof error !== 'object') { + return false; + } + + const errorCode = 'code' in error ? error.code : undefined; + + return errorCode === 'EACCES' || errorCode === 'EPERM'; +} + +function isPathInsideRemovableDevice(pathname: string) { + const normalizedPath = normalizeForPrefixCheck(pathname); + + return REMOVABLE_PATH_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix)); +} + +function isPathInsideOtherCloudProvider(pathname: string) { + const normalizedPath = pathname.toLowerCase(); + + return OTHER_CLOUD_PROVIDER_KEYWORDS.some((providerKeyword) => normalizedPath.includes(providerKeyword)); +} + +async function canAccessFolderForMount({ + basePath, + virtualDriveFolderName, +}: { + basePath: string; + virtualDriveFolderName: string; +}) { + try { + const mountPath = path.join(basePath, virtualDriveFolderName); + + await fs.access(basePath, constants.R_OK | constants.W_OK | constants.X_OK); + await fs.mkdir(mountPath, { recursive: true }); + await fs.access(mountPath, constants.R_OK | constants.W_OK | constants.X_OK); + + return true; + } catch (error) { + if (isPermissionError(error)) { + return false; + } + + throw error; + } +} + +function normalizeForPrefixCheck(pathname: string) { + const resolvedPath = path.resolve(pathname); + return resolvedPath.endsWith(path.sep) ? resolvedPath : resolvedPath + path.sep; +} diff --git a/src/apps/renderer/localize/locales/en.json b/src/apps/renderer/localize/locales/en.json index 8c6954c8e9..03ae629c04 100644 --- a/src/apps/renderer/localize/locales/en.json +++ b/src/apps/renderer/localize/locales/en.json @@ -157,7 +157,14 @@ }, "virtual-drive-root": { "label": "Internxt Folder", - "action": "Change" + "action": "Change", + "errors": { + "title": "Path change is not allowed", + "removable-device": "You cannot mount Internxt Drive inside a removable device.", + "other-cloud-provider": "You cannot mount Internxt Drive inside folders owned by other cloud providers.", + "insufficient-permission": "You do not have enough permissions to use this path.", + "unknown": "The selected path could not be applied." + } }, "app-info": { "open-logs": "Open logs", diff --git a/src/apps/renderer/localize/locales/es.json b/src/apps/renderer/localize/locales/es.json index 80976c0a29..15c6f2e0f8 100644 --- a/src/apps/renderer/localize/locales/es.json +++ b/src/apps/renderer/localize/locales/es.json @@ -157,7 +157,14 @@ }, "virtual-drive-root": { "label": "Directorio de Internxt", - "action": "Cambiar" + "action": "Cambiar", + "errors": { + "title": "No se puede cambiar la ruta", + "removable-device": "No puedes montar Internxt Drive dentro de un dispositivo extraible.", + "other-cloud-provider": "No puedes montar Internxt Drive dentro de carpetas de otros proveedores cloud.", + "insufficient-permission": "No tienes permisos suficientes para usar esta ruta.", + "unknown": "No se ha podido cambiar la ruta seleccionada." + } }, "app-info": { "open-logs": "Abrir registros", diff --git a/src/apps/renderer/localize/locales/fr.json b/src/apps/renderer/localize/locales/fr.json index 684e72f6ec..eb49470645 100644 --- a/src/apps/renderer/localize/locales/fr.json +++ b/src/apps/renderer/localize/locales/fr.json @@ -157,7 +157,14 @@ }, "virtual-drive-root": { "label": "Dossier Internxt", - "action": "Changer" + "action": "Changer", + "errors": { + "title": "Changement de chemin non autorise", + "removable-device": "Vous ne pouvez pas monter Internxt Drive dans un peripherique amovible.", + "other-cloud-provider": "Vous ne pouvez pas monter Internxt Drive dans un dossier gere par un autre fournisseur cloud.", + "insufficient-permission": "Vous n'avez pas les permissions necessaires pour utiliser ce chemin.", + "unknown": "Le chemin selectionne n'a pas pu etre applique." + } }, "app-info": { "open-logs": "Ouvrir les registres", diff --git a/src/apps/renderer/pages/Settings/General/VirtualDriveRootPicker.test.tsx b/src/apps/renderer/pages/Settings/General/VirtualDriveRootPicker.test.tsx index 34c1922b26..4eca93cff9 100644 --- a/src/apps/renderer/pages/Settings/General/VirtualDriveRootPicker.test.tsx +++ b/src/apps/renderer/pages/Settings/General/VirtualDriveRootPicker.test.tsx @@ -9,8 +9,12 @@ describe('VirtualDriveRootPicker', () => { beforeEach(() => { vi.clearAllMocks(); window.electron.getVirtualDriveRoot = vi.fn().mockResolvedValue('/old/root/Internxt Drive/'); - window.electron.chooseSyncRootWithDialog = vi.fn().mockResolvedValue('/new/root/'); + window.electron.chooseSyncRootWithDialog = vi.fn().mockResolvedValue({ + status: 'success', + path: '/new/root/Internxt Drive/', + }); window.electron.logger.error = vi.fn(); + global.Notification = vi.fn() as unknown as typeof Notification; }); it('should render the current virtual drive root path', async () => { @@ -22,11 +26,6 @@ describe('VirtualDriveRootPicker', () => { }); it('should refresh displayed path after changing the folder', async () => { - window.electron.getVirtualDriveRoot = vi - .fn() - .mockResolvedValueOnce('/old/root/Internxt Drive/') - .mockResolvedValueOnce('/new/root/Internxt Drive/'); - render(); const changeFolderButton = await screen.findByRole('button', { @@ -37,8 +36,28 @@ describe('VirtualDriveRootPicker', () => { await waitFor(() => { expect(window.electron.chooseSyncRootWithDialog).toHaveBeenCalledOnce(); - expect(window.electron.getVirtualDriveRoot).toHaveBeenCalledTimes(2); + expect(window.electron.getVirtualDriveRoot).toHaveBeenCalledTimes(1); expect(screen.getByText('/new/root/Internxt Drive/')).toBeInTheDocument(); }); }); + + it('should show notification when selected path is not allowed', async () => { + window.electron.chooseSyncRootWithDialog = vi.fn().mockResolvedValue({ + status: 'error', + code: 'REMOVABLE_DEVICE', + }); + + render(); + + const changeFolderButton = await screen.findByRole('button', { + name: 'settings.general.virtual-drive-root.action', + }); + + fireEvent.click(changeFolderButton); + + await waitFor(() => { + expect(global.Notification).toHaveBeenCalledOnce(); + expect(screen.getByText('/old/root/Internxt Drive/')).toBeInTheDocument(); + }); + }); }); diff --git a/src/apps/renderer/pages/Settings/General/useVirtualDriveRootPicker.ts b/src/apps/renderer/pages/Settings/General/useVirtualDriveRootPicker.ts index 900f5e6032..ee24b2f344 100644 --- a/src/apps/renderer/pages/Settings/General/useVirtualDriveRootPicker.ts +++ b/src/apps/renderer/pages/Settings/General/useVirtualDriveRootPicker.ts @@ -1,6 +1,13 @@ import { useEffect, useState } from 'react'; +import { useTranslationContext } from '../../../context/LocalContext'; + +type NotificationMessagePops = { + title: string; + body: string; +}; export default function useVirtualDriveRootPicker() { + const { translate } = useTranslationContext(); const [rootPath, setRootPath] = useState(''); const [isUpdating, setIsUpdating] = useState(false); @@ -14,16 +21,31 @@ export default function useVirtualDriveRootPicker() { try { const selectedPath = await window.electron.chooseSyncRootWithDialog(); - if (!selectedPath) { + + if (selectedPath.status === 'cancelled') { return; } - await refreshRootPath(); + if (selectedPath.status === 'error') { + showValidationNotification({ + title: translate('settings.general.virtual-drive-root.errors.title'), + body: getChangeRootErrorMessage({ errorCode: selectedPath.code, translate }), + }); + + return; + } + + setRootPath(selectedPath.path); } catch (error) { window.electron.logger.error({ msg: '[SETTINGS][GENERAL] Failed to update virtual drive root folder', error, }); + + showValidationNotification({ + title: translate('settings.general.virtual-drive-root.errors.title'), + body: translate('settings.general.virtual-drive-root.errors.unknown'), + }); } finally { setIsUpdating(false); } @@ -39,3 +61,29 @@ export default function useVirtualDriveRootPicker() { onChooseFolder, }; } + +function showValidationNotification({ title, body }: NotificationMessagePops) { + new Notification(title, { body }); +} + +function getChangeRootErrorMessage({ + errorCode, + translate, +}: { + errorCode: 'REMOVABLE_DEVICE' | 'OTHER_CLOUD_PROVIDER' | 'INSUFFICIENT_PERMISSION' | 'UNKNOWN'; + translate: (key: string) => string; +}) { + if (errorCode === 'REMOVABLE_DEVICE') { + return translate('settings.general.virtual-drive-root.errors.removable-device'); + } + + if (errorCode === 'OTHER_CLOUD_PROVIDER') { + return translate('settings.general.virtual-drive-root.errors.other-cloud-provider'); + } + + if (errorCode === 'INSUFFICIENT_PERMISSION') { + return translate('settings.general.virtual-drive-root.errors.insufficient-permission'); + } + + return translate('settings.general.virtual-drive-root.errors.unknown'); +} diff --git a/src/apps/renderer/pages/Widget/SyncErrorBanner.tsx b/src/apps/renderer/pages/Widget/SyncErrorBanner.tsx index 4f1a6d78ee..c2925ddc90 100644 --- a/src/apps/renderer/pages/Widget/SyncErrorBanner.tsx +++ b/src/apps/renderer/pages/Widget/SyncErrorBanner.tsx @@ -13,7 +13,7 @@ const fatalErrorActionMap: Record void } name: 'Select folder', func: async () => { const result = await window.electron.chooseSyncRootWithDialog(); - if (result) { + if (result.status === 'success') { window.electron.startRemoteSync(); } }, @@ -22,7 +22,7 @@ const fatalErrorActionMap: Record void } name: 'Change folder', func: async () => { const result = await window.electron.chooseSyncRootWithDialog(); - if (result) { + if (result.status === 'success') { window.electron.startRemoteSync(); } }, diff --git a/src/core/utils/get-multiple-paths-from-dialog.ts b/src/core/utils/get-multiple-paths-from-dialog.ts index bebf03aa48..d94a258b56 100644 --- a/src/core/utils/get-multiple-paths-from-dialog.ts +++ b/src/core/utils/get-multiple-paths-from-dialog.ts @@ -3,6 +3,7 @@ import type { OpenDialogOptions } from 'electron'; import path from 'node:path'; import { PathTypeChecker } from '../../apps/shared/fs/PathTypeChecker '; import type { PathInfo } from '../../context/shared/domain/system-path/PathInfo'; +import { createAbsolutePath } from '../../context/local/localFile/infrastructure/AbsolutePath'; type Props = { allowFiles?: boolean; @@ -26,7 +27,7 @@ export async function getMultiplePathsFromDialog({ allowFiles = false }: Props = const itemName = path.basename(filePath); return { - path: filePath, + path: createAbsolutePath(filePath), itemName, isDirectory: isFolder, };