Feat/add option to change virtual drive path#345
Conversation
* feat: Get Attributes operation + Electron 21 * fix: node: http
… and French locales (#301)
* feat: golang lint * feat: golang test pipeline * chore: go daemon testing * chore: testing for virtual drive module * fix: go lint workflow * fix: go lint workflow bump version * chore: lint issues in golang * fix: pr comments * fix: format
…r creation dependencies (#310)
* feat: Implement Open and OpenDir FUSE opeartions * fix:format * fix: linting warnings * fix: simplification of fuse status transport betweeen daemon and electron * chore:tests * fix:format * chore: skip unused fuseApp class test + fix test * Fix lint warning
… services, and tests (#320)
* feat: update drive destkop linux to electron 29 and node 20 * fix: add js script to import main.ts because output code is commonjs
4c8f8cf to
3b702db
Compare
… in setup_test.go and handle-read-callback.test.ts
| const VIRTUAL_DRIVE_FOLDER_NAME = 'Internxt Drive'; | ||
|
|
||
| function normalizePathname(pathname: string) { | ||
| return pathname[pathname.length - 1] === path.sep ? pathname : pathname + path.sep; |
| import { PATHS } from '../../../core/electron/paths'; | ||
|
|
||
| const VIRTUAL_DRIVE_FOLDER = PATHS.ROOT_DRIVE_FOLDER; | ||
| const VIRTUAL_DRIVE_FOLDER_NAME = 'Internxt Drive'; |
There was a problem hiding this comment.
Maybe this could go into the paths file
| const VIRTUAL_DRIVE_FOLDER = PATHS.ROOT_DRIVE_FOLDER; | ||
| const VIRTUAL_DRIVE_FOLDER_NAME = 'Internxt Drive'; | ||
|
|
||
| function normalizePathname(pathname: string) { |
There was a problem hiding this comment.
Since this functions are not exported, maybe they could go at the bottom of the file
| virtualDriveRoot: '/home/user/Internxt Drive/', | ||
| syncRoot: '/home/user/Internxt Drive', |
There was a problem hiding this comment.
So we have 2 values that point out to the same path?
There was a problem hiding this comment.
I've removed the legacy key; I think someone tried to do this before, but it was left half-finished.
| for (const electronLog of getElectronLogModules()) { | ||
| electronLog.transports.file.resolvePathFn = (_, message) => { | ||
| return resolveAppLogFilePath({ logsPath, message }); | ||
| }; | ||
|
|
||
| coreElectronLog.transports.file.resolvePath = coreElectronLog.transports.file.resolvePathFn; | ||
| electronLog.transports.file.resolvePath = electronLog.transports.file.resolvePathFn; |
There was a problem hiding this comment.
The change was made before the antivirus log issue was resolved, but it is no longer necessary
| @@ -0,0 +1,22 @@ | |||
| import { logger } from '@internxt/drive-desktop-core/build/backend'; | |||
| import { stopVirtualDriveOnce } from './virtual-drive.service'; | |||
| } | ||
| logger.debug({ msg: '[VIRTUAL DRIVE] stopping server...' }); | ||
| await stopFuseDaemonServer(); | ||
| container = undefined; |
There was a problem hiding this comment.
It's not necessary; I was testing it in another file, and when I moved this function here, it ended up staying by accident.
| import { StorageFilesRepository } from '../../../../../context/storage/StorageFiles/domain/StorageFilesRepository'; | ||
| import { Container } from 'diod'; | ||
|
|
||
| export async function stopVirtualDrive({ container }: { container?: Container }) { |
There was a problem hiding this comment.
how different is this from the already existing stopVirtualDrive from services/virtual-drive.service.ts?
There was a problem hiding this comment.
This function is the same one; it was moved from there to here.
| await expect(startPromise).rejects.toThrow('fuse daemon exited before ready with code 1'); | ||
| }); | ||
|
|
||
| it('should require a new ready signal for each restart', async () => { |
There was a problem hiding this comment.
This test looks weird, secondREsolved is never true?
There was a problem hiding this comment.
I see why it looks a bit odd. secondResolved is intentionally checked before the second ready signal, so at that point it should still be false. After resolveDaemonReady is called with the new boot ID, secondStart resolves, which means that then callback can run and secondResolved can become true. So the behavior is correct, but I agree the test would be clearer if we also added an explicit final assertion that secondResolved becomes true.
| } | ||
|
|
||
| export function startDaemon(mountPoint: string): Promise<void> { | ||
| const bootId = randomUUID(); |
There was a problem hiding this comment.
The idea is that if there are simultaneous initializations, we can identify each one separately and keep only one active while blocking the others.
|



What is Changed / Added
Virtual Drive Root Path Selection
This PR introduces the ability for users to choose where the Internxt virtual drive is mounted on their system.
How it works
The user selects a base directory (e.g.
~/Downloads). The app always creates a fixed subfolder calledInternxt Driveinside it, so the final mount point is always<base>/Internxt Drive/. This separation ensures the FUSE filesystem is isolated and never conflicts with the user's own files in the chosen folder.The selected base path is persisted in the config store under
virtualDriveRoot. The legacysyncRootkey is kept in sync for backwards compatibility with older code paths that still read it.Remount lifecycle
Changing the root path triggers a full remount cycle orchestrated by
remountVirtualDriveOnRootChange:Stop — The running FUSE daemon receives
SIGTERM. The Go process callsserver.Unmount()to detach the filesystem from the kernel. If that fails (e.g. the file manager has the folder open), the daemon still exits.Stale mount cleanup — Before deleting the old directory,
fusermount3 -uz <oldPath>is issued as a lazy unmount. This forces the kernel to release the mount entry even if the daemon exited without cleanly unmounting, preventingENOTCONNerrors on subsequent filesystem operations.Old directory removal — The previous
Internxt Drivefolder is deleted. Several safety guards prevent accidental deletion: the path must be non-empty, must not resolve to/, and must not resolve to the user's home directory.Start — A new FUSE daemon is spawned with the updated
INTERNXT_MOUNTenvironment variable pointing to the new path. The folder is created automatically if it does not exist.Boot ID correlation
Each daemon spawn generates a random
bootId(UUID). It is passed to the Go process viaINTERNXT_BOOT_IDand echoed back in thePOST /daemon/readyHTTP payload. The Node.js side validates that the received boot ID matches the active one before resolving the startup promise. This prevents a stale ready signal from a slow-exiting previous daemon from being mistaken for the new daemon being ready.Concurrency guards
Both
stopVirtualDriveOnceandremountVirtualDriveOnRootChangededuplicate concurrent calls via an in-flight promise. If a remount is already in progress when a second call arrives (e.g. the user clicks "Change" twice), the second caller awaits the same promise rather than starting a parallel remount. The in-flight reference is cleared in afinallyblock so subsequent calls can proceed normally after completion or failure.