diff --git a/packages/fuse-daemon/internal/filesystem/mount.go b/packages/fuse-daemon/internal/filesystem/mount.go index 66e5214ce..8d5174c1c 100644 --- a/packages/fuse-daemon/internal/filesystem/mount.go +++ b/packages/fuse-daemon/internal/filesystem/mount.go @@ -22,6 +22,7 @@ func Mount(mountPoint string, logger *slog.Logger, client *client.Client) (*fuse MaxReadAhead: 128 * 1024, DisableXAttrs: false, Debug: false, + DirectMount: true, } server, _, err := nodefs.Mount(mountPoint, nodeFileSystem.Root(), mountOptions, nil) diff --git a/packages/fuse-daemon/internal/filesystem/operations.go b/packages/fuse-daemon/internal/filesystem/operations.go index b348b8615..25638ac83 100644 --- a/packages/fuse-daemon/internal/filesystem/operations.go +++ b/packages/fuse-daemon/internal/filesystem/operations.go @@ -2,6 +2,7 @@ package filesystem import ( "log/slog" + "syscall" "github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse/nodefs" @@ -30,14 +31,29 @@ func NewInternxtFilesystem(logger *slog.Logger, client *client.Client) *Internxt client: client, } } + +func isRootPath(name string) bool { + return name == "" || name == "/" +} + func (fs *InternxtFilesystem) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) { - fs.logger.Warn("not implemented", "op", "GetAttr", "path", name) - return nil, fuse.ENOSYS + if isRootPath(name) { + return &fuse.Attr{ + Mode: uint32(syscall.S_IFDIR | 0o755), + Nlink: 2, + Owner: fuse.Owner{Uid: context.Owner.Uid, Gid: context.Owner.Gid}, + }, fuse.OK + } + + return nil, fuse.ENOENT } func (fs *InternxtFilesystem) OpenDir(name string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) { - fs.logger.Warn("not implemented", "op", "OpenDir", "path", name) - return nil, fuse.ENOSYS + if isRootPath(name) { + return []fuse.DirEntry{}, fuse.OK + } + + return nil, fuse.ENOENT } // Open returns a file handle for the given path. // When implementing: return a nodefs.File handle that implements Read, Write, Release, and Flush. diff --git a/src/apps/main/interface.d.ts b/src/apps/main/interface.d.ts index 9b59155de..d61bec268 100644 --- a/src/apps/main/interface.d.ts +++ b/src/apps/main/interface.d.ts @@ -67,6 +67,7 @@ export interface IElectronAPI { onRemoteChanges(func: (value: import('../main/realtime').EventPayload) => void): () => void; openVirtualDriveFolder(): Promise; + onVirtualDriveFolderOpenError(callback: () => void): () => void; openProcessIssuesWindow(): void; diff --git a/src/apps/main/preload.d.ts b/src/apps/main/preload.d.ts index 11a06ee72..9bea8c0fe 100644 --- a/src/apps/main/preload.d.ts +++ b/src/apps/main/preload.d.ts @@ -150,6 +150,7 @@ declare interface Window { onVirtualDriveStatusChange( callback: (event: { status: import('../drive/fuse/FuseDriveStatus').FuseDriveStatus }) => void, ): () => void; + onVirtualDriveFolderOpenError(callback: () => void): () => void; startRemoteSync: () => Promise; openUrl: (url: string) => Promise; getPreferredAppLanguage: () => Promise>; diff --git a/src/apps/main/preload.js b/src/apps/main/preload.js index a4f396576..19a1c98e3 100644 --- a/src/apps/main/preload.js +++ b/src/apps/main/preload.js @@ -275,6 +275,15 @@ contextBridge.exposeInMainWorld('electron', { return () => ipcRenderer.removeListener(eventName, callbackWrapper); }, + onVirtualDriveFolderOpenError(callback) { + const eventName = 'virtual-drive-folder-open-error'; + const callbackWrapper = () => { + callback(); + }; + ipcRenderer.on(eventName, callbackWrapper); + + return () => ipcRenderer.removeListener(eventName, callbackWrapper); + }, openUrl: (url) => { ipcRenderer.invoke('open-url', url); }, diff --git a/src/apps/main/virtual-root-folder/service.ts b/src/apps/main/virtual-root-folder/service.ts index e90086038..d5c12cdeb 100644 --- a/src/apps/main/virtual-root-folder/service.ts +++ b/src/apps/main/virtual-root-folder/service.ts @@ -1,11 +1,13 @@ -import { dialog, shell } from 'electron'; +import { dialog } from 'electron'; import fs from 'fs/promises'; -import path from 'node:path'; +import { sep } from 'node:path'; import configStore from '../config'; import eventBus from '../event-bus'; -import { exec } from 'child_process'; +import { execFile } from 'node:child_process'; import { ensureFolderExists } from '../../shared/fs/ensure-folder-exists'; import { PATHS } from '../../../core/electron/paths'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import { broadcastToWindows } from '../windows'; const VIRTUAL_DRIVE_FOLDER = PATHS.ROOT_DRIVE_FOLDER; @@ -37,7 +39,7 @@ async function isEmptyFolder(pathname: string): Promise { } function setSyncRoot(pathname: string): void { - const pathNameWithSepInTheEnd = pathname[pathname.length - 1] === path.sep ? pathname : pathname + path.sep; + const pathNameWithSepInTheEnd = pathname[pathname.length - 1] === sep ? pathname : pathname + sep; configStore.set('syncRoot', pathNameWithSepInTheEnd); configStore.set('lastSavedListing', ''); } @@ -72,16 +74,15 @@ export async function chooseSyncRootWithDialog(): Promise { return null; } -export async function openVirtualDriveRootFolder() { +export async function openVirtualDriveRootFolder(): Promise { const syncFolderPath = getRootVirtualDrive(); - if (process.platform === 'linux') { - // shell.openPath is not working as intended with the mounted directory - // this is only a workaround to fix it + function openWithXdg(targetPath: string): Promise { return new Promise((resolve, reject) => { - exec(`xdg-open "${syncFolderPath}"`, (error) => { + execFile('xdg-open', [targetPath], (error) => { if (error) { reject(error); + return; } resolve(); @@ -89,7 +90,16 @@ export async function openVirtualDriveRootFolder() { }); } - const errorMessage = await shell.openPath(syncFolderPath); + try { + await openWithXdg(syncFolderPath); + return; + } catch (error) { + logger.warn({ + msg: '[VIRTUAL DRIVE] opening mountpoint failed', + syncFolderPath, + error, + }); - if (errorMessage) throw new Error(errorMessage); + broadcastToWindows('virtual-drive-folder-open-error', undefined); + } } diff --git a/src/apps/renderer/App.tsx b/src/apps/renderer/App.tsx index 495529abd..32538c220 100644 --- a/src/apps/renderer/App.tsx +++ b/src/apps/renderer/App.tsx @@ -72,6 +72,16 @@ export default function App() { return cleanup; }, []); + useEffect(() => { + const cleanup = window.electron.onVirtualDriveFolderOpenError(() => { + new Notification(i18next.t('widget.virtual-drive-folder-open-error.title'), { + body: i18next.t('widget.virtual-drive-folder-open-error.message'), + }); + }); + + return cleanup; + }, []); + return ( }> diff --git a/src/apps/renderer/localize/locales/en.json b/src/apps/renderer/localize/locales/en.json index f98187a7a..13843525c 100644 --- a/src/apps/renderer/localize/locales/en.json +++ b/src/apps/renderer/localize/locales/en.json @@ -186,6 +186,10 @@ "mounting": "Mounting...", "button": "Mount" }, + "virtual-drive-folder-open-error": { + "title": "Internxt Drive", + "message": "Internxt Drive folder is not available right now." + }, "banners": { "update-available": { "body": "A new version is available.", diff --git a/src/apps/renderer/localize/locales/es.json b/src/apps/renderer/localize/locales/es.json index 78daef474..91dec6c21 100644 --- a/src/apps/renderer/localize/locales/es.json +++ b/src/apps/renderer/localize/locales/es.json @@ -186,6 +186,10 @@ "mounting": "Montando..", "button": "Montar" }, + "virtual-drive-folder-open-error": { + "title": "Internxt Drive", + "message": "La carpeta de Internxt Drive no esta disponible en este momento." + }, "banners": { "update-available": { "body": "Hay una nueva versión disponible.", diff --git a/src/apps/renderer/localize/locales/fr.json b/src/apps/renderer/localize/locales/fr.json index cdc4faafa..f44ad0eb2 100644 --- a/src/apps/renderer/localize/locales/fr.json +++ b/src/apps/renderer/localize/locales/fr.json @@ -186,6 +186,10 @@ "mounting": "Montage...", "button": "Monter" }, + "virtual-drive-folder-open-error": { + "title": "Internxt Drive", + "message": "Le dossier Internxt Drive n'est pas disponible pour le moment." + }, "banners": { "update-available": { "body": "Une nouvelle version est disponible.",