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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module.exports = {
'@typescript-eslint/no-explicit-any': 'warn', // TODO: Change back to 'error' after fixing existing violations
'@typescript-eslint/no-unused-vars': 'warn', // TODO: Change back to 'error' after fixing existing violations
'@typescript-eslint/no-unsafe-declaration-merging': 'warn', // TODO: Change back to 'error' after fixing existing violations
'object-shorthand': ['warn', 'always'],
},
parser: '@typescript-eslint/parser',
settings: {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir .",
"reinstall:nautilus-extension": "NODE_ENV=development ts-node src/apps/nautilus-extension/reload.ts",
"lint": "cross-env NODE_ENV=development eslint . --ext .ts,.tsx --max-warnings=215",
"lint": "cross-env NODE_ENV=development eslint . --ext .ts,.tsx --max-warnings=235",
"lint:fix": "npm run lint --fix",
"format": "prettier src --check",
"format:fix": "prettier src --write",
Expand All @@ -37,7 +37,7 @@
"prepare": "husky install",
"generate_schema": "openapi-typescript http://localhost:3005/api-json -o ./src/infra/schemas.d.ts",
"generate_schema:prod": "openapi-typescript https://gateway.internxt.com/drive/api-json -o ./src/infra/schemas.d.ts",
"find-deadcode": "knip --max-issues=284"
"find-deadcode": "knip --max-issues=206"
},
"build": {
"productName": "Internxt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ export class BackupConfiguration {
}

async obtainBackupsInfo(): Promise<Array<BackupInfo>> {
const device = await DeviceModule.getOrCreateDevice();
if (device instanceof Error) return [];
const { error, data } = await DeviceModule.getOrCreateDevice();
if (error) return [];

const enabledBackupEntries = await DeviceModule.getBackupsFromDevice(device, true);
const enabledBackupEntries = await DeviceModule.getBackupsFromDevice(data, true);

return this.map(enabledBackupEntries, device.bucket);
return this.map(enabledBackupEntries, data.bucket);
}

hasDiscoveredBackups(): boolean {
Expand Down
61 changes: 25 additions & 36 deletions src/apps/main/backups/add-backup.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import * as getPathFromDialogModule from '../device/service';
import * as createBackupModule from './create-backup';
import * as DeviceModuleModule from './../../../backend/features/device/device.module';
import * as enableExistingBackupModule from './enable-existing-backup';
import * as fetchDeviceModule from '../../../backend/features/device/fetchDevice';
import configStoreModule from '../config';
import { addBackup } from './add-backup';
import { getPathFromDialog } from '../device/service';
import configStore from '../config';
import { createBackup } from './create-backup';
import { DeviceModule } from './../../../backend/features/device/device.module';
import { logger } from '@internxt/drive-desktop-core/build/backend';
import { enableExistingBackup } from './enable-existing-backup';

vi.mock('../device/service');
vi.mock('../config');
vi.mock('./create-backup');
vi.mock('./../../../backend/features/device/device.module');
vi.mock('./enable-existing-backup');
vi.mock('../../../backend/features/device/fetchDevice', () => ({
fetchDevice: vi.fn(),
}));

const mockedGetPathFromDialog = vi.mocked(getPathFromDialog);
const mockedConfigStore = vi.mocked(configStore);
const mockedCreateBackup = vi.mocked(createBackup);
const mockedDeviceModule = vi.mocked(DeviceModule);
const mockedLogger = vi.mocked(logger);
const mockedEnableExistingBackup = vi.mocked(enableExistingBackup);
import { loggerMock } from 'tests/vitest/mocks.helper';
import { call, partialSpyOn } from 'tests/vitest/utils.helper';

const mockedGetPathFromDialog = partialSpyOn(getPathFromDialogModule, 'getPathFromDialog');
const mockedConfigStoreGet = partialSpyOn(configStoreModule, 'get');
const mockedCreateBackup = partialSpyOn(createBackupModule, 'createBackup');
const mockedGetOrCreateDevice = partialSpyOn(DeviceModuleModule.DeviceModule, 'getOrCreateDevice');
const mockedEnableExistingBackup = partialSpyOn(enableExistingBackupModule, 'enableExistingBackup');
const mockedFetchDevice = partialSpyOn(fetchDeviceModule, 'fetchDevice');

describe('addBackup', () => {
const mockDevice = {
Expand All @@ -33,25 +26,21 @@ describe('addBackup', () => {
};

beforeEach(() => {
vi.clearAllMocks();
mockedFetchDevice.mockResolvedValue({ error: undefined, data: mockDevice });
});

it('should throw error when device is not found', async () => {
const mockError = new Error('Device not found');
mockedDeviceModule.getOrCreateDevice.mockResolvedValue(mockError);
mockedLogger.error.mockImplementation(() => {
throw new Error('Error message');
});
mockedGetOrCreateDevice.mockResolvedValue({ error: mockError, data: undefined });

await expect(addBackup()).rejects.toThrow('Error message');
expect(mockedLogger.error).toBeCalledWith({
tag: 'BACKUPS',
call(loggerMock.error).toMatchObject({
msg: 'Error adding backup: No device found',
});
});

it('should return undefined when no path is chosen', async () => {
mockedDeviceModule.getOrCreateDevice.mockResolvedValue(mockDevice);
mockedGetOrCreateDevice.mockResolvedValue({ error: undefined, data: mockDevice });
mockedGetPathFromDialog.mockResolvedValue(null);

const result = await addBackup();
Expand All @@ -70,14 +59,14 @@ describe('addBackup', () => {
backupsBucket: 'test-bucket',
};

mockedDeviceModule.getOrCreateDevice.mockResolvedValue(mockDevice);
mockedGetOrCreateDevice.mockResolvedValue({ error: undefined, data: mockDevice });
mockedGetPathFromDialog.mockResolvedValue({ path: chosenPath, itemName: 'backup' });
mockedConfigStore.get.mockReturnValue({});
mockedConfigStoreGet.mockReturnValue({});
mockedCreateBackup.mockResolvedValue(mockBackupInfo);

const result = await addBackup();

expect(mockedCreateBackup).toBeCalledWith({
call(mockedCreateBackup).toMatchObject({
pathname: chosenPath,
device: mockDevice,
});
Expand All @@ -100,14 +89,14 @@ describe('addBackup', () => {
backupsBucket: 'test-bucket',
};

mockedDeviceModule.getOrCreateDevice.mockResolvedValue(mockDevice);
mockedGetOrCreateDevice.mockResolvedValue({ error: undefined, data: mockDevice });
mockedGetPathFromDialog.mockResolvedValue({ path: chosenPath, itemName: 'existing' });
mockedConfigStore.get.mockReturnValue({ [chosenPath]: existingBackupData });
mockedConfigStoreGet.mockReturnValue({ [chosenPath]: existingBackupData });
mockedEnableExistingBackup.mockResolvedValue(mockBackupInfo);

const result = await addBackup();

expect(mockedEnableExistingBackup).toBeCalledWith(chosenPath, mockDevice);
call(mockedEnableExistingBackup).toMatchObject([chosenPath, mockDevice]);
expect(result).toStrictEqual(mockBackupInfo);
});
});
8 changes: 4 additions & 4 deletions src/apps/main/backups/add-backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { logger } from '@internxt/drive-desktop-core/build/backend';
import { enableExistingBackup } from './enable-existing-backup';

export async function addBackup() {
const device = await DeviceModule.getOrCreateDevice();
if (device instanceof Error) {
const { error, data } = await DeviceModule.getOrCreateDevice();
if (error) {
throw logger.error({ tag: 'BACKUPS', msg: 'Error adding backup: No device found' });
}

Expand All @@ -19,8 +19,8 @@ export async function addBackup() {
const existingBackup = backupList[chosenPath];

if (!existingBackup) {
return await createBackup({ pathname: chosenPath, device });
return await createBackup({ pathname: chosenPath, device: data });
} else {
return await enableExistingBackup(chosenPath, device);
return await enableExistingBackup(chosenPath, data);
}
}
8 changes: 4 additions & 4 deletions src/apps/main/device/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,11 @@ export function findBackupPathnameFromId(id: number): string | undefined {
export async function createBackupsFromLocalPaths(folderPaths: string[]) {
configStore.set('backupsEnabled', true);

const result = await DeviceModule.getOrCreateDevice();
if (result instanceof Error) {
throw result;
const { error, data } = await DeviceModule.getOrCreateDevice();
if (error) {
throw error;
}
const operations = folderPaths.map((folderPath) => createBackup({ pathname: folderPath, device: result }));
const operations = folderPaths.map((folderPath) => createBackup({ pathname: folderPath, device: data }));

await Promise.all(operations);
}
Expand Down
2 changes: 1 addition & 1 deletion src/apps/main/interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface IElectronAPI {

setBackupsInterval(value: number): Promise<void>;

getOrCreateDevice: () => Promise<Device | Error>;
getOrCreateDevice: () => Promise<Result<Device, Error>>;

getBackupsFromDevice: (device: Device, isCurrent?: boolean) => Promise<Array<BackupInfo>>;

Expand Down
13 changes: 6 additions & 7 deletions src/apps/renderer/context/DeviceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,13 @@ export function DeviceProvider({ children }: { children: ReactNode }) {

const refreshDevice = () => {
setDeviceState({ status: 'LOADING' });
window.electron
.getOrCreateDevice()
.then((device) => {
setCurrentDevice(device);
})
.catch(() => {
window.electron.getOrCreateDevice().then(({ error, data: device }) => {
if (error) {
setDeviceState({ status: 'ERROR' });
});
return;
}
setCurrentDevice(device);
});
};

const setCurrentDevice = (newDevice: Device) => {
Expand Down
46 changes: 24 additions & 22 deletions src/backend/features/device/createAndSetupNewDevice.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
import { DependencyInjectionUserProvider } from './../../../apps/shared/dependency-injection/DependencyInjectionUserProvider';
import { Device } from './../../../apps/main/device/service';
import { createNewDevice } from './createNewDevice';
import { BrowserWindow } from 'electron';
import { broadcastToWindows } from '../../../apps/main/windows';
import { logger } from '@internxt/drive-desktop-core/build/backend';
import { getDeviceIdentifier } from './getDeviceIdentifier';

export async function createAndSetupNewDevice(): Promise<Device | Error> {
const getDeviceIdentifierResult = getDeviceIdentifier();
if (getDeviceIdentifierResult.isLeft()) {
return getDeviceIdentifierResult.getLeft();
}
const deviceIdentifier = getDeviceIdentifierResult.getRight();
export async function createAndSetupNewDevice() {
const { error, data: deviceIdentifier } = getDeviceIdentifier();
if (error) return { error };

const createNewDeviceEither = await createNewDevice(deviceIdentifier);
if (createNewDeviceEither.isRight()) {
const device = createNewDeviceEither.getRight();
const user = DependencyInjectionUserProvider.get();
user.backupsBucket = device.bucket;

const mainWindow = BrowserWindow.getAllWindows()[0];
if (mainWindow) {
mainWindow.webContents.send('reinitialize-backups');
}
broadcastToWindows('device-created', device);
logger.debug({
if (createNewDeviceEither.isLeft()) {
logger.error({
tag: 'BACKUPS',
msg: '[DEVICE] Created new device',
deviceUUID: device.uuid,
msg: '[DEVICE] Error creating new device',
error: createNewDeviceEither.getLeft(),
});
return device;
return { error: createNewDeviceEither.getLeft() };
}

const device = createNewDeviceEither.getRight();
const user = DependencyInjectionUserProvider.get();
user.backupsBucket = device.bucket;

Choose a reason for hiding this comment

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

Technically backupsBucket is always defined so we shouldn't do this anymore. We had the same code on windows and we deleted it. Check if that's true and you can simplify this.

Copy link
Author

Choose a reason for hiding this comment

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

I ran some tests and confirmed that the backupsBucket does not exist for new users until they create their first device, so this line is still necessary.

Choose a reason for hiding this comment

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

Oh shit, then we have a bug on windows. Thanks for the info :)


const mainWindow = BrowserWindow.getAllWindows()[0];
if (mainWindow) {
mainWindow.webContents.send('reinitialize-backups');
}
return createNewDeviceEither.getLeft();
broadcastToWindows('device-created', device);
logger.debug({
tag: 'BACKUPS',
msg: '[DEVICE] Created new device',
deviceUUID: device.uuid,
});
return { data: device };
}
61 changes: 17 additions & 44 deletions src/backend/features/device/fetchDevice.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,59 @@
import { driveServerModule } from './../../../infra/drive-server/drive-server.module';
import { Either, left, right } from '../../../context/shared/domain/Either';
import { Device } from '../../../apps/main/device/service';
import { logger } from '@internxt/drive-desktop-core/build/backend';
import { BackupError } from '../../../infra/drive-server/services/backup/backup.error';
import { addUnknownDeviceIssue } from './addUnknownDeviceIssue';
import { DeviceIdentifierDTO } from './device.types';

export type FetchDeviceProps = { deviceIdentifier: DeviceIdentifierDTO } | { uuid: string } | { legacyId: string };

async function getDeviceByProps(props: FetchDeviceProps): Promise<Either<Error, Device | null>> {
async function getDeviceByProps(props: FetchDeviceProps) {

Choose a reason for hiding this comment

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

[Non blocking] Just to give some information. We found that maybe this is no longer necessary since this is just a backup to recover to the device identifier of the user in case the app is uninstalled. However, the backup paths are lost anyway, so this doesn't provide a solution.

if ('deviceIdentifier' in props) {
const query = {
key: props.deviceIdentifier.key,
platform: props.deviceIdentifier.platform,
hostname: props.deviceIdentifier.hostname,
limit: 1,
offset: 0,
};
const result = await driveServerModule.backup.getDevicesByIdentifier(query);

if (result.isLeft()) return left(result.getLeft());
const { error, data } = await driveServerModule.backup.getDevicesByIdentifier({ query });
if (error) return { error };

const devices = result.getRight();
if (devices.length === 0) return right(null);
if (devices.length > 1) return left(new Error('Multiple devices found for the same identifier'));
if (data.length === 0) {
return { error: new BackupError('Device not found with given identifier', 'NOT_FOUND') };
}

return right(devices[0]);
return { data: data[0] };
} else {
const deviceResult =
'uuid' in props
? await driveServerModule.backup.getDevice(props.uuid)
: await driveServerModule.backup.getDeviceById(props.legacyId);

if (deviceResult.isLeft()) return left(deviceResult.getLeft());
if (deviceResult.isLeft()) return { error: deviceResult.getLeft() };

return right(deviceResult.getRight());
return { data: deviceResult.getRight() };
}
}

/**
* Checks if a device exists using the provided identifier.
* @param props - Union type object containing either:
* - { uuid: string } for UUID-based lookup
* - { legacyId: string } for legacy ID-based lookup
* - { deviceIdentifier: DeviceIdentifierDTO } for lookup by device identifier (key, platform, hostname)
*
* The function will automatically select the correct lookup method based on the provided property.
*
* @returns Either<Error, Device | null> - Right(Device) if found, Right(null) if not found, Left(Error) if error
*/
export async function fetchDevice(props: FetchDeviceProps): Promise<Either<Error, Device | null>> {
const getDeviceEither = await getDeviceByProps(props);

if (getDeviceEither.isRight()) {
const device = getDeviceEither.getRight();
if (device && !device.removed) {
logger.debug({
tag: 'BACKUPS',
msg: '[DEVICE] Found device',
device: device.name,
});
return right(device);
}
}

if (getDeviceEither.isLeft()) {
const error = getDeviceEither.getLeft();
export async function fetchDevice(props: FetchDeviceProps) {
const { error, data } = await getDeviceByProps(props);

if (error) {
if (error instanceof BackupError && error.code === 'NOT_FOUND') {
const msg = 'Device not found';
logger.debug({ tag: 'BACKUPS', msg: `[DEVICE] ${msg}` });
addUnknownDeviceIssue(new Error(msg));
return right(null);
}

if (error instanceof BackupError && error.code === 'FORBIDDEN') {
const msg = 'Device request returned forbidden';
logger.debug({ tag: 'BACKUPS', msg: `[DEVICE] ${msg}` });
addUnknownDeviceIssue(new Error(msg));
return right(null);
}

logger.error({ tag: 'BACKUPS', msg: '[DEVICE] Error fetching device', error: error.name });
return left(error);
return { error };
}

return right(null);
return { data };
}
15 changes: 0 additions & 15 deletions src/backend/features/device/fetchDeviceByIdentifier.ts

This file was deleted.

Loading
Loading