Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/fuse-daemon/cmd/daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func main() {

logger.Info("fuse filesystem mounted", "mount", config.MountPoint)

if err := client.NotifyReady(logger); err != nil {
if err := client.NotifyReady(logger, config.BootID); err != nil {
logger.Error("failed to notify electron of readiness", "error", err)
if err := server.Unmount(); err != nil {
logger.Error("failed to unmount fuse filesystem", "error", err)
Expand Down
14 changes: 12 additions & 2 deletions packages/fuse-daemon/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ type Client struct {
socketPath string
}

type notifyReadyRequest struct {
BootID string `json:"bootId"`
}

func NewClient(socketPath string) *Client {
return &Client{
http: NewUnixSocketClient(socketPath),
Expand All @@ -38,14 +42,20 @@ func NewUnixSocketClient(socketPath string) *http.Client {
}

// NotifyReady sends POST /daemon/ready to Electron to signal the daemon is up.
func (client *Client) NotifyReady(logger *slog.Logger) error {
func (client *Client) NotifyReady(logger *slog.Logger, bootID string) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://localhost/daemon/ready", nil)
payload, err := json.Marshal(notifyReadyRequest{BootID: bootID})
if err != nil {
return fmt.Errorf("marshalling ready payload: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://localhost/daemon/ready", bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("creating ready request: %w", err)
}
req.Header.Set("Content-Type", "application/json")

resp, err := client.http.Do(req)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions packages/fuse-daemon/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ type Config struct {
MountPoint string
SocketPath string
LogFile string
BootID string
}
func ParseConfig() Config {
config := Config{
MountPoint: os.Getenv("INTERNXT_MOUNT"),
SocketPath: os.Getenv("INTERNXT_SOCKET"),
LogFile: os.Getenv("INTERNXT_LOG_FILE"),
BootID: os.Getenv("INTERNXT_BOOT_ID"),
}

var missing []string
Expand All @@ -27,6 +29,9 @@ func ParseConfig() Config {
if config.LogFile == "" {
missing = append(missing, "INTERNXT_LOG_FILE")
}
if config.BootID == "" {
missing = append(missing, "INTERNXT_BOOT_ID")
}

if len(missing) > 0 {
for _, envVar := range missing {
Expand Down
5 changes: 4 additions & 1 deletion src/apps/main/auth/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ describe('saveConfig and canHisConfigBeRestored', () => {
backupsEnabled: true,
backupInterval: 86_400_000,
lastBackup: 1000,
virtualDriveRoot: '/home/user/Internxt Dirve/',
syncRoot: '/home/user/Internxt',
lastSavedListing: '',
lastSync: -1,
Expand Down Expand Up @@ -132,7 +133,8 @@ describe('saveConfig and canHisConfigBeRestored', () => {
backupsEnabled: true,
backupInterval: 86_400_000,
lastBackup: 1000,
syncRoot: '/home/user/Internxt',
virtualDriveRoot: '/home/user/Internxt Drive/',
syncRoot: '/home/user/Internxt Drive',
lastSavedListing: '',
lastSync: -1,
lastOnboardingShown: '2025-01-01',
Expand Down Expand Up @@ -182,6 +184,7 @@ describe('saveConfig and canHisConfigBeRestored', () => {
backupsEnabled: true,
backupInterval: 86_400_000,
lastBackup: 1000,
virtualDriveRoot: '/home/user/Internxt Drive/',
syncRoot: '/home/user/Internxt',
lastSavedListing: '',
lastSync: -1,
Expand Down
1 change: 1 addition & 0 deletions src/apps/main/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const schema: Schema<AppStore> = {
backgroundScanEnabled: { type: 'boolean' },
backupInterval: { type: 'number' },
lastBackup: { type: 'number' },
virtualDriveRoot: { type: 'string' },
syncRoot: { type: 'string' },
lastSavedListing: { type: 'string' },
lastSync: { type: 'number' },
Expand Down
2 changes: 2 additions & 0 deletions src/apps/main/config/save-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const savedConfigFields = [
'backgroundScanEnabled',
'backupInterval',
'lastBackup',
'virtualDriveRoot',
'syncRoot',
'lastSavedListing',
'lastSync',
Expand All @@ -25,6 +26,7 @@ export function saveConfig({ uuid }: { uuid: string }) {
backgroundScanEnabled: ConfigStore.get('backgroundScanEnabled'),
backupInterval: ConfigStore.get('backupInterval'),
lastBackup: ConfigStore.get('lastBackup'),
virtualDriveRoot: ConfigStore.get('virtualDriveRoot'),
syncRoot: ConfigStore.get('syncRoot'),
lastSavedListing: ConfigStore.get('lastSavedListing'),
lastSync: ConfigStore.get('lastSync'),
Expand Down
2 changes: 1 addition & 1 deletion src/apps/main/event-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface Events {
// in on app start and the tokens are correct
USER_LOGGED_IN: () => void;

SYNC_ROOT_CHANGED: (newPath: string) => void;
SYNC_ROOT_CHANGED: ({ oldPath, newPath }: { oldPath: string; newPath: string }) => void;

USER_LOGGED_OUT: () => void;

Expand Down
1 change: 1 addition & 0 deletions src/apps/main/interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export interface IElectronAPI {
removeInfectedFiles: (infectedFiles: string[]) => Promise<void>;
cancelScan: () => Promise<void>;
};
getVirtualDriveRoot(): Promise<string>;
chooseSyncRootWithDialog(): Promise<string | null>;
getBackupErrorByFolder(folderId: number): Promise<BackupErrorRecord | undefined>;
getLastBackupHadIssues(): Promise<boolean>;
Expand Down
102 changes: 102 additions & 0 deletions src/apps/main/virtual-root-folder/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { dialog } from 'electron';
import configStore from '../config';
import eventBus from '../event-bus';
import { chooseSyncRootWithDialog, getRootVirtualDrive } from './service';

vi.mock('electron', () => ({
dialog: {
showOpenDialog: vi.fn(),
},
shell: {
openPath: vi.fn(),
},
}));

vi.mock('../../../core/electron/paths', () => ({
PATHS: {
ROOT_DRIVE_FOLDER: '/home/user/Internxt Drive',
},
}));

vi.mock('../config', () => ({
default: {
get: vi.fn(),
set: vi.fn(),
},
}));

vi.mock('../event-bus', () => ({
default: {
emit: vi.fn(),
},
}));

vi.mock('../../shared/fs/ensure-folder-exists', () => ({
ensureFolderExists: vi.fn(),
}));

describe('service', () => {
const configGetMock = vi.mocked(configStore.get);
const configSetMock = vi.mocked(configStore.set);
const eventBusEmitMock = vi.mocked(eventBus.emit);

beforeEach(() => {
const state: Record<string, string> = {
virtualDriveRoot: '',
syncRoot: '',
lastSavedListing: '',
};

configGetMock.mockImplementation((key) => state[key]);
configSetMock.mockImplementation((key, value) => {
state[key] = value;
});
});

it('should fallback to default root folder when no saved path exists', () => {
const state: Record<string, string> = {
virtualDriveRoot: '',
syncRoot: '',
lastSavedListing: '',
};

configGetMock.mockImplementation((key) => {
return state[key];
});
configSetMock.mockImplementation((key, value) => {
state[key] = value;
});

const rootPath = getRootVirtualDrive();

expect(rootPath).toBe('/home/user/Internxt Drive/');
expect(configSetMock).toHaveBeenCalledWith('virtualDriveRoot', '/home/user/');
expect(configSetMock).toHaveBeenCalledWith('syncRoot', '/home/user/');
});

it('should emit SYNC_ROOT_CHANGED with old and new paths when user picks a different folder', async () => {
const state: Record<string, string> = {
virtualDriveRoot: '/old/root/',
syncRoot: '/old/root/',
lastSavedListing: '',
};

configGetMock.mockImplementation((key) => state[key]);
configSetMock.mockImplementation((key, value) => {
state[key] = value;
});

vi.mocked(dialog.showOpenDialog).mockResolvedValue({
canceled: false,
filePaths: ['/new/root'],
} as Awaited<ReturnType<typeof dialog.showOpenDialog>>);

const selectedPath = await chooseSyncRootWithDialog();

expect(selectedPath).toBe('/new/root/Internxt Drive/');
expect(eventBusEmitMock).toHaveBeenCalledWith('SYNC_ROOT_CHANGED', {
oldPath: '/old/root/Internxt Drive/',
newPath: '/new/root/Internxt Drive/',
});
});
});
64 changes: 56 additions & 8 deletions src/apps/main/virtual-root-folder/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@
import { PATHS } from '../../../core/electron/paths';

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

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

Check warning on line 14 in src/apps/main/virtual-root-folder/service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the 'String#endsWith' method instead.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AZ5LWoTTS4U7B2AoEmMq&open=AZ5LWoTTS4U7B2AoEmMq&pullRequest=345

Check warning on line 14 in src/apps/main/virtual-root-folder/service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.at(…)` over `[….length - index]`.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AZ5LWoTTS4U7B2AoEmMr&open=AZ5LWoTTS4U7B2AoEmMr&pullRequest=345
}

function getVirtualDriveMountPath(basePath: string) {
return path.join(basePath, VIRTUAL_DRIVE_FOLDER_NAME);
}

function getBasePathFromMountPath(pathname: string) {
if (path.basename(path.resolve(pathname)) === VIRTUAL_DRIVE_FOLDER_NAME) {
return path.dirname(path.resolve(pathname));
}

return pathname;
}

function getPathFromConfig() {
return configStore.get('virtualDriveRoot');
}

export async function clearDirectory(pathname: string): Promise<boolean> {
try {
Expand All @@ -21,31 +42,58 @@
}

export function setupRootFolder(pathname: string): void {
const pathNameWithSepInTheEnd = pathname[pathname.length - 1] === path.sep ? pathname : pathname + path.sep;
const pathNameWithSepInTheEnd = normalizePathname(pathname);
configStore.set('virtualDriveRoot', pathNameWithSepInTheEnd);
// Keep legacy key synchronized for older call paths still reading syncRoot.
configStore.set('syncRoot', pathNameWithSepInTheEnd);
configStore.set('lastSavedListing', '');
}

export function getRootVirtualDrive(): string {
const current = configStore.get('syncRoot');
ensureFolderExists(current);
const current = getPathFromConfig();

if (current !== VIRTUAL_DRIVE_FOLDER) {
setupRootFolder(VIRTUAL_DRIVE_FOLDER);
if (current) {
const resolvedCurrent = path.resolve(current);

if (path.basename(resolvedCurrent) === VIRTUAL_DRIVE_FOLDER_NAME) {
setupRootFolder(getBasePathFromMountPath(resolvedCurrent));
ensureFolderExists(normalizePathname(resolvedCurrent));

return normalizePathname(resolvedCurrent);
}

const mountPath = getVirtualDriveMountPath(resolvedCurrent);
ensureFolderExists(mountPath);

return normalizePathname(mountPath);
}

return configStore.get('syncRoot');
const fallbackPath = getBasePathFromMountPath(VIRTUAL_DRIVE_FOLDER);

setupRootFolder(fallbackPath);

const rootPath = getPathFromConfig();
const mountPath = getVirtualDriveMountPath(getBasePathFromMountPath(rootPath));
ensureFolderExists(mountPath);

return normalizePathname(mountPath);
}

export async function chooseSyncRootWithDialog(): Promise<string | null> {
const previousPath = getRootVirtualDrive();
const result = await dialog.showOpenDialog({ properties: ['openDirectory'] });

if (!result.canceled) {
const chosenPath = result.filePaths[0];

setupRootFolder(chosenPath);
eventBus.emit('SYNC_ROOT_CHANGED', chosenPath);
const nextPath = getRootVirtualDrive();

if (previousPath !== nextPath) {
eventBus.emit('SYNC_ROOT_CHANGED', { oldPath: previousPath, newPath: nextPath });
}

return chosenPath;
return nextPath;
}

return null;
Expand Down
4 changes: 4 additions & 0 deletions src/apps/renderer/localize/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@
"dark": "Dark"
}
},
"virtual-drive-root": {
"label": "Internxt Folder",
"action": "Change"
},
"app-info": {
"open-logs": "Open logs",
"more": "Learn more about Internxt"
Expand Down
4 changes: 4 additions & 0 deletions src/apps/renderer/localize/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@
"dark": "Oscuro"
}
},
"virtual-drive-root": {
"label": "Directorio de Internxt",
"action": "Cambiar"
},
"app-info": {
"open-logs": "Abrir registros",
"more": "Más información sobre Internxt"
Expand Down
4 changes: 4 additions & 0 deletions src/apps/renderer/localize/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@
"dark": "Sombre"
}
},
"virtual-drive-root": {
"label": "Dossier Internxt",
"action": "Changer"
},
"app-info": {
"open-logs": "Ouvrir les registres",
"more": "Plus d'informations sur Internxt"
Expand Down
Loading
Loading