Skip to content

Feat/add option to change virtual drive path#345

Merged
egalvis27 merged 26 commits into
mainfrom
feat/add-option-to-change-virtual-drive-path
Jun 9, 2026
Merged

Feat/add option to change virtual drive path#345
egalvis27 merged 26 commits into
mainfrom
feat/add-option-to-change-virtual-drive-path

Conversation

@egalvis27

Copy link
Copy Markdown

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 called Internxt Drive inside 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 legacy syncRoot key 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:

  1. Stop — The running FUSE daemon receives SIGTERM. The Go process calls server.Unmount() to detach the filesystem from the kernel. If that fails (e.g. the file manager has the folder open), the daemon still exits.

  2. 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, preventing ENOTCONN errors on subsequent filesystem operations.

  3. Old directory removal — The previous Internxt Drive folder 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.

  4. Start — A new FUSE daemon is spawned with the updated INTERNXT_MOUNT environment 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 via INTERNXT_BOOT_ID and echoed back in the POST /daemon/ready HTTP 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 stopVirtualDriveOnce and remountVirtualDriveOnRootChange deduplicate 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 a finally block so subsequent calls can proceed normally after completion or failure.

Base automatically changed from feat/go-fuse-daemon to main June 1, 2026 15:20
@egalvis27
egalvis27 marked this pull request as ready for review June 1, 2026 21:43
AlexisMora and others added 15 commits June 2, 2026 12:02
* feat: Get Attributes operation + Electron 21

* fix: node: http
* 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
* 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
* feat: update drive destkop linux to electron 29 and node 20

* fix: add js script to import main.ts because output code is commonjs
@egalvis27
egalvis27 force-pushed the feat/add-option-to-change-virtual-drive-path branch from 4c8f8cf to 3b702db Compare June 2, 2026 17:39
const VIRTUAL_DRIVE_FOLDER_NAME = 'Internxt Drive';

function normalizePathname(pathname: string) {
return pathname[pathname.length - 1] === path.sep ? pathname : pathname + path.sep;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check this sonarcloud issues

import { PATHS } from '../../../core/electron/paths';

const VIRTUAL_DRIVE_FOLDER = PATHS.ROOT_DRIVE_FOLDER;
const VIRTUAL_DRIVE_FOLDER_NAME = 'Internxt Drive';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could go into the paths file

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

const VIRTUAL_DRIVE_FOLDER = PATHS.ROOT_DRIVE_FOLDER;
const VIRTUAL_DRIVE_FOLDER_NAME = 'Internxt Drive';

function normalizePathname(pathname: string) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this functions are not exported, maybe they could go at the bottom of the file

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/apps/main/auth/service.test.ts Outdated
Comment on lines +139 to +140
virtualDriveRoot: '/home/user/Internxt Drive/',
syncRoot: '/home/user/Internxt Drive',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we have 2 values that point out to the same path?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the legacy key; I think someone tried to do this before, but it was left half-finished.

Comment on lines +107 to +112
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change is needed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check here the sonarcloud issues

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
logger.debug({ msg: '[VIRTUAL DRIVE] stopping server...' });
await stopFuseDaemonServer();
container = undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how different is this from the already existing stopVirtualDrive from services/virtual-drive.service.ts?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test looks weird, secondREsolved is never true?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it needs to be random

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that if there are simultaneous initializations, we can identify each one separately and keep only one active while blocking the others.

@sonarqubecloud

sonarqubecloud Bot commented Jun 8, 2026

Copy link
Copy Markdown

@egalvis27
egalvis27 merged commit 122e1d2 into main Jun 9, 2026
11 checks passed
@egalvis27
egalvis27 deleted the feat/add-option-to-change-virtual-drive-path branch June 9, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants