Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

init backups #506

Open
wants to merge 9 commits into
base: feat/2-2-0-release
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .erb/configs/webpack.paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const srcPath = path.join(rootPath, 'src', 'apps');
const srcMainPath = path.join(srcPath, 'main');
const srcRendererPath = path.join(srcPath, 'renderer');
const srcSyncPath = path.join(srcPath, 'workers', 'sync');
const srcBackupsPath = path.join(srcPath, 'workers', 'backups');
const srcBackupsPath = path.join(srcPath, 'backups');
const srcSyncEnginePath = path.join(srcPath, 'sync-engine');

const releasePath = path.join(rootPath, 'release');
Expand Down
13 changes: 13 additions & 0 deletions .hintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": [
"development"
],
"hints": {
"axe/aria": [
"default",
{
"aria-required-children": "off"
}
]
}
}
22 changes: 20 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,23 @@
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
},
"autoBarrel.language.defaultLanguage": "TypeScript",
"autoBarrel.files.disableRecursiveBarrelling": false,
"autoBarrel.files.includeExtensionOnExport": [
"ts",
"tsx",
"vue"
],
"autoBarrel.files.ignoreFilePathPatternOnExport": [
"**/*.spec.*",
"**/*.test.*"
],
"autoBarrel.files.keepExtensionOnExport": false,
"autoBarrel.files.detectExportsInFiles": false,
"autoBarrel.files.exportDefaultFilename": "filename",
"autoBarrel.formatting.excludeSemiColonAtEndOfLine": false,
"autoBarrel.formatting.useSingleQuotes": true,
"autoBarrel.formatting.endOfLine": "lf",
"autoBarrel.formatting.insertFinalNewline": true,
}
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@
"url": "https://github.com/internxt/drive-desktop"
},
"scripts": {
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:sync-engine\"",
"build": "concurrently \"npm run build:main\" \"npm run build:backups\" \"npm run build:renderer\" \"npm run build:sync-engine\"",
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
"build:sync-engine": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.sync-engine.ts",
"build:backups": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.backups.ts",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
"lint": "cross-env NODE_ENV=development eslint . --ext .ts,.tsx",
"lint:fix": "yarn run lint --fix",
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
"publish": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish always",
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts && opencollective-postinstall",
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:sync-engine && npm run start:renderer",
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:sync-engine && npm run start:renderer && npm run start:backups",
"start:main": "cross-env NODE_ENV=development electron -r ts-node/register/transpile-only ./src/apps/main/main.ts",
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
"start:sync": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.sync.ts",
"start:sync-engine": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.sync-engine.ts",
"start:backups": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.backups.ts",
"test": "jest && playwright test --config=src/test",
"test:unit": "jest --silent",
"test:e2e": "playwright test --config=src/test",
Expand Down Expand Up @@ -263,7 +265,7 @@
"@iconscout/react-unicons": "^1.1.6",
"@internxt/inxt-js": "^2.0.8",
"@internxt/lib": "^1.1.6",
"@internxt/sdk": "^1.4.68",
"@internxt/sdk": "^1.5.3",
"@phosphor-icons/react": "2.0.9",
"@radix-ui/react-select": "^1.2.2",
"@sentry/electron": "^4.5.0",
Expand All @@ -274,11 +276,13 @@
"chance": "^1.1.11",
"crypto-js": "^4.1.1",
"dayjs": "^1.10.7",
"diod": "^2.0.0",
"electron-debug": "^3.2.0",
"electron-fetch": "^1.7.4",
"electron-log": "^4.4.4",
"electron-store": "^8.0.1",
"electron-updater": "^4.6.4",
"fflate": "^0.8.2",
"form-data": "^4.0.0",
"framer-motion": "^5.6.0",
"gm": "^1.25.0",
Expand Down
18 changes: 18 additions & 0 deletions src/apps/backups/BackupError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type BackupErrorCause =
| 'NOT_EXISTS'
| 'NO_PERMISSION'
| 'NO_INTERNET'
| 'NO_REMOTE_CONNECTION'
| 'BAD_RESPONSE'
| 'EMPTY_FILE'
| 'FILE_TOO_BIG'
| 'FILE_NON_EXTENSION'
| 'UNKNOWN'
| 'FILE_ALREADY_EXISTS'
| 'FOLDER_ALREADY_EXISTS';

export class BackupError extends Error {
constructor(public readonly cause: BackupErrorCause) {
super();
}
}
8 changes: 8 additions & 0 deletions src/apps/backups/BackupInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type BackupInfo = {
folderUuid: string;
folderId: number;
tmpPath: string;
backupsBucket: string;
pathname: string;
name: string;
};
263 changes: 263 additions & 0 deletions src/apps/backups/Backups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import { Service } from 'diod';
import Logger from 'electron-log';
import { FileBatchUpdater } from '../../context/local/localFile/application/update/FileBatchUpdater';
import { FileBatchUploader } from '../../context/local/localFile/application/upload/FileBatchUploader';
import { LocalFile } from '../../context/local/localFile/domain/LocalFile';
import { AbsolutePath } from '../../context/local/localFile/infrastructure/AbsolutePath';
import LocalTreeBuilder from '../../context/local/localTree/application/LocalTreeBuilder';
import { LocalTree } from '../../context/local/localTree/domain/LocalTree';
import { File } from '../../context/virtual-drive/files/domain/File';
import { SimpleFolderCreator } from '../../context/virtual-drive/folders/application/create/SimpleFolderCreator';
import { BackupInfo } from './BackupInfo';
import { BackupsIPCRenderer } from './BackupsIPCRenderer';
import { AddedFilesBatchCreator } from './batches/AddedFilesBatchCreator';
import { ModifiedFilesBatchCreator } from './batches/ModifiedFilesBatchCreator';
import { DiffFilesCalculator, FilesDiff } from './diff/DiffFilesCalculator';
import {
FoldersDiff,
FoldersDiffCalculator,
} from './diff/FoldersDiffCalculator';
import { getParentDirectory, relative, relativeV2 } from './utils/relative';

Check warning on line 20 in src/apps/backups/Backups.ts

View workflow job for this annotation

GitHub Actions / 🧪 Lint and test

'relative' is defined but never used
import { DriveDesktopError } from '../../context/shared/domain/errors/DriveDesktopError';
import { UserAvaliableSpaceValidator } from '../../context/user/usage/application/UserAvaliableSpaceValidator';
import { FileDeleter } from '../../context/virtual-drive/files/application/delete/FileDeleter';
import { RemoteTreeBuilder } from '../../context/virtual-drive/remoteTree/application/RemoteTreeBuilder';
import { RemoteTree } from '../../context/virtual-drive/remoteTree/domain/RemoteTree';

@Service()
export class Backup {
constructor(
private readonly localTreeBuilder: LocalTreeBuilder,
private readonly remoteTreeBuilder: RemoteTreeBuilder,
private readonly fileBatchUploader: FileBatchUploader,
private readonly fileBatchUpdater: FileBatchUpdater,
private readonly remoteFileDeleter: FileDeleter,
private readonly simpleFolderCreator: SimpleFolderCreator,
private readonly userAvaliableSpaceValidator: UserAvaliableSpaceValidator
) {}

private backed = 0;

async run(
info: BackupInfo,
abortController: AbortController
): Promise<DriveDesktopError | undefined> {
Logger.info('[BACKUPS] Backing:', info);

Logger.info('[BACKUPS] Generating local tree');
const localTreeEither = await this.localTreeBuilder.run(
info.pathname as AbsolutePath
);

Logger.debug('[BACKUPS] Local tree either', localTreeEither);

if (localTreeEither.isLeft()) {
Logger.error('[BACKUPS] local tree is left', localTreeEither);
return localTreeEither.getLeft();
}

const local = localTreeEither.getRight();

Logger.info('[BACKUPS] Generating remote tree', info.folderId);
const remote = await this.remoteTreeBuilder.run(info.folderId, true);

Logger.debug('[BACKUPS] Remote tree', JSON.stringify(remote));

Logger.debug('[BACKUPS] Remote tree file', remote.files);

Logger.debug('[BACKUPS] Remote tree folder', remote.folders);

const foldersDiff = FoldersDiffCalculator.calculate(local, remote);

Logger.debug('[BACKUPS] Folders diff', foldersDiff);

const filesDiff = DiffFilesCalculator.calculate(local, remote);

Logger.debug('[BACKUPS] Files diff', filesDiff);

await this.isThereEnoughSpace(filesDiff);

const alreadyBacked =
filesDiff.unmodified.length + foldersDiff.unmodified.length;

this.backed = alreadyBacked;

BackupsIPCRenderer.send(
'backups.total-items-calculated',
filesDiff.total + foldersDiff.total,
alreadyBacked
);

await this.backupFolders(foldersDiff, local, remote);

await this.backupFiles(filesDiff, local, remote, abortController);

return undefined;
}

private async isThereEnoughSpace(filesDiff: FilesDiff): Promise<void> {
const bytesToUpload = filesDiff.added.reduce((acc, file) => {
acc += file.size;

return acc;
}, 0);

const bytesToUpdate = Array.from(filesDiff.modified.entries()).reduce(
(acc, [local, remote]) => {
acc += local.size - remote.size;

return acc;
},
0
);

const total = bytesToUpdate + bytesToUpload;

const thereIsEnoughSpace = await this.userAvaliableSpaceValidator.run(
total
);

if (!thereIsEnoughSpace) {
throw new DriveDesktopError(
'NOT_ENOUGH_SPACE',
'The size of the files to upload is greater than the avaliable space'
);
}
}

private async backupFolders(
diff: FoldersDiff,
local: LocalTree,
remote: RemoteTree
) {
Logger.info('[BACKUPS] Backing folders');
Logger.info('[BACKUPS] Folders added', diff.added.length);

await Promise.all(
diff.added.map(async (localFolder) => {
const relativePath = relativeV2(local.root.path, localFolder.path);

if (relativePath === '/') {
return; // Ignorar la carpeta raíz
}

const remoteParentPath = getParentDirectory(
local.root.path,
localFolder.path
);
const parentExists = remote.has(remoteParentPath);

if (!parentExists) {
return;
}

const parent = remote.getParent(remoteParentPath);
const existingItems = remote.has(relativePath);

if (existingItems) {
return;
}

const folder = await this.simpleFolderCreator.run(
relativePath,
parent.id
);

remote.addFolder(parent, folder);

this.backed++;
BackupsIPCRenderer.send('backups.progress-update', this.backed);
})
);
}

private async backupFiles(
filesDiff: FilesDiff,
local: LocalTree,
remote: RemoteTree,
abortController: AbortController
) {
Logger.info('[BACKUPS] Backing files');

const { added, modified, deleted } = filesDiff;

Logger.info('[BACKUPS] Files added', added.length);
await this.uploadAndCreate(local.root.path, added, remote, abortController);

Logger.info('[BACKUPS] Files modified', modified.size);
await this.uploadAndUpdate(modified, local, remote, abortController);

Logger.info('[BACKUPS] Files deleted', deleted.length);
await this.deleteRemoteFiles(deleted, abortController);
}

private async uploadAndCreate(
localRootPath: string,
added: Array<LocalFile>,
tree: RemoteTree,
abortController: AbortController
): Promise<void> {
const batches = AddedFilesBatchCreator.run(added);

for (const batch of batches) {
if (abortController.signal.aborted) {
return;
}
// eslint-disable-next-line no-await-in-loop
await this.fileBatchUploader.run(
localRootPath,
tree,
batch,
abortController.signal
);

this.backed += batch.length;

Logger.debug('[Backed]', this.backed);
BackupsIPCRenderer.send('backups.progress-update', this.backed);
}
}

private async uploadAndUpdate(
modified: Map<LocalFile, File>,
localTree: LocalTree,
remoteTree: RemoteTree,
abortController: AbortController
): Promise<void> {
const batches = ModifiedFilesBatchCreator.run(modified);

for (const batch of batches) {
Logger.debug('Signal aborted', abortController.signal.aborted);
if (abortController.signal.aborted) {
return;
}
// eslint-disable-next-line no-await-in-loop
await this.fileBatchUpdater.run(
localTree.root,
remoteTree,
Array.from(batch.keys()),
abortController.signal
);

this.backed += batch.size;
BackupsIPCRenderer.send('backups.progress-update', this.backed);
}
}

private async deleteRemoteFiles(
deleted: Array<File>,
abortController: AbortController
) {
for (const file of deleted) {
if (abortController.signal.aborted) {
return;
}

// eslint-disable-next-line no-await-in-loop
await this.remoteFileDeleter.run(file);
}

this.backed += deleted.length;
BackupsIPCRenderer.send('backups.progress-update', this.backed);
}
}
Loading
Loading