From bfcb7881cdf404a7e2847515a062d5a2dacdbe1a Mon Sep 17 00:00:00 2001 From: Alexis Mora Date: Wed, 15 Apr 2026 21:15:27 +0200 Subject: [PATCH 01/18] Go fuse daemon: Skeleton, mount and unix socket connection (#303) --- README.md | 2 + .../controllers/daemon.controller.ts | 9 +++ .../controllers/operations.controller.ts | 2 + src/backend/features/fuse-daemon/daemon.ts | 57 +++++++++++++++++++ .../fuse-daemon/routes/daemon.routes.ts | 10 ++++ .../fuse-daemon/routes/operations.routes.ts | 9 +++ src/backend/features/fuse-daemon/server.ts | 45 +++++++++++++++ .../fuse-daemon/services/daemon.service.ts | 9 +++ 8 files changed, 143 insertions(+) create mode 100644 src/backend/features/fuse-daemon/controllers/daemon.controller.ts create mode 100644 src/backend/features/fuse-daemon/controllers/operations.controller.ts create mode 100644 src/backend/features/fuse-daemon/daemon.ts create mode 100644 src/backend/features/fuse-daemon/routes/daemon.routes.ts create mode 100644 src/backend/features/fuse-daemon/routes/operations.routes.ts create mode 100644 src/backend/features/fuse-daemon/server.ts create mode 100644 src/backend/features/fuse-daemon/services/daemon.service.ts diff --git a/README.md b/README.md index f4d8d8aec..9ec329767 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ For the best experience with SSO authentication, we recommend using the .deb pac If working on the FUSE daemon (Go), see [packages/fuse-daemon/README.md](packages/fuse-daemon/README.md) for Go and linting tool prerequisites. +If working on the FUSE daemon (Go), see [packages/fuse-daemon/README.md](packages/fuse-daemon/README.md) for Go and linting tool prerequisites. + ### Install Clone the repo and install dependencies: diff --git a/src/backend/features/fuse-daemon/controllers/daemon.controller.ts b/src/backend/features/fuse-daemon/controllers/daemon.controller.ts new file mode 100644 index 000000000..cd4f5f4fd --- /dev/null +++ b/src/backend/features/fuse-daemon/controllers/daemon.controller.ts @@ -0,0 +1,9 @@ +import { Request, Response } from 'express'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import { resolveDaemonReady } from '../services/daemon.service'; + +export function daemonReadyController(_: Request, res: Response): void { + logger.debug({ msg: '[FUSE DAEMON] daemon ready signal received' }); + resolveDaemonReady(); + res.sendStatus(200); +} diff --git a/src/backend/features/fuse-daemon/controllers/operations.controller.ts b/src/backend/features/fuse-daemon/controllers/operations.controller.ts new file mode 100644 index 000000000..de92323a0 --- /dev/null +++ b/src/backend/features/fuse-daemon/controllers/operations.controller.ts @@ -0,0 +1,2 @@ +// Controllers for FUSE operation endpoints (POST /op/). +// Each operation will get its own handler here as it is implemented in PB-6161. diff --git a/src/backend/features/fuse-daemon/daemon.ts b/src/backend/features/fuse-daemon/daemon.ts new file mode 100644 index 000000000..78b2676df --- /dev/null +++ b/src/backend/features/fuse-daemon/daemon.ts @@ -0,0 +1,57 @@ +import { spawn, ChildProcess } from 'node:child_process'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import { PATHS } from '../../../core/electron/paths'; +import { daemonReady } from './services/daemon.service'; + +const SIGKILL_TIMEOUT_MS = 5_000; + +let daemon: ChildProcess | null = null; + +export function startDaemon(mountPoint: string): Promise { + const spawnedDaemon = spawn(PATHS.FUSE_DAEMON_BINARY, [], { + env: { + ...process.env, + INTERNXT_MOUNT: mountPoint, + INTERNXT_SOCKET: PATHS.FUSE_DAEMON_SOCKET, + INTERNXT_LOG_FILE: PATHS.FUSE_DAEMON_LOG, + }, + }); + + daemon = spawnedDaemon; + + spawnedDaemon.stderr?.on('data', (data: Buffer) => { + logger.debug({ msg: `[FUSE DAEMON] ${data.toString().trim()}` }); + }); + + return new Promise((resolve, reject) => { + spawnedDaemon.once('exit', (code: number) => { + if (code !== 0) { + reject(new Error(`fuse daemon exited before ready with code ${code}`)); + } + }); + + daemonReady.then(resolve); + }); +} + +export function stopDaemon(): Promise { + return new Promise((resolve) => { + if (!daemon) { + resolve(); + return; + } + + const timeout = setTimeout(() => { + logger.warn({ msg: '[FUSE DAEMON] daemon did not exit after SIGTERM, sending SIGKILL' }); + daemon?.kill('SIGKILL'); + }, SIGKILL_TIMEOUT_MS); + + daemon.once('exit', () => { + clearTimeout(timeout); + daemon = null; + resolve(); + }); + + daemon.kill('SIGTERM'); + }); +} diff --git a/src/backend/features/fuse-daemon/routes/daemon.routes.ts b/src/backend/features/fuse-daemon/routes/daemon.routes.ts new file mode 100644 index 000000000..1d5672447 --- /dev/null +++ b/src/backend/features/fuse-daemon/routes/daemon.routes.ts @@ -0,0 +1,10 @@ +import { Router } from 'express'; +import { daemonReadyController } from '../controllers/daemon.controller'; + +export function buildDaemonRouter(): Router { + const router = Router(); + + router.post('/ready', daemonReadyController); + + return router; +} diff --git a/src/backend/features/fuse-daemon/routes/operations.routes.ts b/src/backend/features/fuse-daemon/routes/operations.routes.ts new file mode 100644 index 000000000..337c75b31 --- /dev/null +++ b/src/backend/features/fuse-daemon/routes/operations.routes.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; + +// Routes for FUSE operation endpoints (POST /op/). +// Each operation will be registered here as it is implemented in PB-6161. +export function buildOperationsRouter(): Router { + const router = Router(); + + return router; +} diff --git a/src/backend/features/fuse-daemon/server.ts b/src/backend/features/fuse-daemon/server.ts new file mode 100644 index 000000000..754e1f79d --- /dev/null +++ b/src/backend/features/fuse-daemon/server.ts @@ -0,0 +1,45 @@ +import { rmSync } from 'node:fs'; +import express from 'express'; +import { Server } from 'node:http'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import { PATHS } from '../../../core/electron/paths'; +import { buildDaemonRouter } from './routes/daemon.routes'; +import { buildOperationsRouter } from './routes/operations.routes'; + +let server: Server | null = null; + +export function startFuseDaemonServer(): Promise { + return new Promise((resolve) => { + const app = express(); + app.use(express.json()); + + app.use('/daemon', buildDaemonRouter()); + app.use('/op', buildOperationsRouter()); + + rmSync(PATHS.FUSE_DAEMON_SOCKET, { force: true }); + + server = app.listen(PATHS.FUSE_DAEMON_SOCKET, () => { + logger.debug({ msg: '[FUSE DAEMON] server listening', socket: PATHS.FUSE_DAEMON_SOCKET }); + resolve(); + }); + }); +} + +export function stopFuseDaemonServer(): Promise { + return new Promise((resolve, reject) => { + if (!server) { + resolve(); + return; + } + + server.close((err) => { + if (err) { + reject(err); + return; + } + rmSync(PATHS.FUSE_DAEMON_SOCKET, { force: true }); + server = null; + resolve(); + }); + }); +} diff --git a/src/backend/features/fuse-daemon/services/daemon.service.ts b/src/backend/features/fuse-daemon/services/daemon.service.ts new file mode 100644 index 000000000..4362627a2 --- /dev/null +++ b/src/backend/features/fuse-daemon/services/daemon.service.ts @@ -0,0 +1,9 @@ +let resolveReady: () => void; + +export const daemonReady = new Promise((resolve) => { + resolveReady = resolve; +}); + +export function resolveDaemonReady(): void { + resolveReady(); +} From ab1358a784c1de3748135fea260c5a197d67dbfc Mon Sep 17 00:00:00 2001 From: Alexis Mora Date: Mon, 20 Apr 2026 17:11:15 +0200 Subject: [PATCH 02/18] feat: Get Attributes operation + Electron 21 (#315) * feat: Get Attributes operation + Electron 21 * fix: node: http --- package-lock.json | 2 +- .../internal/filesystem/operations.go | 2 + src/backend/features/fuse-daemon/constants.ts | 26 ++++++ .../controllers/operations.controller.ts | 19 ++++- .../fuse-daemon/routes/operations.routes.ts | 6 +- src/backend/features/fuse-daemon/server.ts | 5 +- .../services/operations.service.ts | 85 +++++++++++++++++++ 7 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 src/backend/features/fuse-daemon/constants.ts create mode 100644 src/backend/features/fuse-daemon/services/operations.service.ts diff --git a/package-lock.json b/package-lock.json index f808656ec..420a910ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23933,4 +23933,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/fuse-daemon/internal/filesystem/operations.go b/packages/fuse-daemon/internal/filesystem/operations.go index 3fcc6e1a2..2b0f84e31 100644 --- a/packages/fuse-daemon/internal/filesystem/operations.go +++ b/packages/fuse-daemon/internal/filesystem/operations.go @@ -8,6 +8,8 @@ import ( "internxt/drive-desktop-linux/fuse-daemon/internal/client" + "internxt/drive-desktop-linux/fuse-daemon/internal/client" + "github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse/nodefs" "github.com/hanwen/go-fuse/v2/fuse/pathfs" diff --git a/src/backend/features/fuse-daemon/constants.ts b/src/backend/features/fuse-daemon/constants.ts new file mode 100644 index 000000000..5a151fb25 --- /dev/null +++ b/src/backend/features/fuse-daemon/constants.ts @@ -0,0 +1,26 @@ +/** + * property to define a regular file when requesting in the get attributes fuse request. + * encodes both file type and permissions + */ +export const FILE_MODE = 33188; +/** + * property to define a folder when requesting in the get attributes fuse request. + * encodes both file type and permissions + */ +export const FOLDER_MODE = 16877; + +export type GetAttributesCallbackData = { + mode: number; + size: number; + mtime: Date; + ctime: Date; + atime?: Date; + uid: number; + gid: number; + /** this property tells the kernel the number of hard links + * for directories this is at least 2 + * when nlink reaches 0 and no process has the file open, the kernel interprets + * its a deleted file/folder + * */ + nlink: number; +}; diff --git a/src/backend/features/fuse-daemon/controllers/operations.controller.ts b/src/backend/features/fuse-daemon/controllers/operations.controller.ts index de92323a0..546587c96 100644 --- a/src/backend/features/fuse-daemon/controllers/operations.controller.ts +++ b/src/backend/features/fuse-daemon/controllers/operations.controller.ts @@ -1,2 +1,17 @@ -// Controllers for FUSE operation endpoints (POST /op/). -// Each operation will get its own handler here as it is implemented in PB-6161. +import { Request, Response } from 'express'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import { getAttributes } from '../services/operations.service'; +import { Container } from 'diod'; + +export async function getAttributesController(req: Request, res: Response, container: Container) { + logger.debug({ msg: '[FUSE DAEMON] GetAttributes signal received' }); + const rawPath: string = req.body.path ?? ''; + const normalizedPath = rawPath === '' || rawPath.startsWith('/') ? rawPath : `/${rawPath}`; + const responseGetattr = await getAttributes(normalizedPath, container); + if (responseGetattr.error) { + logger.error({ msg: responseGetattr.error.message }); + res.status(404).send(); + } else { + res.json(responseGetattr.data); + } +} diff --git a/src/backend/features/fuse-daemon/routes/operations.routes.ts b/src/backend/features/fuse-daemon/routes/operations.routes.ts index 337c75b31..56d41dfac 100644 --- a/src/backend/features/fuse-daemon/routes/operations.routes.ts +++ b/src/backend/features/fuse-daemon/routes/operations.routes.ts @@ -1,9 +1,11 @@ import { Router } from 'express'; +import { getAttributesController } from '../controllers/operations.controller'; +import { Container } from 'diod'; // Routes for FUSE operation endpoints (POST /op/). // Each operation will be registered here as it is implemented in PB-6161. -export function buildOperationsRouter(): Router { +export function buildOperationsRouter(container: Container): Router { const router = Router(); - + router.post('/getattributes', (req, res) => getAttributesController(req, res, container)); return router; } diff --git a/src/backend/features/fuse-daemon/server.ts b/src/backend/features/fuse-daemon/server.ts index 754e1f79d..af099f4c5 100644 --- a/src/backend/features/fuse-daemon/server.ts +++ b/src/backend/features/fuse-daemon/server.ts @@ -1,6 +1,7 @@ import { rmSync } from 'node:fs'; import express from 'express'; import { Server } from 'node:http'; +import { Container } from 'diod'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import { PATHS } from '../../../core/electron/paths'; import { buildDaemonRouter } from './routes/daemon.routes'; @@ -8,13 +9,13 @@ import { buildOperationsRouter } from './routes/operations.routes'; let server: Server | null = null; -export function startFuseDaemonServer(): Promise { +export function startFuseDaemonServer(container: Container): Promise { return new Promise((resolve) => { const app = express(); app.use(express.json()); app.use('/daemon', buildDaemonRouter()); - app.use('/op', buildOperationsRouter()); + app.use('/op', buildOperationsRouter(container)); rmSync(PATHS.FUSE_DAEMON_SOCKET, { force: true }); diff --git a/src/backend/features/fuse-daemon/services/operations.service.ts b/src/backend/features/fuse-daemon/services/operations.service.ts new file mode 100644 index 000000000..bfa810b46 --- /dev/null +++ b/src/backend/features/fuse-daemon/services/operations.service.ts @@ -0,0 +1,85 @@ +import { Container } from 'diod'; +import { FILE_MODE, FOLDER_MODE, GetAttributesCallbackData } from '../constants'; +import { FirstsFileSearcher } from '../../../../context/virtual-drive/files/application/search/FirstsFileSearcher'; +import { FileStatuses } from '../../../../context/virtual-drive/files/domain/FileStatus'; +import { SingleFolderMatchingSearcher } from '../../../../context/virtual-drive/folders/application/SingleFolderMatchingSearcher'; +import { TemporalFileByPathFinder } from '../../../../context/storage/TemporalFiles/application/find/TemporalFileByPathFinder'; +import { FuseCodes } from '../../../../apps/drive/fuse/callbacks/FuseCodes'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import { Result } from '../../../../context/shared/domain/Result'; +import { FuseError } from '../../../../apps/drive/fuse/callbacks/FuseErrors'; + +export async function getAttributes( + path: string, + container: Container, +): Promise> { + if (path === '/' || path === '') { + return { + data: { + mode: FOLDER_MODE, + size: 0, + mtime: new Date(), + ctime: new Date(), + atime: undefined, + uid: process.getuid?.() || 0, + gid: process.getgid?.() || 0, + nlink: 2, + }, + }; + } + + const file = await container.get(FirstsFileSearcher).run({ + path, + status: FileStatuses.EXISTS, + }); + if (file) { + return { + data: { + mode: FILE_MODE, + size: file.size, + ctime: file.createdAt, + mtime: file.updatedAt, + atime: new Date(), + uid: process.getuid?.() || 0, + gid: process.getgid?.() || 0, + nlink: 1, + }, + }; + } + const folder = await container.get(SingleFolderMatchingSearcher).run({ + path, + }); + if (folder) { + return { + data: { + mode: FOLDER_MODE, + size: 0, + ctime: folder.createdAt, + mtime: folder.updatedAt, + atime: folder.createdAt, + uid: process.getuid?.() || 0, + gid: process.getgid?.() || 0, + nlink: 2, + }, + }; + } + const document = await container.get(TemporalFileByPathFinder).run(path); + + if (document) { + return { + data: { + mode: FILE_MODE, + size: document.size.value, + mtime: new Date(), + ctime: document.createdAt, + atime: document.createdAt, + uid: process.getuid?.() || 0, + gid: process.getgid?.() || 0, + nlink: 1, + }, + }; + } + const msg = `[FUSE - GetAttributes] File not found: ${path}`; + logger.error({ msg }); + return { error: new FuseError(FuseCodes.ENOENT, msg) }; +} From df53dfb29039998ec40defbeed35edf50aab742c Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Thu, 23 Apr 2026 08:30:29 -0500 Subject: [PATCH 03/18] feat(localization): remove unused translations from English, Spanish, and French locales (#301) --- src/apps/main/interface.d.ts | 1 + src/apps/renderer/localize/locales/en.json | 2 +- src/apps/renderer/localize/locales/es.json | 2 +- src/apps/renderer/localize/locales/fr.json | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/apps/main/interface.d.ts b/src/apps/main/interface.d.ts index 26a8d5cc9..b8a8dc211 100644 --- a/src/apps/main/interface.d.ts +++ b/src/apps/main/interface.d.ts @@ -154,6 +154,7 @@ export interface IElectronAPI { onBackupFatalErrorsChanged(fn: (backupErrors: Array) => void): () => void; getBackupFatalErrors(): Promise>; onBackupProgress(func: (value: number) => void): () => void; + getFolderPath: typeof import('../../backend/features/backup/get-path-from-dialog').getPathFromDialog; startRemoteSync(): Promise; getUpdateStatus(): Promise<{ version: string } | null>; onUpdateAvailable(callback: (info: { version: string }) => void): () => void; diff --git a/src/apps/renderer/localize/locales/en.json b/src/apps/renderer/localize/locales/en.json index ec57078c4..b5ecf3e4b 100644 --- a/src/apps/renderer/localize/locales/en.json +++ b/src/apps/renderer/localize/locales/en.json @@ -412,4 +412,4 @@ "title": "Network connection lost", "message": "Your network connection has been lost. Please check your internet connection and try again." } -} +} \ No newline at end of file diff --git a/src/apps/renderer/localize/locales/es.json b/src/apps/renderer/localize/locales/es.json index f6cb17abf..c20502c85 100644 --- a/src/apps/renderer/localize/locales/es.json +++ b/src/apps/renderer/localize/locales/es.json @@ -412,4 +412,4 @@ "title": "Conexión de red perdida", "message": "Se ha perdido la conexión de red. Por favor, verifica tu conexión a internet e inténtalo de nuevo." } -} +} \ No newline at end of file diff --git a/src/apps/renderer/localize/locales/fr.json b/src/apps/renderer/localize/locales/fr.json index 5c26d5711..6a6e75c5f 100644 --- a/src/apps/renderer/localize/locales/fr.json +++ b/src/apps/renderer/localize/locales/fr.json @@ -412,4 +412,4 @@ "title": "Connexion réseau perdue", "message": "Votre connexion réseau a été perdue. Veuillez vérifier votre connexion Internet et réessayer." } -} +} \ No newline at end of file From 0c0a22140c2393d17f0c67ac0d6b76ac39c9824c Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Thu, 23 Apr 2026 09:38:54 -0500 Subject: [PATCH 04/18] feat: remove non-Linux system references and clean up platform-specific code (#296) --- src/apps/main/interface.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/apps/main/interface.d.ts b/src/apps/main/interface.d.ts index b8a8dc211..73150ae9f 100644 --- a/src/apps/main/interface.d.ts +++ b/src/apps/main/interface.d.ts @@ -62,6 +62,8 @@ export interface IElectronAPI { abortDownloadBackups: (deviceId: string) => void; + addBackupsFromLocalPaths: (folderPaths: string[]) => Promise; + renameDevice: (deviceName: string) => Promise; devices: { getDevices: () => Promise>; From 09b04f20ed34adb3a745d6680fcdb16ec62fbcd1 Mon Sep 17 00:00:00 2001 From: Alexis Mora Date: Thu, 23 Apr 2026 17:16:04 +0200 Subject: [PATCH 05/18] Chore: add golang lint + testing pipline + testing http server (#317) * 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 --- src/backend/features/fuse-daemon/constants.ts | 26 ------ .../controllers/daemon.controller.ts | 9 -- .../controllers/operations.controller.ts | 17 ---- src/backend/features/fuse-daemon/daemon.ts | 57 ------------- .../fuse-daemon/routes/daemon.routes.ts | 10 --- .../fuse-daemon/routes/operations.routes.ts | 11 --- src/backend/features/fuse-daemon/server.ts | 46 ---------- .../fuse-daemon/services/daemon.service.ts | 9 -- .../services/operations.service.ts | 85 ------------------- 9 files changed, 270 deletions(-) delete mode 100644 src/backend/features/fuse-daemon/constants.ts delete mode 100644 src/backend/features/fuse-daemon/controllers/daemon.controller.ts delete mode 100644 src/backend/features/fuse-daemon/controllers/operations.controller.ts delete mode 100644 src/backend/features/fuse-daemon/daemon.ts delete mode 100644 src/backend/features/fuse-daemon/routes/daemon.routes.ts delete mode 100644 src/backend/features/fuse-daemon/routes/operations.routes.ts delete mode 100644 src/backend/features/fuse-daemon/server.ts delete mode 100644 src/backend/features/fuse-daemon/services/daemon.service.ts delete mode 100644 src/backend/features/fuse-daemon/services/operations.service.ts diff --git a/src/backend/features/fuse-daemon/constants.ts b/src/backend/features/fuse-daemon/constants.ts deleted file mode 100644 index 5a151fb25..000000000 --- a/src/backend/features/fuse-daemon/constants.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * property to define a regular file when requesting in the get attributes fuse request. - * encodes both file type and permissions - */ -export const FILE_MODE = 33188; -/** - * property to define a folder when requesting in the get attributes fuse request. - * encodes both file type and permissions - */ -export const FOLDER_MODE = 16877; - -export type GetAttributesCallbackData = { - mode: number; - size: number; - mtime: Date; - ctime: Date; - atime?: Date; - uid: number; - gid: number; - /** this property tells the kernel the number of hard links - * for directories this is at least 2 - * when nlink reaches 0 and no process has the file open, the kernel interprets - * its a deleted file/folder - * */ - nlink: number; -}; diff --git a/src/backend/features/fuse-daemon/controllers/daemon.controller.ts b/src/backend/features/fuse-daemon/controllers/daemon.controller.ts deleted file mode 100644 index cd4f5f4fd..000000000 --- a/src/backend/features/fuse-daemon/controllers/daemon.controller.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Request, Response } from 'express'; -import { logger } from '@internxt/drive-desktop-core/build/backend'; -import { resolveDaemonReady } from '../services/daemon.service'; - -export function daemonReadyController(_: Request, res: Response): void { - logger.debug({ msg: '[FUSE DAEMON] daemon ready signal received' }); - resolveDaemonReady(); - res.sendStatus(200); -} diff --git a/src/backend/features/fuse-daemon/controllers/operations.controller.ts b/src/backend/features/fuse-daemon/controllers/operations.controller.ts deleted file mode 100644 index 546587c96..000000000 --- a/src/backend/features/fuse-daemon/controllers/operations.controller.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Request, Response } from 'express'; -import { logger } from '@internxt/drive-desktop-core/build/backend'; -import { getAttributes } from '../services/operations.service'; -import { Container } from 'diod'; - -export async function getAttributesController(req: Request, res: Response, container: Container) { - logger.debug({ msg: '[FUSE DAEMON] GetAttributes signal received' }); - const rawPath: string = req.body.path ?? ''; - const normalizedPath = rawPath === '' || rawPath.startsWith('/') ? rawPath : `/${rawPath}`; - const responseGetattr = await getAttributes(normalizedPath, container); - if (responseGetattr.error) { - logger.error({ msg: responseGetattr.error.message }); - res.status(404).send(); - } else { - res.json(responseGetattr.data); - } -} diff --git a/src/backend/features/fuse-daemon/daemon.ts b/src/backend/features/fuse-daemon/daemon.ts deleted file mode 100644 index 78b2676df..000000000 --- a/src/backend/features/fuse-daemon/daemon.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { spawn, ChildProcess } from 'node:child_process'; -import { logger } from '@internxt/drive-desktop-core/build/backend'; -import { PATHS } from '../../../core/electron/paths'; -import { daemonReady } from './services/daemon.service'; - -const SIGKILL_TIMEOUT_MS = 5_000; - -let daemon: ChildProcess | null = null; - -export function startDaemon(mountPoint: string): Promise { - const spawnedDaemon = spawn(PATHS.FUSE_DAEMON_BINARY, [], { - env: { - ...process.env, - INTERNXT_MOUNT: mountPoint, - INTERNXT_SOCKET: PATHS.FUSE_DAEMON_SOCKET, - INTERNXT_LOG_FILE: PATHS.FUSE_DAEMON_LOG, - }, - }); - - daemon = spawnedDaemon; - - spawnedDaemon.stderr?.on('data', (data: Buffer) => { - logger.debug({ msg: `[FUSE DAEMON] ${data.toString().trim()}` }); - }); - - return new Promise((resolve, reject) => { - spawnedDaemon.once('exit', (code: number) => { - if (code !== 0) { - reject(new Error(`fuse daemon exited before ready with code ${code}`)); - } - }); - - daemonReady.then(resolve); - }); -} - -export function stopDaemon(): Promise { - return new Promise((resolve) => { - if (!daemon) { - resolve(); - return; - } - - const timeout = setTimeout(() => { - logger.warn({ msg: '[FUSE DAEMON] daemon did not exit after SIGTERM, sending SIGKILL' }); - daemon?.kill('SIGKILL'); - }, SIGKILL_TIMEOUT_MS); - - daemon.once('exit', () => { - clearTimeout(timeout); - daemon = null; - resolve(); - }); - - daemon.kill('SIGTERM'); - }); -} diff --git a/src/backend/features/fuse-daemon/routes/daemon.routes.ts b/src/backend/features/fuse-daemon/routes/daemon.routes.ts deleted file mode 100644 index 1d5672447..000000000 --- a/src/backend/features/fuse-daemon/routes/daemon.routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from 'express'; -import { daemonReadyController } from '../controllers/daemon.controller'; - -export function buildDaemonRouter(): Router { - const router = Router(); - - router.post('/ready', daemonReadyController); - - return router; -} diff --git a/src/backend/features/fuse-daemon/routes/operations.routes.ts b/src/backend/features/fuse-daemon/routes/operations.routes.ts deleted file mode 100644 index 56d41dfac..000000000 --- a/src/backend/features/fuse-daemon/routes/operations.routes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Router } from 'express'; -import { getAttributesController } from '../controllers/operations.controller'; -import { Container } from 'diod'; - -// Routes for FUSE operation endpoints (POST /op/). -// Each operation will be registered here as it is implemented in PB-6161. -export function buildOperationsRouter(container: Container): Router { - const router = Router(); - router.post('/getattributes', (req, res) => getAttributesController(req, res, container)); - return router; -} diff --git a/src/backend/features/fuse-daemon/server.ts b/src/backend/features/fuse-daemon/server.ts deleted file mode 100644 index af099f4c5..000000000 --- a/src/backend/features/fuse-daemon/server.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { rmSync } from 'node:fs'; -import express from 'express'; -import { Server } from 'node:http'; -import { Container } from 'diod'; -import { logger } from '@internxt/drive-desktop-core/build/backend'; -import { PATHS } from '../../../core/electron/paths'; -import { buildDaemonRouter } from './routes/daemon.routes'; -import { buildOperationsRouter } from './routes/operations.routes'; - -let server: Server | null = null; - -export function startFuseDaemonServer(container: Container): Promise { - return new Promise((resolve) => { - const app = express(); - app.use(express.json()); - - app.use('/daemon', buildDaemonRouter()); - app.use('/op', buildOperationsRouter(container)); - - rmSync(PATHS.FUSE_DAEMON_SOCKET, { force: true }); - - server = app.listen(PATHS.FUSE_DAEMON_SOCKET, () => { - logger.debug({ msg: '[FUSE DAEMON] server listening', socket: PATHS.FUSE_DAEMON_SOCKET }); - resolve(); - }); - }); -} - -export function stopFuseDaemonServer(): Promise { - return new Promise((resolve, reject) => { - if (!server) { - resolve(); - return; - } - - server.close((err) => { - if (err) { - reject(err); - return; - } - rmSync(PATHS.FUSE_DAEMON_SOCKET, { force: true }); - server = null; - resolve(); - }); - }); -} diff --git a/src/backend/features/fuse-daemon/services/daemon.service.ts b/src/backend/features/fuse-daemon/services/daemon.service.ts deleted file mode 100644 index 4362627a2..000000000 --- a/src/backend/features/fuse-daemon/services/daemon.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -let resolveReady: () => void; - -export const daemonReady = new Promise((resolve) => { - resolveReady = resolve; -}); - -export function resolveDaemonReady(): void { - resolveReady(); -} diff --git a/src/backend/features/fuse-daemon/services/operations.service.ts b/src/backend/features/fuse-daemon/services/operations.service.ts deleted file mode 100644 index bfa810b46..000000000 --- a/src/backend/features/fuse-daemon/services/operations.service.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Container } from 'diod'; -import { FILE_MODE, FOLDER_MODE, GetAttributesCallbackData } from '../constants'; -import { FirstsFileSearcher } from '../../../../context/virtual-drive/files/application/search/FirstsFileSearcher'; -import { FileStatuses } from '../../../../context/virtual-drive/files/domain/FileStatus'; -import { SingleFolderMatchingSearcher } from '../../../../context/virtual-drive/folders/application/SingleFolderMatchingSearcher'; -import { TemporalFileByPathFinder } from '../../../../context/storage/TemporalFiles/application/find/TemporalFileByPathFinder'; -import { FuseCodes } from '../../../../apps/drive/fuse/callbacks/FuseCodes'; -import { logger } from '@internxt/drive-desktop-core/build/backend'; -import { Result } from '../../../../context/shared/domain/Result'; -import { FuseError } from '../../../../apps/drive/fuse/callbacks/FuseErrors'; - -export async function getAttributes( - path: string, - container: Container, -): Promise> { - if (path === '/' || path === '') { - return { - data: { - mode: FOLDER_MODE, - size: 0, - mtime: new Date(), - ctime: new Date(), - atime: undefined, - uid: process.getuid?.() || 0, - gid: process.getgid?.() || 0, - nlink: 2, - }, - }; - } - - const file = await container.get(FirstsFileSearcher).run({ - path, - status: FileStatuses.EXISTS, - }); - if (file) { - return { - data: { - mode: FILE_MODE, - size: file.size, - ctime: file.createdAt, - mtime: file.updatedAt, - atime: new Date(), - uid: process.getuid?.() || 0, - gid: process.getgid?.() || 0, - nlink: 1, - }, - }; - } - const folder = await container.get(SingleFolderMatchingSearcher).run({ - path, - }); - if (folder) { - return { - data: { - mode: FOLDER_MODE, - size: 0, - ctime: folder.createdAt, - mtime: folder.updatedAt, - atime: folder.createdAt, - uid: process.getuid?.() || 0, - gid: process.getgid?.() || 0, - nlink: 2, - }, - }; - } - const document = await container.get(TemporalFileByPathFinder).run(path); - - if (document) { - return { - data: { - mode: FILE_MODE, - size: document.size.value, - mtime: new Date(), - ctime: document.createdAt, - atime: document.createdAt, - uid: process.getuid?.() || 0, - gid: process.getgid?.() || 0, - nlink: 1, - }, - }; - } - const msg = `[FUSE - GetAttributes] File not found: ${path}`; - logger.error({ msg }); - return { error: new FuseError(FuseCodes.ENOENT, msg) }; -} From 8b497f155556046f957960f1c8125b24bc461536 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Mon, 27 Apr 2026 09:51:45 -0500 Subject: [PATCH 06/18] reafctor: device service.ts (#316) --- src/apps/main/interface.d.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/apps/main/interface.d.ts b/src/apps/main/interface.d.ts index 73150ae9f..26a8d5cc9 100644 --- a/src/apps/main/interface.d.ts +++ b/src/apps/main/interface.d.ts @@ -62,8 +62,6 @@ export interface IElectronAPI { abortDownloadBackups: (deviceId: string) => void; - addBackupsFromLocalPaths: (folderPaths: string[]) => Promise; - renameDevice: (deviceName: string) => Promise; devices: { getDevices: () => Promise>; @@ -156,7 +154,6 @@ export interface IElectronAPI { onBackupFatalErrorsChanged(fn: (backupErrors: Array) => void): () => void; getBackupFatalErrors(): Promise>; onBackupProgress(func: (value: number) => void): () => void; - getFolderPath: typeof import('../../backend/features/backup/get-path-from-dialog').getPathFromDialog; startRemoteSync(): Promise; getUpdateStatus(): Promise<{ version: string } | null>; onUpdateAvailable(callback: (info: { version: string }) => void): () => void; From 91c6e0403ed8443e07fa07d7d0569b1047895d70 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Mon, 27 Apr 2026 09:52:32 -0500 Subject: [PATCH 07/18] fix: [PB-6255] implement PendingFolderCreationTracker to manage folder creation dependencies (#310) --- .../callbacks/TrashFolderCallback.test.ts | 103 ++++++++++++++++++ .../application/create/FileCreator.test.ts | 1 + .../application/create/FolderCreator.test.ts | 2 + 3 files changed, 106 insertions(+) create mode 100644 src/apps/drive/fuse/callbacks/TrashFolderCallback.test.ts diff --git a/src/apps/drive/fuse/callbacks/TrashFolderCallback.test.ts b/src/apps/drive/fuse/callbacks/TrashFolderCallback.test.ts new file mode 100644 index 000000000..ab6486952 --- /dev/null +++ b/src/apps/drive/fuse/callbacks/TrashFolderCallback.test.ts @@ -0,0 +1,103 @@ +import { FolderDeleter } from '../../../../context/virtual-drive/folders/application/FolderDeleter'; +import { SingleFolderMatchingFinder } from '../../../../context/virtual-drive/folders/application/SingleFolderMatchingFinder'; +import { FolderMother } from '../../../../context/virtual-drive/folders/domain/__test-helpers__/FolderMother'; +import { FolderStatuses } from '../../../../context/virtual-drive/folders/domain/FolderStatus'; +import { SyncFolderMessenger } from '../../../../context/virtual-drive/folders/domain/SyncFolderMessenger'; +import { ContainerMock } from '../../__mocks__/ContainerMock'; +import { TrashFolderCallback } from './TrashFolderCallback'; + +describe('TrashFolderCallback', () => { + it('returns success even when folder deletion exceeds callback timeout', async () => { + vi.useFakeTimers(); + + try { + const container = new ContainerMock(); + const folder = FolderMother.any(); + + const folderFinder = { + run: vi.fn(async () => { + return folder; + }), + } as unknown as SingleFolderMatchingFinder; + + const folderDeleter = { + run: vi.fn(() => { + return new Promise((resolve) => { + setTimeout(resolve, 5_000); + }); + }), + } as unknown as FolderDeleter; + + container.set(SingleFolderMatchingFinder, folderFinder); + container.set(FolderDeleter, folderDeleter); + + const callback = new TrashFolderCallback(container as never); + const resultPromise = callback.execute('/Files/SlowFolder'); + + await vi.advanceTimersByTimeAsync(1_600); + + const result = await resultPromise; + + expect(result.isRight()).toBe(true); + expect(folderFinder.run).toHaveBeenCalledWith({ + path: '/Files/SlowFolder', + status: FolderStatuses.EXISTS, + }); + expect(folderDeleter.run).toHaveBeenCalledWith(folder.uuid); + } finally { + vi.useRealTimers(); + } + }); + + it('reports issue when background deletion fails after timeout', async () => { + vi.useFakeTimers(); + + try { + const container = new ContainerMock(); + const folder = FolderMother.any(); + + const folderFinder = { + run: vi.fn(async () => { + return folder; + }), + } as unknown as SingleFolderMatchingFinder; + + const folderDeleter = { + run: vi.fn(() => { + return new Promise((_resolve, reject) => { + setTimeout(() => { + reject(new Error('slow-delete-failed')); + }, 5_000); + }); + }), + } as unknown as FolderDeleter; + + const syncFolderMessenger = { + issue: vi.fn(async () => undefined), + } as unknown as SyncFolderMessenger; + + container.set(SingleFolderMatchingFinder, folderFinder); + container.set(FolderDeleter, folderDeleter); + container.set(SyncFolderMessenger, syncFolderMessenger); + + const callback = new TrashFolderCallback(container as never); + const resultPromise = callback.execute('/Files/SlowFolder'); + + await vi.advanceTimersByTimeAsync(1_600); + + const result = await resultPromise; + expect(result.isRight()).toBe(true); + + await vi.advanceTimersByTimeAsync(3_500); + await Promise.resolve(); + + expect(syncFolderMessenger.issue).toHaveBeenCalledWith({ + error: 'FOLDER_TRASH_ERROR', + cause: 'UNKNOWN', + name: 'SlowFolder', + }); + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/src/context/virtual-drive/files/application/create/FileCreator.test.ts b/src/context/virtual-drive/files/application/create/FileCreator.test.ts index 52c894630..a4f977005 100644 --- a/src/context/virtual-drive/files/application/create/FileCreator.test.ts +++ b/src/context/virtual-drive/files/application/create/FileCreator.test.ts @@ -27,6 +27,7 @@ describe('File Creator', () => { const parentFolderFinder = FolderFinderFactory.existingFolder(); eventBus = new EventBusMock(); notifier = new FileSyncNotifierMock(); + clearPendingCreations(); SUT = new FileCreator(remoteFileSystemMock, fileRepository, parentFolderFinder, eventBus, notifier); }); diff --git a/src/context/virtual-drive/folders/application/create/FolderCreator.test.ts b/src/context/virtual-drive/folders/application/create/FolderCreator.test.ts index bc26be20e..8fe622f76 100644 --- a/src/context/virtual-drive/folders/application/create/FolderCreator.test.ts +++ b/src/context/virtual-drive/folders/application/create/FolderCreator.test.ts @@ -10,6 +10,7 @@ import { FolderRemoteFileSystemMock } from '../../__mocks__/FolderRemoteFileSyst import { FolderRepositoryMock } from '../../__mocks__/FolderRepositoryMock'; import { FolderPathMother } from '../../domain/__test-helpers__/FolderPathMother'; import { FolderMother } from '../../domain/__test-helpers__/FolderMother'; +import { clearPendingCreations } from './PendingFolderCreationTracker'; describe('Folder Creator', () => { let repository: FolderRepositoryMock; @@ -22,6 +23,7 @@ describe('Folder Creator', () => { repository = new FolderRepositoryMock(); remote = new FolderRemoteFileSystemMock(); eventBus = new EventBusMock(); + clearPendingCreations(); const parentFolderFinder = new ParentFolderFinder(repository); From 180deb98e4d4e6935645b38ac2b552fa46afe44c Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Tue, 14 Apr 2026 18:30:13 -0500 Subject: [PATCH 08/18] feat: Refactor device management functions to use Result type for error handling --- .../BackupConfiguration.ts | 3 +- src/apps/main/interface.d.ts | 4 +- src/apps/main/preload.d.ts | 15 +- src/apps/renderer/context/DeviceContext.tsx | 17 +- .../renderer/hooks/backups/useBackups.tsx | 24 ++- .../device/createAndSetupNewDevice.test.ts | 103 ++++++++++++ .../device/createAndSetupNewDevice.ts | 9 +- .../features/device/createNewDevice.test.ts | 42 +++++ .../features/device/createNewDevice.ts | 18 +-- .../features/device/createUniqueDevice.ts | 22 +-- .../device/getBackupsFromDevice.test.ts | 133 ++++++++++++++++ .../features/device/getBackupsFromDevice.ts | 36 +++-- .../features/device/getOrCreateDevice.test.ts | 147 +++++++++++++++++- .../features/device/getOrCreateDevice.ts | 40 +---- .../features/device/renameDevice.test.ts | 67 ++++++++ src/backend/features/device/renameDevice.ts | 38 +++-- .../features/device/tryCreateDevice.ts | 20 +-- .../utils/getOrCreateDeviceHelpers.test.ts | 143 +++++++++++++++++ .../device/utils/getOrCreateDeviceHelpers.ts | 73 +++++++++ 19 files changed, 830 insertions(+), 124 deletions(-) create mode 100644 src/backend/features/device/createAndSetupNewDevice.test.ts create mode 100644 src/backend/features/device/createNewDevice.test.ts create mode 100644 src/backend/features/device/getBackupsFromDevice.test.ts create mode 100644 src/backend/features/device/renameDevice.test.ts create mode 100644 src/backend/features/device/utils/getOrCreateDeviceHelpers.test.ts create mode 100644 src/backend/features/device/utils/getOrCreateDeviceHelpers.ts diff --git a/src/apps/main/background-processes/backups/BackupConfiguration/BackupConfiguration.ts b/src/apps/main/background-processes/backups/BackupConfiguration/BackupConfiguration.ts index 97057cefe..b6999a656 100644 --- a/src/apps/main/background-processes/backups/BackupConfiguration/BackupConfiguration.ts +++ b/src/apps/main/background-processes/backups/BackupConfiguration/BackupConfiguration.ts @@ -33,7 +33,8 @@ export class BackupConfiguration { const { error, data } = await DeviceModule.getOrCreateDevice(); if (error) return []; - const enabledBackupEntries = await DeviceModule.getBackupsFromDevice(data, true); + const { error: backupsError, data: enabledBackupEntries } = await DeviceModule.getBackupsFromDevice(data, true); + if (backupsError || !enabledBackupEntries) return []; return this.map(enabledBackupEntries, data.bucket); } diff --git a/src/apps/main/interface.d.ts b/src/apps/main/interface.d.ts index 26a8d5cc9..cc6f327c4 100644 --- a/src/apps/main/interface.d.ts +++ b/src/apps/main/interface.d.ts @@ -36,7 +36,7 @@ export interface IElectronAPI { getOrCreateDevice: () => Promise>; - getBackupsFromDevice: (device: Device, isCurrent?: boolean) => Promise>; + getBackupsFromDevice: (device: Device, isCurrent?: boolean) => Promise, Error>>; addBackup: () => Promise>; @@ -62,7 +62,7 @@ export interface IElectronAPI { abortDownloadBackups: (deviceId: string) => void; - renameDevice: (deviceName: string) => Promise; + renameDevice: (deviceName: string) => Promise>; devices: { getDevices: () => Promise>; }; diff --git a/src/apps/main/preload.d.ts b/src/apps/main/preload.d.ts index 2a5774606..56ae9c813 100644 --- a/src/apps/main/preload.d.ts +++ b/src/apps/main/preload.d.ts @@ -100,9 +100,13 @@ declare interface Window { path: typeof import('path'); - getOrCreateDevice: typeof import('../../backend/features/device/device.module').DeviceModule.getOrCreateDevice; + getOrCreateDevice: () => Promise< + import('../../context/shared/domain/Result').Result + >; - renameDevice: typeof import('../../backend/features/device/device.module').DeviceModule.renameDevice; + renameDevice: ( + deviceName: string, + ) => Promise>; devices: { getDevices: () => Promise>; @@ -110,7 +114,12 @@ declare interface Window { onDeviceCreated(func: (value: Device) => void): () => void; - getBackupsFromDevice: typeof import('../../backend/features/device/device.module').DeviceModule.getBackupsFromDevice; + getBackupsFromDevice: ( + device: import('../main/device/service').Device, + isCurrent?: boolean, + ) => Promise< + import('../../context/shared/domain/Result').Result + >; addBackup: typeof import('../../backend/features/backup/add-backup').addBackup; diff --git a/src/apps/renderer/context/DeviceContext.tsx b/src/apps/renderer/context/DeviceContext.tsx index dc7a1e3aa..7318504e7 100644 --- a/src/apps/renderer/context/DeviceContext.tsx +++ b/src/apps/renderer/context/DeviceContext.tsx @@ -58,18 +58,15 @@ export function DeviceProvider({ children }: { children: ReactNode }) { const deviceRename = async (deviceName: string) => { setDeviceState({ status: 'LOADING' }); - try { - const updatedDevice = await window.electron.renameDevice(deviceName); - setDeviceState({ status: 'SUCCESS', device: updatedDevice }); - setCurrent(updatedDevice); - setSelected(updatedDevice); - } catch (err) { - window.electron.logger.error({ - msg: '[RENDERER] Failed to rename device', - error: err, - }); + const { error, data: updatedDevice } = await window.electron.renameDevice(deviceName); + if (error || !updatedDevice) { setDeviceState({ status: 'ERROR' }); + return; } + + setDeviceState({ status: 'SUCCESS', device: updatedDevice }); + setCurrent(updatedDevice); + setSelected(updatedDevice); }; return ( diff --git a/src/apps/renderer/hooks/backups/useBackups.tsx b/src/apps/renderer/hooks/backups/useBackups.tsx index 9a169ec57..d4cf97337 100644 --- a/src/apps/renderer/hooks/backups/useBackups.tsx +++ b/src/apps/renderer/hooks/backups/useBackups.tsx @@ -23,10 +23,17 @@ export function useBackups(): BackupContextProps { const [backups, setBackups] = useState>([]); const [hasExistingBackups, setHasExistingBackups] = useState(false); - async function fetchBackups(): Promise { - if (!selected) return; - const backups = await window.electron.getBackupsFromDevice(selected, selected === current); - setBackups(backups); + async function fetchBackups(): Promise { + if (!selected) return true; + + const { error, data } = await window.electron.getBackupsFromDevice(selected, selected === current); + if (error || !data) { + setBackups([]); + return false; + } + + setBackups(data); + return true; } const validateIfBackupExists = async () => { @@ -38,13 +45,14 @@ export function useBackups(): BackupContextProps { setBackupsState('LOADING'); setBackups([]); - try { - await fetchBackups(); - setBackupsState('SUCCESS'); - } catch { + const isLoaded = await fetchBackups(); + if (!isLoaded) { setBackupsState('ERROR'); setBackups([]); + return; } + + setBackupsState('SUCCESS'); } useEffect(() => { diff --git a/src/backend/features/device/createAndSetupNewDevice.test.ts b/src/backend/features/device/createAndSetupNewDevice.test.ts new file mode 100644 index 000000000..8c7d9509a --- /dev/null +++ b/src/backend/features/device/createAndSetupNewDevice.test.ts @@ -0,0 +1,103 @@ +import { BrowserWindow } from 'electron'; +import { broadcastToWindows } from '../../../apps/main/windows'; +import { DependencyInjectionUserProvider } from '../../../apps/shared/dependency-injection/DependencyInjectionUserProvider'; +import { createNewDevice } from './createNewDevice'; +import { createAndSetupNewDevice } from './createAndSetupNewDevice'; +import { getDeviceIdentifier } from './getDeviceIdentifier'; + +vi.mock('electron', async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + app: { + ...actual.app, + getPath: vi.fn().mockReturnValue('/tmp/backups'), + }, + ipcMain: { + ...actual.ipcMain, + on: vi.fn(), + handle: vi.fn(), + removeHandler: vi.fn(), + }, + BrowserWindow: { + ...actual.BrowserWindow, + getAllWindows: vi.fn(), + }, + }; +}); +vi.mock('./getDeviceIdentifier'); +vi.mock('./createNewDevice'); +vi.mock('../../../apps/main/windows', () => ({ + broadcastToWindows: vi.fn(), +})); +vi.mock('../../../apps/shared/dependency-injection/DependencyInjectionUserProvider', () => ({ + DependencyInjectionUserProvider: { get: vi.fn(), updateUser: vi.fn() }, +})); + +describe('createAndSetupNewDevice', () => { + const mockedGetDeviceIdentifier = vi.mocked(getDeviceIdentifier); + const mockedCreateNewDevice = vi.mocked(createNewDevice); + const mockedBroadcastToWindows = vi.mocked(broadcastToWindows); + const mockedBrowserWindowGetAllWindows = vi.mocked(BrowserWindow.getAllWindows); + const mockedUserProviderGet = vi.mocked(DependencyInjectionUserProvider.get); + const mockedUserProviderUpdate = vi.mocked(DependencyInjectionUserProvider.updateUser); + + beforeEach(() => { + vi.clearAllMocks(); + mockedUserProviderGet.mockReturnValue({ backupsBucket: '' } as never); + mockedBrowserWindowGetAllWindows.mockReturnValue([] as never); + }); + + it('should return only error when the device identifier is unavailable', async () => { + const error = new Error('Missing device identifier'); + mockedGetDeviceIdentifier.mockReturnValue({ error }); + + const result = await createAndSetupNewDevice(); + + expect(result).toStrictEqual({ error }); + expect(mockedCreateNewDevice).not.toHaveBeenCalled(); + expect(mockedBroadcastToWindows).not.toHaveBeenCalled(); + }); + + it('should return only error when the device creation fails', async () => { + const error = new Error('Create device failed'); + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedCreateNewDevice.mockResolvedValue({ error }); + + const result = await createAndSetupNewDevice(); + + expect(result).toStrictEqual({ error }); + expect(mockedBroadcastToWindows).not.toHaveBeenCalled(); + expect(mockedUserProviderUpdate).not.toHaveBeenCalled(); + }); + + it('should update the user and notify windows when the device is created', async () => { + const user = { backupsBucket: '' }; + const send = vi.fn(); + const device = { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedUserProviderGet.mockReturnValue(user as never); + mockedBrowserWindowGetAllWindows.mockReturnValue([{ webContents: { send } }] as never); + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedCreateNewDevice.mockResolvedValue({ data: device }); + + const result = await createAndSetupNewDevice(); + + expect(result).toStrictEqual({ data: device }); + expect(user.backupsBucket).toBe('bucket-1'); + expect(mockedUserProviderUpdate).toHaveBeenCalledWith(user); + expect(send).toHaveBeenCalledWith('reinitialize-backups'); + expect(mockedBroadcastToWindows).toHaveBeenCalledWith('device-created', device); + }); +}); diff --git a/src/backend/features/device/createAndSetupNewDevice.ts b/src/backend/features/device/createAndSetupNewDevice.ts index f8b451a27..0210b1109 100644 --- a/src/backend/features/device/createAndSetupNewDevice.ts +++ b/src/backend/features/device/createAndSetupNewDevice.ts @@ -9,17 +9,16 @@ export async function createAndSetupNewDevice() { const { error, data: deviceIdentifier } = getDeviceIdentifier(); if (error) return { error }; - const createNewDeviceEither = await createNewDevice(deviceIdentifier); - if (createNewDeviceEither.isLeft()) { + const { error: createDeviceError, data: device } = await createNewDevice(deviceIdentifier); + if (createDeviceError) { logger.error({ tag: 'BACKUPS', msg: '[DEVICE] Error creating new device', - error: createNewDeviceEither.getLeft(), + error: createDeviceError, }); - return { error: createNewDeviceEither.getLeft() }; + return { error: createDeviceError }; } - const device = createNewDeviceEither.getRight(); const user = DependencyInjectionUserProvider.get(); user.backupsBucket = device.bucket; DependencyInjectionUserProvider.updateUser(user); diff --git a/src/backend/features/device/createNewDevice.test.ts b/src/backend/features/device/createNewDevice.test.ts new file mode 100644 index 000000000..0ed690cf9 --- /dev/null +++ b/src/backend/features/device/createNewDevice.test.ts @@ -0,0 +1,42 @@ +import { createNewDevice } from './createNewDevice'; +import { createUniqueDevice } from './createUniqueDevice'; +import { saveDeviceToConfig } from './saveDeviceToConfig'; + +vi.mock('./createUniqueDevice'); +vi.mock('./saveDeviceToConfig'); + +describe('createNewDevice', () => { + const mockedCreateUniqueDevice = vi.mocked(createUniqueDevice); + const mockedSaveDeviceToConfig = vi.mocked(saveDeviceToConfig); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return only error when creating a unique device fails', async () => { + const error = new Error('Could not create device'); + mockedCreateUniqueDevice.mockResolvedValue({ error }); + + const result = await createNewDevice({ key: 'key', platform: 'linux', hostname: 'host' }); + + expect(result).toStrictEqual({ error }); + expect(mockedSaveDeviceToConfig).not.toHaveBeenCalled(); + }); + + it('should save the device to config when creating the device succeeds', async () => { + const device = { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedCreateUniqueDevice.mockResolvedValue({ data: device }); + + const result = await createNewDevice({ key: 'key', platform: 'linux', hostname: 'host' }); + + expect(result).toStrictEqual({ data: device }); + expect(mockedSaveDeviceToConfig).toHaveBeenCalledWith(device); + }); +}); diff --git a/src/backend/features/device/createNewDevice.ts b/src/backend/features/device/createNewDevice.ts index d094f5e98..c5459d4f6 100644 --- a/src/backend/features/device/createNewDevice.ts +++ b/src/backend/features/device/createNewDevice.ts @@ -1,15 +1,13 @@ -import { Either, right } from './../../../context/shared/domain/Either'; -import { Device } from '../backup/types/Device'; +import { Device } from '../../../apps/main/device/service'; +import { Result } from '../../../context/shared/domain/Result'; import { createUniqueDevice } from './createUniqueDevice'; import { saveDeviceToConfig } from './saveDeviceToConfig'; import { DeviceIdentifierDTO } from './device.types'; -export async function createNewDevice(deviceIdentifier: DeviceIdentifierDTO): Promise> { - const createUniqueDeviceEither = await createUniqueDevice(deviceIdentifier); - if (createUniqueDeviceEither.isRight()) { - const device = createUniqueDeviceEither.getRight(); - saveDeviceToConfig(device); - return right(device); - } - return createUniqueDeviceEither; +export async function createNewDevice(deviceIdentifier: DeviceIdentifierDTO): Promise> { + const { data: device, error } = await createUniqueDevice(deviceIdentifier); + if (error) return { error }; + + saveDeviceToConfig(device); + return { data: device }; } diff --git a/src/backend/features/device/createUniqueDevice.ts b/src/backend/features/device/createUniqueDevice.ts index 5cc630404..12c06e444 100644 --- a/src/backend/features/device/createUniqueDevice.ts +++ b/src/backend/features/device/createUniqueDevice.ts @@ -2,18 +2,19 @@ import { Device } from '../backup/types/Device'; import { hostname } from 'node:os'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import { tryCreateDevice } from './tryCreateDevice'; -import { Either, left, right } from '../../../context/shared/domain/Either'; import { addUnknownDeviceIssue } from './addUnknownDeviceIssue'; import { DeviceIdentifierDTO } from './device.types'; +import { Result } from '../../../context/shared/domain/Result'; +import { BackupError } from '../../../infra/drive-server/services/backup/backup.error'; /** * Creates a new device with a unique name - * @returns Either containing the created device or an error if device creation fails after multiple attempts + * @returns Result containing the created device or an error if device creation fails after multiple attempts * @param attempts The number of attempts to create a device with a unique name, defaults to 1000 */ export async function createUniqueDevice( deviceIdentifier: DeviceIdentifierDTO, attempts = 1000, -): Promise> { +): Promise> { const baseName = hostname(); const nameVariants = [baseName, ...Array.from({ length: attempts }, (_, i) => `${baseName} (${i + 1})`)]; @@ -22,21 +23,22 @@ export async function createUniqueDevice( tag: 'BACKUPS', msg: `Trying to create device with name "${name}"`, }); - const tryCreateDeviceEither = await tryCreateDevice(name, deviceIdentifier); + const { data, error } = await tryCreateDevice(name, deviceIdentifier); - if (tryCreateDeviceEither.isRight()) { - return right(tryCreateDeviceEither.getRight()); + if (data) { + return { data }; } - const error = tryCreateDeviceEither.getLeft(); - if (error.message == 'Error creating device') { - return left(tryCreateDeviceEither.getLeft()); + + if (!(error instanceof BackupError && error.code === 'ALREADY_EXISTS')) { + return { error }; } } + const finalError = logger.error({ tag: 'BACKUPS', msg: 'Could not create device trying different names', }); addUnknownDeviceIssue(finalError); - return left(finalError); + return { error: finalError }; } diff --git a/src/backend/features/device/getBackupsFromDevice.test.ts b/src/backend/features/device/getBackupsFromDevice.test.ts new file mode 100644 index 000000000..b34963ce3 --- /dev/null +++ b/src/backend/features/device/getBackupsFromDevice.test.ts @@ -0,0 +1,133 @@ +import { app } from 'electron'; +import configStore from '../../../apps/main/config'; +import { findBackupPathnameFromId } from '../../../apps/main/device/service'; +import { fetchFolder } from '../../../infra/drive-server/services/folder/services/fetch-folder'; +import { getBackupsFromDevice } from './getBackupsFromDevice'; + +vi.mock('electron', async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + app: { + ...actual.app, + getPath: vi.fn().mockReturnValue('/tmp/backups'), + }, + ipcMain: { + ...actual.ipcMain, + on: vi.fn(), + handle: vi.fn(), + removeHandler: vi.fn(), + }, + }; +}); +vi.mock('../../../infra/drive-server/services/folder/services/fetch-folder'); +vi.mock('../../../apps/main/config', () => ({ + default: { get: vi.fn() }, +})); +vi.mock('../../../apps/main/device/service', () => ({ + findBackupPathnameFromId: vi.fn(), +})); + +describe('getBackupsFromDevice', () => { + const mockedFetchFolder = vi.mocked(fetchFolder); + const mockedConfigStore = vi.mocked(configStore); + const mockedFindBackupPathnameFromId = vi.mocked(findBackupPathnameFromId); + const mockedAppGetPath = vi.mocked(app.getPath); + + beforeEach(() => { + vi.clearAllMocks(); + mockedAppGetPath.mockReturnValue('/tmp/backups'); + }); + + it('should return only error when fetching the folder fails', async () => { + const error = new Error('Folder fetch failed'); + mockedFetchFolder.mockResolvedValue({ error } as never); + + const result = await getBackupsFromDevice({ uuid: 'device-uuid', bucket: 'bucket-1' } as never, false); + + expect(result.error).toBe(error); + expect(result.data).toBeUndefined(); + }); + + it('should return only data when the backups are retrieved for a non-current device', async () => { + mockedConfigStore.get.mockReturnValue({}); + mockedFetchFolder.mockResolvedValue({ + data: { + children: [{ id: 1, uuid: 'folder-uuid', plainName: 'Documents' }], + }, + } as never); + + const result = await getBackupsFromDevice({ uuid: 'device-uuid', bucket: 'bucket-1' } as never, false); + + expect(result).toStrictEqual({ + data: [ + { + name: 'Documents', + pathname: '', + folderId: 1, + folderUuid: 'folder-uuid', + tmpPath: '', + backupsBucket: 'bucket-1', + }, + ], + }); + }); + + it('should return only enabled current backups with their mapped pathname', async () => { + mockedConfigStore.get.mockReturnValue({ + '/home/docs': { enabled: true, folderId: 1, folderUuid: 'folder-uuid-1' }, + '/home/photos': { enabled: false, folderId: 2, folderUuid: 'folder-uuid-2' }, + }); + mockedFindBackupPathnameFromId.mockImplementation((backupId: number) => { + if (backupId === 1) return '/home/docs'; + if (backupId === 2) return '/home/photos'; + return undefined; + }); + mockedFetchFolder.mockResolvedValue({ + data: { + children: [ + { id: 1, uuid: 'folder-uuid-1', plainName: 'Documents', bucket: 'bucket-docs' }, + { id: 2, uuid: 'folder-uuid-2', plainName: 'Photos', bucket: 'bucket-photos' }, + ], + }, + } as never); + + const result = await getBackupsFromDevice({ uuid: 'device-uuid', bucket: 'bucket-1' } as never, true); + + expect(result).toStrictEqual({ + data: [ + { + name: 'Documents', + pathname: '/home/docs', + folderId: 1, + folderUuid: 'folder-uuid-1', + tmpPath: '/tmp/backups', + backupsBucket: 'bucket-docs', + }, + ], + }); + }); + + it('should return an empty list when current backups are missing a pathname or are disabled', async () => { + mockedConfigStore.get.mockReturnValue({ + '/home/photos': { enabled: false, folderId: 2, folderUuid: 'folder-uuid-2' }, + }); + mockedFindBackupPathnameFromId.mockImplementation((backupId: number) => { + if (backupId === 2) return '/home/photos'; + return undefined; + }); + mockedFetchFolder.mockResolvedValue({ + data: { + children: [ + { id: 1, uuid: 'folder-uuid-1', plainName: 'Documents', bucket: 'bucket-docs' }, + { id: 2, uuid: 'folder-uuid-2', plainName: 'Photos', bucket: 'bucket-photos' }, + ], + }, + } as never); + + const result = await getBackupsFromDevice({ uuid: 'device-uuid', bucket: 'bucket-1' } as never, true); + + expect(result).toStrictEqual({ data: [] }); + }); +}); diff --git a/src/backend/features/device/getBackupsFromDevice.ts b/src/backend/features/device/getBackupsFromDevice.ts index 460a3c307..7db85a95b 100644 --- a/src/backend/features/device/getBackupsFromDevice.ts +++ b/src/backend/features/device/getBackupsFromDevice.ts @@ -2,16 +2,22 @@ import { FolderDtoWithPathname } from './device.types'; import { fetchFolder } from '../../../infra/drive-server/services/folder/services/fetch-folder'; import configStore from '../../../apps/main/config'; import { BackupInfo } from './../../../apps/backups/BackupInfo'; -import { Device } from '../backup/types/Device'; +import { Device } from './../../../apps/main/device/service'; +import { Result } from '../../../context/shared/domain/Result'; +import { AbsolutePath } from '../../../context/local/localFile/infrastructure/AbsolutePath'; import { FolderDto } from '../../../infra/drive-server/out/dto'; import { mapFolderDtoToBackupInfo } from './utils/mapFolderDtoToBackupInfo'; import { findBackupPathnameFromId } from '../backup/find-backup-pathname-from-id'; -export async function getBackupsFromDevice(device: Device, isCurrent?: boolean): Promise> { +export async function getBackupsFromDevice( + device: Device, + isCurrent?: boolean, +): Promise, Error>> { const { data: folder, error } = await fetchFolder(device.uuid); if (error) { - throw error; + return { error }; } + if (isCurrent) { const backupsList = configStore.get('backupList'); const result = folder.children @@ -23,16 +29,18 @@ export async function getBackupsFromDevice(device: Device, isCurrent?: boolean): return !!(backup.pathname && backupsList[backup.pathname]?.enabled); }) .map(mapFolderDtoToBackupInfo); - return result; - } else { - const result = folder.children.map((backup) => ({ - name: backup.plainName, - pathname: '', - folderId: backup.id, - folderUuid: backup.uuid, - tmpPath: '', - backupsBucket: device.bucket, - })); - return result; + + return { data: result }; } + + const result = folder.children.map((backup) => ({ + name: backup.plainName, + pathname: '' as AbsolutePath, + folderId: backup.id, + folderUuid: backup.uuid, + tmpPath: '', + backupsBucket: device.bucket, + })); + + return { data: result }; } diff --git a/src/backend/features/device/getOrCreateDevice.test.ts b/src/backend/features/device/getOrCreateDevice.test.ts index fc7747d68..30dc54412 100644 --- a/src/backend/features/device/getOrCreateDevice.test.ts +++ b/src/backend/features/device/getOrCreateDevice.test.ts @@ -1,8 +1,11 @@ -import { getOrCreateDevice } from './getOrCreateDevice'; -import { getDeviceIdentifier } from './getDeviceIdentifier'; +import configStore from '../../../apps/main/config'; +import { DependencyInjectionUserProvider } from '../../../apps/shared/dependency-injection/DependencyInjectionUserProvider'; import { addUnknownDeviceIssue } from './addUnknownDeviceIssue'; +import { createAndSetupNewDevice } from './createAndSetupNewDevice'; import { fetchDevice } from './fetchDevice'; -import configStore from '../../../apps/main/config'; +import { fetchDeviceLegacyAndMigrate } from './fetchDeviceLegacyAndMigrate'; +import { getDeviceIdentifier } from './getDeviceIdentifier'; +import { getOrCreateDevice } from './getOrCreateDevice'; vi.mock('./getDeviceIdentifier'); vi.mock('./addUnknownDeviceIssue'); @@ -20,10 +23,143 @@ describe('getOrCreateDevice', () => { const mockedGetDeviceIdentifier = vi.mocked(getDeviceIdentifier); const mockedAddUnknownDeviceIssue = vi.mocked(addUnknownDeviceIssue); const mockedFetchDevice = vi.mocked(fetchDevice); + const mockedFetchDeviceLegacyAndMigrate = vi.mocked(fetchDeviceLegacyAndMigrate); + const mockedCreateAndSetupNewDevice = vi.mocked(createAndSetupNewDevice); const mockedConfigStore = vi.mocked(configStore); + const mockedUserProviderGet = vi.mocked(DependencyInjectionUserProvider.get); + const mockedUserProviderUpdate = vi.mocked(DependencyInjectionUserProvider.updateUser); beforeEach(() => { vi.clearAllMocks(); + mockedUserProviderGet.mockReturnValue({ backupsBucket: '' } as never); + mockedConfigStore.get.mockImplementation((key: string) => { + if (key === 'deviceId') return -1; + if (key === 'deviceUUID') return ''; + return undefined; + }); + }); + + it('should return the identifier error when the device identifier is unavailable', async () => { + const error = new Error('Unsupported platform'); + mockedGetDeviceIdentifier.mockReturnValue({ error }); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ error }); + expect(mockedFetchDevice).not.toHaveBeenCalled(); + expect(mockedFetchDeviceLegacyAndMigrate).not.toHaveBeenCalled(); + }); + + it('should return the existing device and update the user bucket when no saved identifiers exist', async () => { + const user = { backupsBucket: '' }; + const device = { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedUserProviderGet.mockReturnValue(user as never); + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedFetchDevice.mockResolvedValue({ data: device } as never); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ data: device }); + expect(mockedFetchDevice).toHaveBeenCalledWith({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + expect(user.backupsBucket).toBe('bucket-1'); + expect(mockedUserProviderUpdate).toHaveBeenCalledWith(user); + }); + + it('should create a new device when the current identifier lookup does not find one', async () => { + const device = { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedFetchDevice.mockResolvedValue({ error: new Error('Not found') } as never); + mockedCreateAndSetupNewDevice.mockResolvedValue({ data: device }); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ data: device }); + expect(mockedCreateAndSetupNewDevice).toHaveBeenCalled(); + }); + + it('should report the setup error when creating a new device fails', async () => { + const setupError = new Error('Create device failed'); + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedFetchDevice.mockResolvedValue({ error: new Error('Not found') } as never); + mockedCreateAndSetupNewDevice.mockResolvedValue({ error: setupError }); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ error: setupError }); + expect(mockedAddUnknownDeviceIssue).toHaveBeenCalledWith(setupError); + }); + + it('should use the saved uuid when it exists in config', async () => { + const device = { + id: 1, + uuid: 'saved-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedConfigStore.get.mockImplementation((key: string) => { + if (key === 'deviceId') return -1; + if (key === 'deviceUUID') return 'saved-uuid'; + return undefined; + }); + mockedFetchDeviceLegacyAndMigrate.mockResolvedValue({ data: device } as never); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ data: device }); + expect(mockedFetchDeviceLegacyAndMigrate).toHaveBeenCalledWith({ uuid: 'saved-uuid' }); + expect(mockedFetchDevice).not.toHaveBeenCalled(); + }); + + it('should use the legacy id when there is no saved uuid', async () => { + const device = { + id: 1, + uuid: 'legacy-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedConfigStore.get.mockImplementation((key: string) => { + if (key === 'deviceId') return 42; + if (key === 'deviceUUID') return ''; + return undefined; + }); + mockedFetchDeviceLegacyAndMigrate.mockResolvedValue({ data: device } as never); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ data: device }); + expect(mockedFetchDeviceLegacyAndMigrate).toHaveBeenCalledWith({ legacyId: '42' }); }); describe('when an unexpected error is thrown', () => { @@ -55,11 +191,6 @@ describe('getOrCreateDevice', () => { mockedGetDeviceIdentifier.mockReturnValue({ data: { key: 'key', platform: 'linux', hostname: 'host' }, }); - mockedConfigStore.get.mockImplementation((key: string) => { - if (key === 'deviceId') return -1; - if (key === 'deviceUUID') return ''; - return undefined; - }); const fetchError = new Error('Network error'); mockedFetchDevice.mockRejectedValue(fetchError); diff --git a/src/backend/features/device/getOrCreateDevice.ts b/src/backend/features/device/getOrCreateDevice.ts index 170292fce..983abc6b9 100644 --- a/src/backend/features/device/getOrCreateDevice.ts +++ b/src/backend/features/device/getOrCreateDevice.ts @@ -1,13 +1,10 @@ -import { Device } from '../backup/types/Device'; -import configStore from '../../../apps/main/config'; +import { Device } from '../../../apps/main/device/service'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import { addUnknownDeviceIssue } from './addUnknownDeviceIssue'; -import { fetchDeviceLegacyAndMigrate } from './fetchDeviceLegacyAndMigrate'; -import { fetchDevice } from './fetchDevice'; import { createAndSetupNewDevice } from './createAndSetupNewDevice'; import { getDeviceIdentifier } from './getDeviceIdentifier'; -import { Result } from './../../../context/shared/domain/Result'; -import { DependencyInjectionUserProvider } from './../../../apps/shared/dependency-injection/DependencyInjectionUserProvider'; +import { Result } from '../../../context/shared/domain/Result'; +import { fetchSavedOrCurrentDevice, syncUserBackupsBucket } from './utils/getOrCreateDeviceHelpers'; async function handleFetchDeviceResult(deviceResult: Result) { if (deviceResult.error) { @@ -22,39 +19,16 @@ async function handleFetchDeviceResult(deviceResult: Result) { return { data }; } - const user = DependencyInjectionUserProvider.get(); - user.backupsBucket = deviceResult.data.bucket; - DependencyInjectionUserProvider.updateUser(user); - + syncUserBackupsBucket({ device: deviceResult.data }); return { data: deviceResult.data }; } -export async function getOrCreateDevice(): Promise> { +export async function getOrCreateDevice() { try { - const { error, data } = getDeviceIdentifier(); + const { error, data: deviceIdentifier } = getDeviceIdentifier(); if (error) return { error }; - const legacyId = configStore.get('deviceId'); - const savedUUID = configStore.get('deviceUUID'); - logger.debug({ - tag: 'BACKUPS', - msg: '[DEVICE] Checking saved device identifiers', - legacyId, - savedUUID, - }); - - const hasLegacyId = legacyId !== -1; - const hasUuid = savedUUID !== ''; - if (!hasLegacyId && !hasUuid) { - const result = await fetchDevice({ deviceIdentifier: data }); - return await handleFetchDeviceResult(result); - } - - /* eventually, this whole if section is going to be replaced - when all the users naturaly migrated to the new identification mechanism */ - const prop = hasUuid ? { uuid: savedUUID } : { legacyId: legacyId.toString() }; - - const deviceResult = await fetchDeviceLegacyAndMigrate(prop); + const deviceResult = await fetchSavedOrCurrentDevice({ deviceIdentifier }); return await handleFetchDeviceResult(deviceResult); } catch (error) { const unknownError = error instanceof Error ? error : new Error('Unexpected error in getOrCreateDevice'); diff --git a/src/backend/features/device/renameDevice.test.ts b/src/backend/features/device/renameDevice.test.ts new file mode 100644 index 000000000..c80e0cfff --- /dev/null +++ b/src/backend/features/device/renameDevice.test.ts @@ -0,0 +1,67 @@ +import { driveServerModule } from '../../../infra/drive-server/drive-server.module'; +import { getDeviceIdentifier } from './getDeviceIdentifier'; +import { renameDevice } from './renameDevice'; + +vi.mock('./getDeviceIdentifier'); +vi.mock('../../../infra/drive-server/drive-server.module', () => ({ + driveServerModule: { + backup: { + updateDeviceByIdentifier: vi.fn(), + }, + }, +})); + +describe('renameDevice', () => { + const mockedGetDeviceIdentifier = vi.mocked(getDeviceIdentifier); + const mockedUpdateDeviceByIdentifier = vi.mocked(driveServerModule.backup.updateDeviceByIdentifier); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return only error when the device identifier is unavailable', async () => { + const error = new Error('No device identifier'); + mockedGetDeviceIdentifier.mockReturnValue({ error }); + + const result = await renameDevice('new-name'); + + expect(result.error).toBe(error); + expect(result.data).toBeUndefined(); + expect(mockedUpdateDeviceByIdentifier).not.toHaveBeenCalled(); + }); + + it('should return only data when the rename succeeds', async () => { + const device = { uuid: 'uuid-1', name: 'new-name', bucket: 'bucket-1' }; + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'device-key', platform: 'linux', hostname: 'host' }, + }); + mockedUpdateDeviceByIdentifier.mockResolvedValue({ + isRight: () => true, + isLeft: () => false, + getRight: () => device, + getLeft: () => undefined, + } as never); + + const result = await renameDevice('new-name'); + + expect(result).toStrictEqual({ data: device }); + }); + + it('should return only error when the rename request fails', async () => { + const error = new Error('Rename failed'); + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'device-key', platform: 'linux', hostname: 'host' }, + }); + mockedUpdateDeviceByIdentifier.mockResolvedValue({ + isRight: () => false, + isLeft: () => true, + getRight: () => undefined, + getLeft: () => error, + } as never); + + const result = await renameDevice('new-name'); + + expect(result.error).toBe(error); + expect(result.data).toBeUndefined(); + }); +}); diff --git a/src/backend/features/device/renameDevice.ts b/src/backend/features/device/renameDevice.ts index e2e86f3b8..66af3d95c 100644 --- a/src/backend/features/device/renameDevice.ts +++ b/src/backend/features/device/renameDevice.ts @@ -1,17 +1,33 @@ -import { Device } from '../backup/types/Device'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import { Device } from '../../../apps/main/device/service'; +import { Result } from '../../../context/shared/domain/Result'; import { driveServerModule } from '../../../infra/drive-server/drive-server.module'; import { getDeviceIdentifier } from './getDeviceIdentifier'; -export async function renameDevice(deviceName: string): Promise { - const deviceIdentifier = getDeviceIdentifier(); - if (deviceIdentifier.error) { - throw new Error('Error in the request to rename a device'); - } +export async function renameDevice(deviceName: string): Promise> { + try { + const { error, data: deviceIdentifier } = getDeviceIdentifier(); + if (error) return { error }; + + const response = await driveServerModule.backup.updateDeviceByIdentifier(deviceIdentifier.key, deviceName); + if (response.isRight()) { + return { data: response.getRight() }; + } - const response = await driveServerModule.backup.updateDeviceByIdentifier(deviceIdentifier.data.key, deviceName); - if (response.isRight()) { - return response.getRight(); - } else { - throw new Error('Error in the request to rename a device'); + const requestError = response.getLeft(); + logger.error({ + tag: 'BACKUPS', + msg: 'Error in the request to rename a device', + error: requestError, + }); + return { error: requestError }; + } catch (error) { + const unexpectedError = error instanceof Error ? error : new Error('Unexpected error renaming device'); + logger.error({ + tag: 'BACKUPS', + msg: 'Unexpected error renaming device', + error: unexpectedError, + }); + return { error: unexpectedError }; } } diff --git a/src/backend/features/device/tryCreateDevice.ts b/src/backend/features/device/tryCreateDevice.ts index bf15078ce..d97e86f94 100644 --- a/src/backend/features/device/tryCreateDevice.ts +++ b/src/backend/features/device/tryCreateDevice.ts @@ -1,15 +1,14 @@ -import { Device } from '../backup/types/Device'; -import { left, right } from './../../../context/shared/domain/Either'; +import { Device } from './../../../apps/main/device/service'; +import { Result } from '../../../context/shared/domain/Result'; import { driveServerModule } from './../../../infra/drive-server/drive-server.module'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import { BackupError } from '../../../infra/drive-server/services/backup/backup.error'; -import { Either } from './../../../context/shared/domain/Either'; import { DeviceIdentifierDTO } from './device.types'; export async function tryCreateDevice( deviceName: string, deviceIdentifier: DeviceIdentifierDTO, -): Promise> { +): Promise> { const createDeviceEither = await driveServerModule.backup.createDeviceWithIdentifier({ name: deviceName, key: deviceIdentifier.key, @@ -17,21 +16,24 @@ export async function tryCreateDevice( platform: deviceIdentifier.platform, }); - if (createDeviceEither.isRight()) return right(createDeviceEither.getRight()); + if (createDeviceEither.isRight()) { + return { data: createDeviceEither.getRight() }; + } const createDeviceError = createDeviceEither.getLeft(); - if (createDeviceError instanceof BackupError && createDeviceError?.code === 'ALREADY_EXISTS') { + if (createDeviceError instanceof BackupError && createDeviceError.code === 'ALREADY_EXISTS') { logger.debug({ tag: 'BACKUPS', msg: 'Device name already exists', deviceName, }); - return left(createDeviceEither.getLeft()); + return { error: createDeviceError }; } - const error = logger.error({ + logger.error({ tag: 'BACKUPS', msg: 'Error creating device', + error: createDeviceError, }); - return left(error); + return { error: createDeviceError }; } diff --git a/src/backend/features/device/utils/getOrCreateDeviceHelpers.test.ts b/src/backend/features/device/utils/getOrCreateDeviceHelpers.test.ts new file mode 100644 index 000000000..90e93dd42 --- /dev/null +++ b/src/backend/features/device/utils/getOrCreateDeviceHelpers.test.ts @@ -0,0 +1,143 @@ +import configStore from '../../../../apps/main/config'; +import { Device } from '../../../../apps/main/device/service'; +import { DependencyInjectionUserProvider } from '../../../../apps/shared/dependency-injection/DependencyInjectionUserProvider'; +import { fetchDevice } from '../fetchDevice'; +import { fetchDeviceLegacyAndMigrate } from '../fetchDeviceLegacyAndMigrate'; +import { + fetchSavedOrCurrentDevice, + getSavedDeviceIdentifiers, + resolveFetchProps, + syncUserBackupsBucket, +} from './getOrCreateDeviceHelpers'; + +vi.mock('../fetchDevice'); +vi.mock('../fetchDeviceLegacyAndMigrate'); +vi.mock('../../../../apps/main/config', () => ({ + default: { get: vi.fn() }, +})); +vi.mock('../../../../apps/shared/dependency-injection/DependencyInjectionUserProvider', () => ({ + DependencyInjectionUserProvider: { get: vi.fn(), updateUser: vi.fn() }, +})); + +describe('getOrCreateDeviceHelpers', () => { + const mockedConfigStore = vi.mocked(configStore); + const mockedFetchDevice = vi.mocked(fetchDevice); + const mockedFetchDeviceLegacyAndMigrate = vi.mocked(fetchDeviceLegacyAndMigrate); + const mockedUserProviderGet = vi.mocked(DependencyInjectionUserProvider.get); + const mockedUserProviderUpdate = vi.mocked(DependencyInjectionUserProvider.updateUser); + + beforeEach(() => { + vi.clearAllMocks(); + mockedConfigStore.get.mockImplementation((key: string) => { + if (key === 'deviceId') return -1; + if (key === 'deviceUUID') return ''; + return undefined; + }); + }); + + it('should read the saved device identifiers from config', () => { + const result = getSavedDeviceIdentifiers(); + + expect(result).toStrictEqual({ + legacyId: -1, + savedUUID: '', + hasLegacyId: false, + hasUuid: false, + }); + }); + + it('should resolve current identifier props when there are no saved identifiers', () => { + const result = resolveFetchProps({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + savedDeviceIdentifiers: { + legacyId: -1, + savedUUID: '', + hasLegacyId: false, + hasUuid: false, + }, + }); + + expect(result).toStrictEqual({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + }); + + it('should resolve saved uuid props when they exist', () => { + const result = resolveFetchProps({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + savedDeviceIdentifiers: { + legacyId: -1, + savedUUID: 'saved-uuid', + hasLegacyId: false, + hasUuid: true, + }, + }); + + expect(result).toStrictEqual({ uuid: 'saved-uuid' }); + }); + + it('should fetch the current device when no saved identifiers exist', async () => { + const device = { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + } satisfies Device; + mockedFetchDevice.mockResolvedValue({ data: device }); + + const result = await fetchSavedOrCurrentDevice({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + + expect(result).toStrictEqual({ data: device }); + expect(mockedFetchDevice).toHaveBeenCalledWith({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + expect(mockedFetchDeviceLegacyAndMigrate).not.toHaveBeenCalled(); + }); + + it('should fetch the saved device when a uuid is stored', async () => { + const device = { + id: 1, + uuid: 'saved-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + } satisfies Device; + mockedConfigStore.get.mockImplementation((key: string) => { + if (key === 'deviceId') return -1; + if (key === 'deviceUUID') return 'saved-uuid'; + return undefined; + }); + mockedFetchDeviceLegacyAndMigrate.mockResolvedValue({ data: device }); + + const result = await fetchSavedOrCurrentDevice({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + + expect(result).toStrictEqual({ data: device }); + expect(mockedFetchDeviceLegacyAndMigrate).toHaveBeenCalledWith({ uuid: 'saved-uuid' }); + }); + + it('should sync the user backup bucket from the device', () => { + const user = { backupsBucket: '' }; + mockedUserProviderGet.mockReturnValue(user as never); + + syncUserBackupsBucket({ + device: { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }, + }); + + expect(user.backupsBucket).toBe('bucket-1'); + expect(mockedUserProviderUpdate).toHaveBeenCalledWith(user); + }); +}); diff --git a/src/backend/features/device/utils/getOrCreateDeviceHelpers.ts b/src/backend/features/device/utils/getOrCreateDeviceHelpers.ts new file mode 100644 index 000000000..acae7d811 --- /dev/null +++ b/src/backend/features/device/utils/getOrCreateDeviceHelpers.ts @@ -0,0 +1,73 @@ +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import configStore from '../../../../apps/main/config'; +import { Device } from '../../../../apps/main/device/service'; +import { DependencyInjectionUserProvider } from '../../../../apps/shared/dependency-injection/DependencyInjectionUserProvider'; +import { Result } from '../../../../context/shared/domain/Result'; +import { DeviceIdentifierDTO } from '../device.types'; +import { fetchDevice } from '../fetchDevice'; +import { fetchDeviceLegacyAndMigrate } from '../fetchDeviceLegacyAndMigrate'; + +export type SavedDeviceIdentifiers = { + legacyId: number; + savedUUID: string; + hasLegacyId: boolean; + hasUuid: boolean; +}; + +export function syncUserBackupsBucket({ device }: { device: Device }) { + const user = DependencyInjectionUserProvider.get(); + user.backupsBucket = device.bucket; + DependencyInjectionUserProvider.updateUser(user); +} + +export function getSavedDeviceIdentifiers() { + const legacyId = configStore.get('deviceId'); + const savedUUID = configStore.get('deviceUUID'); + + logger.debug({ + tag: 'BACKUPS', + msg: '[DEVICE] Checking saved device identifiers', + legacyId, + savedUUID, + }); + + return { + legacyId, + savedUUID, + hasLegacyId: legacyId !== -1, + hasUuid: savedUUID !== '', + } satisfies SavedDeviceIdentifiers; +} + +export function resolveFetchProps({ + deviceIdentifier, + savedDeviceIdentifiers, +}: { + deviceIdentifier: DeviceIdentifierDTO; + savedDeviceIdentifiers: SavedDeviceIdentifiers; +}) { + if (!savedDeviceIdentifiers.hasLegacyId && !savedDeviceIdentifiers.hasUuid) { + return { deviceIdentifier }; + } + + /* eventually, this whole if section is going to be replaced + when all the users naturaly migrated to the new identification mechanism */ + return savedDeviceIdentifiers.hasUuid + ? { uuid: savedDeviceIdentifiers.savedUUID } + : { legacyId: savedDeviceIdentifiers.legacyId.toString() }; +} + +export async function fetchSavedOrCurrentDevice({ + deviceIdentifier, +}: { + deviceIdentifier: DeviceIdentifierDTO; +}): Promise> { + const savedDeviceIdentifiers = getSavedDeviceIdentifiers(); + const props = resolveFetchProps({ deviceIdentifier, savedDeviceIdentifiers }); + + if ('deviceIdentifier' in props) { + return await fetchDevice(props); + } + + return await fetchDeviceLegacyAndMigrate(props); +} From fe236f3802e9c783be911095154d2a898b99d680 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Mon, 27 Apr 2026 10:48:34 -0500 Subject: [PATCH 09/18] refactor: update backup handling to use structured error responses and improve type imports Co-authored-by: Copilot --- .../features/backup/change-backup-path.ts | 6 ++++-- .../backup/delete-device-backups.test.ts | 8 ++++---- .../features/backup/delete-device-backups.ts | 7 ++++++- .../features/backup/download-backup.test.ts | 3 +++ src/backend/features/device/createNewDevice.ts | 2 +- .../features/device/getBackupsFromDevice.test.ts | 16 ++++++++-------- .../features/device/getBackupsFromDevice.ts | 11 +++++------ src/backend/features/device/getOrCreateDevice.ts | 2 +- src/backend/features/device/renameDevice.ts | 2 +- src/backend/features/device/tryCreateDevice.ts | 2 +- 10 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/backend/features/backup/change-backup-path.ts b/src/backend/features/backup/change-backup-path.ts index 9a2879c58..dc9d67065 100644 --- a/src/backend/features/backup/change-backup-path.ts +++ b/src/backend/features/backup/change-backup-path.ts @@ -42,11 +42,13 @@ export async function changeBackupPath({ currentPath, newPath }: Props): Promise delete backupsList[currentPath]; - const migratedExistingBackup = await migrateBackupEntryIfNeeded({ + const {error, data} = await migrateBackupEntryIfNeeded({ pathname: newPath, backup: existingBackup, }); - backupsList[newPath] = migratedExistingBackup; + if (error) return { error }; + + backupsList[newPath] = data; configStore.set('backupList', backupsList); diff --git a/src/backend/features/backup/delete-device-backups.test.ts b/src/backend/features/backup/delete-device-backups.test.ts index f036bceeb..3b828700f 100644 --- a/src/backend/features/backup/delete-device-backups.test.ts +++ b/src/backend/features/backup/delete-device-backups.test.ts @@ -36,8 +36,8 @@ describe('delete-device-backups', () => { }, ]; - getBackupsFromDeviceMock.mockResolvedValue(backups); - deleteBackupMock.mockResolvedValue(undefined); + getBackupsFromDeviceMock.mockResolvedValue({ data: backups }); + deleteBackupMock.mockResolvedValue({ data: undefined }); getBackupFolderTreeSnapshotMock.mockResolvedValue({ data: { tree: { @@ -68,8 +68,8 @@ describe('delete-device-backups', () => { }, ]; - getBackupsFromDeviceMock.mockResolvedValue(backups); - deleteBackupMock.mockResolvedValue(undefined); + getBackupsFromDeviceMock.mockResolvedValue({ data: backups }); + deleteBackupMock.mockResolvedValue({ data: undefined }); getBackupFolderTreeSnapshotMock.mockResolvedValue({ data: { tree: { children: [{ id: 10, uuid: 'folder-uuid-1' }] } }, } as never); diff --git a/src/backend/features/backup/delete-device-backups.ts b/src/backend/features/backup/delete-device-backups.ts index a821caf83..3d2031a1f 100644 --- a/src/backend/features/backup/delete-device-backups.ts +++ b/src/backend/features/backup/delete-device-backups.ts @@ -11,7 +11,12 @@ type Props = { }; export async function deleteDeviceBackups({ device, isCurrent }: Props) { - const backups = await DeviceModule.getBackupsFromDevice(device, isCurrent); + const { error: getBackupsError, data: backups } = await DeviceModule.getBackupsFromDevice(device, isCurrent); + if (getBackupsError) { + logger.error({ tag: 'BACKUPS', msg: 'Error fetching backups from device', error: getBackupsError }); + return; + } + logger.debug({ tag: 'BACKUPS', msg: '[BACKUPS] Deleting backups from device', count: backups.length }); logger.debug({ tag: 'BACKUPS', msg: '[BACKUPS] Backups details', backups }); diff --git a/src/backend/features/backup/download-backup.test.ts b/src/backend/features/backup/download-backup.test.ts index 0c4f5d6ef..f4c914223 100644 --- a/src/backend/features/backup/download-backup.test.ts +++ b/src/backend/features/backup/download-backup.test.ts @@ -53,6 +53,7 @@ describe('download-backup', () => { it('should download backup and broadcast progress when not aborted', async () => { downloadDeviceBackupZipMock.mockImplementation(async ({ updateProgress }) => { updateProgress(33); + return { data: true }; }); await downloadBackup({ device, pathname }); @@ -86,6 +87,7 @@ describe('download-backup', () => { const abortListener = ipcMainOnMock.mock.calls[0]?.[1]; abortListener?.({} as never, device.uuid); updateProgress(90); + return { data: true }; }); await downloadBackup({ device, pathname }); @@ -98,6 +100,7 @@ describe('download-backup', () => { const abortListener = ipcMainOnMock.mock.calls[0]?.[1]; abortListener?.({} as never, 'other-device-uuid'); updateProgress(12); + return { data: true }; }); await downloadBackup({ device, pathname }); diff --git a/src/backend/features/device/createNewDevice.ts b/src/backend/features/device/createNewDevice.ts index c5459d4f6..c4c7e8b5b 100644 --- a/src/backend/features/device/createNewDevice.ts +++ b/src/backend/features/device/createNewDevice.ts @@ -1,4 +1,4 @@ -import { Device } from '../../../apps/main/device/service'; +import { Device } from '../backup/types/Device'; import { Result } from '../../../context/shared/domain/Result'; import { createUniqueDevice } from './createUniqueDevice'; import { saveDeviceToConfig } from './saveDeviceToConfig'; diff --git a/src/backend/features/device/getBackupsFromDevice.test.ts b/src/backend/features/device/getBackupsFromDevice.test.ts index b34963ce3..e83906285 100644 --- a/src/backend/features/device/getBackupsFromDevice.test.ts +++ b/src/backend/features/device/getBackupsFromDevice.test.ts @@ -1,6 +1,6 @@ import { app } from 'electron'; import configStore from '../../../apps/main/config'; -import { findBackupPathnameFromId } from '../../../apps/main/device/service'; +import { findBackupPathnameFromId } from '../backup/find-backup-pathname-from-id'; import { fetchFolder } from '../../../infra/drive-server/services/folder/services/fetch-folder'; import { getBackupsFromDevice } from './getBackupsFromDevice'; @@ -25,7 +25,7 @@ vi.mock('../../../infra/drive-server/services/folder/services/fetch-folder'); vi.mock('../../../apps/main/config', () => ({ default: { get: vi.fn() }, })); -vi.mock('../../../apps/main/device/service', () => ({ +vi.mock('../backup/find-backup-pathname-from-id', () => ({ findBackupPathnameFromId: vi.fn(), })); @@ -64,7 +64,7 @@ describe('getBackupsFromDevice', () => { data: [ { name: 'Documents', - pathname: '', + pathname: '.', folderId: 1, folderUuid: 'folder-uuid', tmpPath: '', @@ -79,9 +79,9 @@ describe('getBackupsFromDevice', () => { '/home/docs': { enabled: true, folderId: 1, folderUuid: 'folder-uuid-1' }, '/home/photos': { enabled: false, folderId: 2, folderUuid: 'folder-uuid-2' }, }); - mockedFindBackupPathnameFromId.mockImplementation((backupId: number) => { - if (backupId === 1) return '/home/docs'; - if (backupId === 2) return '/home/photos'; + mockedFindBackupPathnameFromId.mockImplementation(({ id }: { id: number }) => { + if (id === 1) return '/home/docs'; + if (id === 2) return '/home/photos'; return undefined; }); mockedFetchFolder.mockResolvedValue({ @@ -113,8 +113,8 @@ describe('getBackupsFromDevice', () => { mockedConfigStore.get.mockReturnValue({ '/home/photos': { enabled: false, folderId: 2, folderUuid: 'folder-uuid-2' }, }); - mockedFindBackupPathnameFromId.mockImplementation((backupId: number) => { - if (backupId === 2) return '/home/photos'; + mockedFindBackupPathnameFromId.mockImplementation(({ id }: { id: number }) => { + if (id === 2) return '/home/photos'; return undefined; }); mockedFetchFolder.mockResolvedValue({ diff --git a/src/backend/features/device/getBackupsFromDevice.ts b/src/backend/features/device/getBackupsFromDevice.ts index 7db85a95b..782486a24 100644 --- a/src/backend/features/device/getBackupsFromDevice.ts +++ b/src/backend/features/device/getBackupsFromDevice.ts @@ -2,21 +2,20 @@ import { FolderDtoWithPathname } from './device.types'; import { fetchFolder } from '../../../infra/drive-server/services/folder/services/fetch-folder'; import configStore from '../../../apps/main/config'; import { BackupInfo } from './../../../apps/backups/BackupInfo'; -import { Device } from './../../../apps/main/device/service'; +import { Device } from '../backup/types/Device'; import { Result } from '../../../context/shared/domain/Result'; -import { AbsolutePath } from '../../../context/local/localFile/infrastructure/AbsolutePath'; +import { createAbsolutePath } from '../../../context/local/localFile/infrastructure/AbsolutePath'; import { FolderDto } from '../../../infra/drive-server/out/dto'; import { mapFolderDtoToBackupInfo } from './utils/mapFolderDtoToBackupInfo'; import { findBackupPathnameFromId } from '../backup/find-backup-pathname-from-id'; +import { create } from 'lodash'; export async function getBackupsFromDevice( device: Device, isCurrent?: boolean, ): Promise, Error>> { const { data: folder, error } = await fetchFolder(device.uuid); - if (error) { - return { error }; - } + if (error) return { error }; if (isCurrent) { const backupsList = configStore.get('backupList'); @@ -35,7 +34,7 @@ export async function getBackupsFromDevice( const result = folder.children.map((backup) => ({ name: backup.plainName, - pathname: '' as AbsolutePath, + pathname: createAbsolutePath(''), folderId: backup.id, folderUuid: backup.uuid, tmpPath: '', diff --git a/src/backend/features/device/getOrCreateDevice.ts b/src/backend/features/device/getOrCreateDevice.ts index 983abc6b9..70bbfec58 100644 --- a/src/backend/features/device/getOrCreateDevice.ts +++ b/src/backend/features/device/getOrCreateDevice.ts @@ -1,4 +1,4 @@ -import { Device } from '../../../apps/main/device/service'; +import { Device } from '../backup/types/Device'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import { addUnknownDeviceIssue } from './addUnknownDeviceIssue'; import { createAndSetupNewDevice } from './createAndSetupNewDevice'; diff --git a/src/backend/features/device/renameDevice.ts b/src/backend/features/device/renameDevice.ts index 66af3d95c..6fabcb19d 100644 --- a/src/backend/features/device/renameDevice.ts +++ b/src/backend/features/device/renameDevice.ts @@ -1,5 +1,5 @@ import { logger } from '@internxt/drive-desktop-core/build/backend'; -import { Device } from '../../../apps/main/device/service'; +import { Device } from '../backup/types/Device'; import { Result } from '../../../context/shared/domain/Result'; import { driveServerModule } from '../../../infra/drive-server/drive-server.module'; import { getDeviceIdentifier } from './getDeviceIdentifier'; diff --git a/src/backend/features/device/tryCreateDevice.ts b/src/backend/features/device/tryCreateDevice.ts index d97e86f94..7c283781c 100644 --- a/src/backend/features/device/tryCreateDevice.ts +++ b/src/backend/features/device/tryCreateDevice.ts @@ -1,4 +1,4 @@ -import { Device } from './../../../apps/main/device/service'; +import { Device } from '../backup/types/Device'; import { Result } from '../../../context/shared/domain/Result'; import { driveServerModule } from './../../../infra/drive-server/drive-server.module'; import { logger } from '@internxt/drive-desktop-core/build/backend'; From e8db0f6d32a74ac6e8227c1b30d0f4fd60aa2873 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Mon, 27 Apr 2026 10:52:47 -0500 Subject: [PATCH 10/18] refactor: improve formatting of mockResolvedValue calls in backup tests --- src/backend/features/backup/change-backup-path.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/features/backup/change-backup-path.ts b/src/backend/features/backup/change-backup-path.ts index dc9d67065..f2db4e93a 100644 --- a/src/backend/features/backup/change-backup-path.ts +++ b/src/backend/features/backup/change-backup-path.ts @@ -42,7 +42,7 @@ export async function changeBackupPath({ currentPath, newPath }: Props): Promise delete backupsList[currentPath]; - const {error, data} = await migrateBackupEntryIfNeeded({ + const { error, data } = await migrateBackupEntryIfNeeded({ pathname: newPath, backup: existingBackup, }); From 9d79310b4516d45ccfea762ae2e955a9373f7a5d Mon Sep 17 00:00:00 2001 From: Alexis Mora Date: Tue, 28 Apr 2026 09:17:50 +0200 Subject: [PATCH 11/18] feat: Implement Open and OpenDir FUSE opeartions (#319) * 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 --- packages/fuse-daemon/internal/filesystem/setup_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/fuse-daemon/internal/filesystem/setup_test.go b/packages/fuse-daemon/internal/filesystem/setup_test.go index 19356064b..681e19ac5 100644 --- a/packages/fuse-daemon/internal/filesystem/setup_test.go +++ b/packages/fuse-daemon/internal/filesystem/setup_test.go @@ -59,6 +59,16 @@ func (serverMock *mockServer) setHandlers(handlers map[client.OperationPath]http serverMock.server.Handler = router } +// setHandlers replaces the current request handler with one that responds to +// multiple paths. Use this when a single test triggers more than one operation. +func (serverMock *mockServer) setHandlers(handlers map[client.OperationPath]http.HandlerFunc) { + router := http.NewServeMux() + for path, handler := range handlers { + router.HandleFunc(string(path), handler) + } + serverMock.server.Handler = router +} + func (serverMock *mockServer) close() { _ = serverMock.server.Close() _ = serverMock.socket.Close() From 3a38f4806df3d2b8a0f6031a8301a4b657f47440 Mon Sep 17 00:00:00 2001 From: Alexis Mora Date: Tue, 28 Apr 2026 19:12:58 +0200 Subject: [PATCH 12/18] read operation + blocklist processes (#321) --- .../utils/process-blocklist.test.ts | 32 +++++++++++++++++++ .../virtual-drive/utils/process-blocklist.ts | 23 +++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/backend/features/virtual-drive/utils/process-blocklist.test.ts create mode 100644 src/backend/features/virtual-drive/utils/process-blocklist.ts diff --git a/src/backend/features/virtual-drive/utils/process-blocklist.test.ts b/src/backend/features/virtual-drive/utils/process-blocklist.test.ts new file mode 100644 index 000000000..6876d961e --- /dev/null +++ b/src/backend/features/virtual-drive/utils/process-blocklist.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { isBlocklistedProcess } from './process-blocklist'; + +describe('isBlocklistedProcess', () => { + it('should block pool-org.gnome (Nautilus thumbnail generation)', () => { + expect(isBlocklistedProcess('pool-org.gnome')).toBe(true); + }); + + it('should block pool-org.gnome. with trailing dot (kernel 16-char truncation variant)', () => { + expect(isBlocklistedProcess('pool-org.gnome.')).toBe(true); + }); + + it('should not block pool-gnome-text (GNOME Text Editor user open)', () => { + expect(isBlocklistedProcess('pool-gnome-text')).toBe(false); + }); + + it('should not block vlc (user-initiated open)', () => { + expect(isBlocklistedProcess('vlc')).toBe(false); + }); + + it('should not block evince (user-initiated open)', () => { + expect(isBlocklistedProcess('evince')).toBe(false); + }); + + it('should not block empty string (unknown process defaults to allow)', () => { + expect(isBlocklistedProcess('')).toBe(false); + }); + + it('should not block nautilus (file manager process itself is not the thumbnail daemon)', () => { + expect(isBlocklistedProcess('nautilus')).toBe(false); + }); +}); diff --git a/src/backend/features/virtual-drive/utils/process-blocklist.ts b/src/backend/features/virtual-drive/utils/process-blocklist.ts new file mode 100644 index 000000000..ae4d68e6c --- /dev/null +++ b/src/backend/features/virtual-drive/utils/process-blocklist.ts @@ -0,0 +1,23 @@ +/** + * Processes known to trigger file reads for system purposes (thumbnail generation, + * directory browsing) rather than user-initiated file opens. + * + * Matched with startsWith to handle kernel's 16-char /proc//comm truncation + * and version-suffixed variants. + * + * To expand compatibility for a new file manager, add its thumbnail daemon here. + * + * WARNING: Never block the broad `pool-` prefix — GNOME user apps (e.g. Text Editor + * as `pool-gnome-text`, VLC as `vlc`) use different pool names and must be allowed through. + * Only add specific known thumbnail/system daemon prefixes. + */ +const BLOCKLISTED_PROCESS_PREFIXES = [ + 'pool-org.gnome', // GNOME thread pool — Nautilus thumbnail generation + // 'tumblerd', // Thunar, Caja, PCManFM thumbnail daemon (freedesktop spec) + // 'kio_thumbnail', // Dolphin KIO thumbnail worker + // 'thumbnail.so', // Dolphin KIO thumbnail worker (alternative name) +]; + +export function isBlocklistedProcess(processName: string): boolean { + return BLOCKLISTED_PROCESS_PREFIXES.some((prefix) => processName.startsWith(prefix)); +} From fa1a7c2d5478f4f897719c801b2ef6bb60e2a071 Mon Sep 17 00:00:00 2001 From: Alexis Mora Date: Wed, 29 Apr 2026 15:48:02 +0200 Subject: [PATCH 13/18] feat: release operation + read tests (#323) --- .../routes/operations.routes.test.ts | 16 ++++++++++++++++ .../virtual-drive/utils/process-blocklist.ts | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/backend/features/virtual-drive/routes/operations.routes.test.ts b/src/backend/features/virtual-drive/routes/operations.routes.test.ts index d0b3ad345..0e0c0c7b0 100644 --- a/src/backend/features/virtual-drive/routes/operations.routes.test.ts +++ b/src/backend/features/virtual-drive/routes/operations.routes.test.ts @@ -34,4 +34,20 @@ describe('buildOperationsRouter', () => { it('should register POST /release', () => { expect(routes).toContain(OPERATION_PATHS.RELEASE); }); + + it('should register POST /open', () => { + expect(routes).toContain(OPERATION_PATHS.OPEN); + }); + + it('should register POST /opendir', () => { + expect(routes).toContain(OPERATION_PATHS.OPEN_DIR); + }); + + it('should register POST /read', () => { + expect(routes).toContain(OPERATION_PATHS.READ); + }); + + it('should register POST /release', () => { + expect(routes).toContain(OPERATION_PATHS.RELEASE); + }); }); diff --git a/src/backend/features/virtual-drive/utils/process-blocklist.ts b/src/backend/features/virtual-drive/utils/process-blocklist.ts index ae4d68e6c..f76e8d230 100644 --- a/src/backend/features/virtual-drive/utils/process-blocklist.ts +++ b/src/backend/features/virtual-drive/utils/process-blocklist.ts @@ -13,6 +13,8 @@ */ const BLOCKLISTED_PROCESS_PREFIXES = [ 'pool-org.gnome', // GNOME thread pool — Nautilus thumbnail generation + 'gdk-pixbuf-thum', // GDK pixbuf thumbnailer (truncated at 15 chars by kernel) + 'EogJobScheduler', // Eye of GNOME (image viewer) background job scheduler // 'tumblerd', // Thunar, Caja, PCManFM thumbnail daemon (freedesktop spec) // 'kio_thumbnail', // Dolphin KIO thumbnail worker // 'thumbnail.so', // Dolphin KIO thumbnail worker (alternative name) From 8ddf0687acf5aef55822403439a9bd09fc020d05 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Wed, 29 Apr 2026 10:56:43 -0500 Subject: [PATCH 14/18] feat: add unlink and rmdir operations with corresponding controllers, services, and tests (#320) --- packages/fuse-daemon/internal/filesystem/setup_test.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/fuse-daemon/internal/filesystem/setup_test.go b/packages/fuse-daemon/internal/filesystem/setup_test.go index 681e19ac5..453d25c43 100644 --- a/packages/fuse-daemon/internal/filesystem/setup_test.go +++ b/packages/fuse-daemon/internal/filesystem/setup_test.go @@ -59,15 +59,6 @@ func (serverMock *mockServer) setHandlers(handlers map[client.OperationPath]http serverMock.server.Handler = router } -// setHandlers replaces the current request handler with one that responds to -// multiple paths. Use this when a single test triggers more than one operation. -func (serverMock *mockServer) setHandlers(handlers map[client.OperationPath]http.HandlerFunc) { - router := http.NewServeMux() - for path, handler := range handlers { - router.HandleFunc(string(path), handler) - } - serverMock.server.Handler = router -} func (serverMock *mockServer) close() { _ = serverMock.server.Close() From b21d44b81869d6d6d9d13384a03c8c57523e1461 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Thu, 30 Apr 2026 16:54:41 -0500 Subject: [PATCH 15/18] =?UTF-8?q?feat:=20implement=20create=20and=20write?= =?UTF-8?q?=20operations=20with=20corresponding=20contro=E2=80=A6=20(#322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/features/virtual-drive/utils/process-blocklist.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/features/virtual-drive/utils/process-blocklist.ts b/src/backend/features/virtual-drive/utils/process-blocklist.ts index f76e8d230..2e77d5878 100644 --- a/src/backend/features/virtual-drive/utils/process-blocklist.ts +++ b/src/backend/features/virtual-drive/utils/process-blocklist.ts @@ -14,7 +14,7 @@ const BLOCKLISTED_PROCESS_PREFIXES = [ 'pool-org.gnome', // GNOME thread pool — Nautilus thumbnail generation 'gdk-pixbuf-thum', // GDK pixbuf thumbnailer (truncated at 15 chars by kernel) - 'EogJobScheduler', // Eye of GNOME (image viewer) background job scheduler + //'EogJobScheduler', // Eye of GNOME (image viewer) background job scheduler // 'tumblerd', // Thunar, Caja, PCManFM thumbnail daemon (freedesktop spec) // 'kio_thumbnail', // Dolphin KIO thumbnail worker // 'thumbnail.so', // Dolphin KIO thumbnail worker (alternative name) From 5a3f46a9f6b6454921f129a1d63d57cecd6cee21 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Tue, 5 May 2026 17:12:17 -0500 Subject: [PATCH 16/18] fix: update mocked return value for migrateBackupEntryIfNeeded in change-backup-path tests; remove unused lodash import in getBackupsFromDevice --- src/backend/features/backup/change-backup-path.test.ts | 2 +- src/backend/features/device/getBackupsFromDevice.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backend/features/backup/change-backup-path.test.ts b/src/backend/features/backup/change-backup-path.test.ts index d0cccc8e9..625a4bc50 100644 --- a/src/backend/features/backup/change-backup-path.test.ts +++ b/src/backend/features/backup/change-backup-path.test.ts @@ -67,7 +67,7 @@ describe('change-backup-path', () => { mockedConfigStoreGet.mockReturnValue(backupList); mockedGetBackupFolderUuid.mockResolvedValue({ data: 'remote-folder-uuid' }); mockedRenameFolder.mockResolvedValue({ data: {} }); - mockedMigrateBackupEntryIfNeeded.mockResolvedValue(migratedBackup); + mockedMigrateBackupEntryIfNeeded.mockResolvedValue({ data: migratedBackup }); const result = await changeBackupPath({ currentPath, newPath }); diff --git a/src/backend/features/device/getBackupsFromDevice.ts b/src/backend/features/device/getBackupsFromDevice.ts index 782486a24..69a2aaa66 100644 --- a/src/backend/features/device/getBackupsFromDevice.ts +++ b/src/backend/features/device/getBackupsFromDevice.ts @@ -8,7 +8,6 @@ import { createAbsolutePath } from '../../../context/local/localFile/infrastruct import { FolderDto } from '../../../infra/drive-server/out/dto'; import { mapFolderDtoToBackupInfo } from './utils/mapFolderDtoToBackupInfo'; import { findBackupPathnameFromId } from '../backup/find-backup-pathname-from-id'; -import { create } from 'lodash'; export async function getBackupsFromDevice( device: Device, From 75d91dbbd52c8d17ffd6298da2246174a989a704 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Fri, 5 Jun 2026 15:49:54 -0500 Subject: [PATCH 17/18] refactor: remove duplicate imports and clean up test files; ensure newline at end of JSON files --- README.md | 1 - .../internal/filesystem/operations.go | 2 - .../internal/filesystem/setup_test.go | 1 - .../callbacks/TrashFolderCallback.test.ts | 103 ------------------ src/apps/renderer/localize/locales/en.json | 2 +- src/apps/renderer/localize/locales/es.json | 2 +- src/apps/renderer/localize/locales/fr.json | 2 +- .../routes/operations.routes.test.ts | 16 --- .../utils/process-blocklist.test.ts | 32 ------ .../virtual-drive/utils/process-blocklist.ts | 25 ----- .../application/create/FileCreator.test.ts | 1 - .../application/create/FolderCreator.test.ts | 2 - 12 files changed, 3 insertions(+), 186 deletions(-) delete mode 100644 src/apps/drive/fuse/callbacks/TrashFolderCallback.test.ts delete mode 100644 src/backend/features/virtual-drive/utils/process-blocklist.test.ts delete mode 100644 src/backend/features/virtual-drive/utils/process-blocklist.ts diff --git a/README.md b/README.md index 9ec329767..2855d5831 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,6 @@ For the best experience with SSO authentication, we recommend using the .deb pac If working on the FUSE daemon (Go), see [packages/fuse-daemon/README.md](packages/fuse-daemon/README.md) for Go and linting tool prerequisites. -If working on the FUSE daemon (Go), see [packages/fuse-daemon/README.md](packages/fuse-daemon/README.md) for Go and linting tool prerequisites. ### Install diff --git a/packages/fuse-daemon/internal/filesystem/operations.go b/packages/fuse-daemon/internal/filesystem/operations.go index 2b0f84e31..3fcc6e1a2 100644 --- a/packages/fuse-daemon/internal/filesystem/operations.go +++ b/packages/fuse-daemon/internal/filesystem/operations.go @@ -8,8 +8,6 @@ import ( "internxt/drive-desktop-linux/fuse-daemon/internal/client" - "internxt/drive-desktop-linux/fuse-daemon/internal/client" - "github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse/nodefs" "github.com/hanwen/go-fuse/v2/fuse/pathfs" diff --git a/packages/fuse-daemon/internal/filesystem/setup_test.go b/packages/fuse-daemon/internal/filesystem/setup_test.go index 453d25c43..19356064b 100644 --- a/packages/fuse-daemon/internal/filesystem/setup_test.go +++ b/packages/fuse-daemon/internal/filesystem/setup_test.go @@ -59,7 +59,6 @@ func (serverMock *mockServer) setHandlers(handlers map[client.OperationPath]http serverMock.server.Handler = router } - func (serverMock *mockServer) close() { _ = serverMock.server.Close() _ = serverMock.socket.Close() diff --git a/src/apps/drive/fuse/callbacks/TrashFolderCallback.test.ts b/src/apps/drive/fuse/callbacks/TrashFolderCallback.test.ts deleted file mode 100644 index ab6486952..000000000 --- a/src/apps/drive/fuse/callbacks/TrashFolderCallback.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { FolderDeleter } from '../../../../context/virtual-drive/folders/application/FolderDeleter'; -import { SingleFolderMatchingFinder } from '../../../../context/virtual-drive/folders/application/SingleFolderMatchingFinder'; -import { FolderMother } from '../../../../context/virtual-drive/folders/domain/__test-helpers__/FolderMother'; -import { FolderStatuses } from '../../../../context/virtual-drive/folders/domain/FolderStatus'; -import { SyncFolderMessenger } from '../../../../context/virtual-drive/folders/domain/SyncFolderMessenger'; -import { ContainerMock } from '../../__mocks__/ContainerMock'; -import { TrashFolderCallback } from './TrashFolderCallback'; - -describe('TrashFolderCallback', () => { - it('returns success even when folder deletion exceeds callback timeout', async () => { - vi.useFakeTimers(); - - try { - const container = new ContainerMock(); - const folder = FolderMother.any(); - - const folderFinder = { - run: vi.fn(async () => { - return folder; - }), - } as unknown as SingleFolderMatchingFinder; - - const folderDeleter = { - run: vi.fn(() => { - return new Promise((resolve) => { - setTimeout(resolve, 5_000); - }); - }), - } as unknown as FolderDeleter; - - container.set(SingleFolderMatchingFinder, folderFinder); - container.set(FolderDeleter, folderDeleter); - - const callback = new TrashFolderCallback(container as never); - const resultPromise = callback.execute('/Files/SlowFolder'); - - await vi.advanceTimersByTimeAsync(1_600); - - const result = await resultPromise; - - expect(result.isRight()).toBe(true); - expect(folderFinder.run).toHaveBeenCalledWith({ - path: '/Files/SlowFolder', - status: FolderStatuses.EXISTS, - }); - expect(folderDeleter.run).toHaveBeenCalledWith(folder.uuid); - } finally { - vi.useRealTimers(); - } - }); - - it('reports issue when background deletion fails after timeout', async () => { - vi.useFakeTimers(); - - try { - const container = new ContainerMock(); - const folder = FolderMother.any(); - - const folderFinder = { - run: vi.fn(async () => { - return folder; - }), - } as unknown as SingleFolderMatchingFinder; - - const folderDeleter = { - run: vi.fn(() => { - return new Promise((_resolve, reject) => { - setTimeout(() => { - reject(new Error('slow-delete-failed')); - }, 5_000); - }); - }), - } as unknown as FolderDeleter; - - const syncFolderMessenger = { - issue: vi.fn(async () => undefined), - } as unknown as SyncFolderMessenger; - - container.set(SingleFolderMatchingFinder, folderFinder); - container.set(FolderDeleter, folderDeleter); - container.set(SyncFolderMessenger, syncFolderMessenger); - - const callback = new TrashFolderCallback(container as never); - const resultPromise = callback.execute('/Files/SlowFolder'); - - await vi.advanceTimersByTimeAsync(1_600); - - const result = await resultPromise; - expect(result.isRight()).toBe(true); - - await vi.advanceTimersByTimeAsync(3_500); - await Promise.resolve(); - - expect(syncFolderMessenger.issue).toHaveBeenCalledWith({ - error: 'FOLDER_TRASH_ERROR', - cause: 'UNKNOWN', - name: 'SlowFolder', - }); - } finally { - vi.useRealTimers(); - } - }); -}); diff --git a/src/apps/renderer/localize/locales/en.json b/src/apps/renderer/localize/locales/en.json index b5ecf3e4b..ec57078c4 100644 --- a/src/apps/renderer/localize/locales/en.json +++ b/src/apps/renderer/localize/locales/en.json @@ -412,4 +412,4 @@ "title": "Network connection lost", "message": "Your network connection has been lost. Please check your internet connection and try again." } -} \ No newline at end of file +} diff --git a/src/apps/renderer/localize/locales/es.json b/src/apps/renderer/localize/locales/es.json index c20502c85..f6cb17abf 100644 --- a/src/apps/renderer/localize/locales/es.json +++ b/src/apps/renderer/localize/locales/es.json @@ -412,4 +412,4 @@ "title": "Conexión de red perdida", "message": "Se ha perdido la conexión de red. Por favor, verifica tu conexión a internet e inténtalo de nuevo." } -} \ No newline at end of file +} diff --git a/src/apps/renderer/localize/locales/fr.json b/src/apps/renderer/localize/locales/fr.json index 6a6e75c5f..5c26d5711 100644 --- a/src/apps/renderer/localize/locales/fr.json +++ b/src/apps/renderer/localize/locales/fr.json @@ -412,4 +412,4 @@ "title": "Connexion réseau perdue", "message": "Votre connexion réseau a été perdue. Veuillez vérifier votre connexion Internet et réessayer." } -} \ No newline at end of file +} diff --git a/src/backend/features/virtual-drive/routes/operations.routes.test.ts b/src/backend/features/virtual-drive/routes/operations.routes.test.ts index 0e0c0c7b0..d0b3ad345 100644 --- a/src/backend/features/virtual-drive/routes/operations.routes.test.ts +++ b/src/backend/features/virtual-drive/routes/operations.routes.test.ts @@ -34,20 +34,4 @@ describe('buildOperationsRouter', () => { it('should register POST /release', () => { expect(routes).toContain(OPERATION_PATHS.RELEASE); }); - - it('should register POST /open', () => { - expect(routes).toContain(OPERATION_PATHS.OPEN); - }); - - it('should register POST /opendir', () => { - expect(routes).toContain(OPERATION_PATHS.OPEN_DIR); - }); - - it('should register POST /read', () => { - expect(routes).toContain(OPERATION_PATHS.READ); - }); - - it('should register POST /release', () => { - expect(routes).toContain(OPERATION_PATHS.RELEASE); - }); }); diff --git a/src/backend/features/virtual-drive/utils/process-blocklist.test.ts b/src/backend/features/virtual-drive/utils/process-blocklist.test.ts deleted file mode 100644 index 6876d961e..000000000 --- a/src/backend/features/virtual-drive/utils/process-blocklist.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { isBlocklistedProcess } from './process-blocklist'; - -describe('isBlocklistedProcess', () => { - it('should block pool-org.gnome (Nautilus thumbnail generation)', () => { - expect(isBlocklistedProcess('pool-org.gnome')).toBe(true); - }); - - it('should block pool-org.gnome. with trailing dot (kernel 16-char truncation variant)', () => { - expect(isBlocklistedProcess('pool-org.gnome.')).toBe(true); - }); - - it('should not block pool-gnome-text (GNOME Text Editor user open)', () => { - expect(isBlocklistedProcess('pool-gnome-text')).toBe(false); - }); - - it('should not block vlc (user-initiated open)', () => { - expect(isBlocklistedProcess('vlc')).toBe(false); - }); - - it('should not block evince (user-initiated open)', () => { - expect(isBlocklistedProcess('evince')).toBe(false); - }); - - it('should not block empty string (unknown process defaults to allow)', () => { - expect(isBlocklistedProcess('')).toBe(false); - }); - - it('should not block nautilus (file manager process itself is not the thumbnail daemon)', () => { - expect(isBlocklistedProcess('nautilus')).toBe(false); - }); -}); diff --git a/src/backend/features/virtual-drive/utils/process-blocklist.ts b/src/backend/features/virtual-drive/utils/process-blocklist.ts deleted file mode 100644 index 2e77d5878..000000000 --- a/src/backend/features/virtual-drive/utils/process-blocklist.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Processes known to trigger file reads for system purposes (thumbnail generation, - * directory browsing) rather than user-initiated file opens. - * - * Matched with startsWith to handle kernel's 16-char /proc//comm truncation - * and version-suffixed variants. - * - * To expand compatibility for a new file manager, add its thumbnail daemon here. - * - * WARNING: Never block the broad `pool-` prefix — GNOME user apps (e.g. Text Editor - * as `pool-gnome-text`, VLC as `vlc`) use different pool names and must be allowed through. - * Only add specific known thumbnail/system daemon prefixes. - */ -const BLOCKLISTED_PROCESS_PREFIXES = [ - 'pool-org.gnome', // GNOME thread pool — Nautilus thumbnail generation - 'gdk-pixbuf-thum', // GDK pixbuf thumbnailer (truncated at 15 chars by kernel) - //'EogJobScheduler', // Eye of GNOME (image viewer) background job scheduler - // 'tumblerd', // Thunar, Caja, PCManFM thumbnail daemon (freedesktop spec) - // 'kio_thumbnail', // Dolphin KIO thumbnail worker - // 'thumbnail.so', // Dolphin KIO thumbnail worker (alternative name) -]; - -export function isBlocklistedProcess(processName: string): boolean { - return BLOCKLISTED_PROCESS_PREFIXES.some((prefix) => processName.startsWith(prefix)); -} diff --git a/src/context/virtual-drive/files/application/create/FileCreator.test.ts b/src/context/virtual-drive/files/application/create/FileCreator.test.ts index a4f977005..52c894630 100644 --- a/src/context/virtual-drive/files/application/create/FileCreator.test.ts +++ b/src/context/virtual-drive/files/application/create/FileCreator.test.ts @@ -27,7 +27,6 @@ describe('File Creator', () => { const parentFolderFinder = FolderFinderFactory.existingFolder(); eventBus = new EventBusMock(); notifier = new FileSyncNotifierMock(); - clearPendingCreations(); SUT = new FileCreator(remoteFileSystemMock, fileRepository, parentFolderFinder, eventBus, notifier); }); diff --git a/src/context/virtual-drive/folders/application/create/FolderCreator.test.ts b/src/context/virtual-drive/folders/application/create/FolderCreator.test.ts index 8fe622f76..bc26be20e 100644 --- a/src/context/virtual-drive/folders/application/create/FolderCreator.test.ts +++ b/src/context/virtual-drive/folders/application/create/FolderCreator.test.ts @@ -10,7 +10,6 @@ import { FolderRemoteFileSystemMock } from '../../__mocks__/FolderRemoteFileSyst import { FolderRepositoryMock } from '../../__mocks__/FolderRepositoryMock'; import { FolderPathMother } from '../../domain/__test-helpers__/FolderPathMother'; import { FolderMother } from '../../domain/__test-helpers__/FolderMother'; -import { clearPendingCreations } from './PendingFolderCreationTracker'; describe('Folder Creator', () => { let repository: FolderRepositoryMock; @@ -23,7 +22,6 @@ describe('Folder Creator', () => { repository = new FolderRepositoryMock(); remote = new FolderRemoteFileSystemMock(); eventBus = new EventBusMock(); - clearPendingCreations(); const parentFolderFinder = new ParentFolderFinder(repository); From c298b946f724bce1f04f9a34df78bc91ed3aa721 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Fri, 5 Jun 2026 15:52:02 -0500 Subject: [PATCH 18/18] refactor: remove unused minimatch dependency from package-lock.json; clean up README formatting --- README.md | 1 - package-lock.json | 17 +---------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/README.md b/README.md index 2855d5831..f4d8d8aec 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,6 @@ For the best experience with SSO authentication, we recommend using the .deb pac If working on the FUSE daemon (Go), see [packages/fuse-daemon/README.md](packages/fuse-daemon/README.md) for Go and linting tool prerequisites. - ### Install Clone the repo and install dependencies: diff --git a/package-lock.json b/package-lock.json index 420a910ec..245b0106e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22211,21 +22211,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/typeorm/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/typeorm/node_modules/uuid": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", @@ -23933,4 +23918,4 @@ } } } -} \ No newline at end of file +}