diff --git a/User/src/database/schemes/network_scheme.ts b/User/src/database/schemes/network_scheme.ts index 65b6a7c..90517ef 100644 --- a/User/src/database/schemes/network_scheme.ts +++ b/User/src/database/schemes/network_scheme.ts @@ -1,5 +1,4 @@ export interface NetworkUserScheme { - id: string ip: string name: string departmentId: string diff --git a/User/src/helpers/backup_manager.ts b/User/src/helpers/backup_manager.ts index 3f24852..a4e303f 100644 --- a/User/src/helpers/backup_manager.ts +++ b/User/src/helpers/backup_manager.ts @@ -6,18 +6,25 @@ import { operationCodes } from '../network/operation_codes' import { ParsedMessage } from '../network/message_handler' import { Database } from '../database/database' import { NetworkUserScheme } from '../database/schemes/network_scheme' +import { UserInfoScheme } from '../database/schemes/app_config_scheme' + +const backupDirectoryPath = path.join(__dirname, '..', 'backup') export class BackupManager { private fileEncryptor: FileEncryptor | null = null private readonly db: Database - private readonly clientPort: number + private readonly port: number + private serverIp: string = ''; private isBusy: boolean = false private intervalId: NodeJS.Timeout | null = null private stopRequested: boolean = false - constructor(pathToDatabaseFile: string, clientPort: number) { + constructor(pathToDatabaseFile: string, port: number) { this.db = new Database(pathToDatabaseFile) - this.clientPort = clientPort + this.port = port + this.db.read().then((data) => { + this.serverIp = data.network.serverIp + }); } async start(): Promise { @@ -46,11 +53,13 @@ export class BackupManager { this.fileEncryptor = new FileEncryptor(encryptionKey.key, encryptionKey.iv) await this.sendFilesToUsers( - userInfo.name, + userInfo, usersIp, backupDirectoryData.structure, backupDirectoryData.path, ) + + await this.validateBackupDirectories(backupDirectoryPath); } catch (error: any) { this.log(`Error in initialize process: ${error.message}`, 'error') } finally { @@ -72,57 +81,131 @@ export class BackupManager { return this.fileEncryptor.encryptFileToBase64(filePath) } + private async fetchUsersFromServer(): Promise> { + const tcpCommunicator = new TcpCommunicator(this.serverIp, this.port) + + try { + await tcpCommunicator.connect() + this.log(`Connected to server at ${this.serverIp}`) + + await tcpCommunicator.sendMessage(operationCodes.GET_USERS, {}) + + const response = await this.waitForResponse(tcpCommunicator) + if (!response || !response.metaInfo || !response.metaInfo.users) { + throw new Error('Invalid or missing user data from server.') + } + + const validUserDirectories = new Set() + for (const user of response.metaInfo.users) { + const { name, departmentId } = user + validUserDirectories.add(`${name}-${departmentId}`) + } + + return validUserDirectories + } catch (error) { + this.log(`Failed to fetch user list from server. Error: ${error}`, 'error') + return new Set() + } finally { + await tcpCommunicator.disconnect() + this.log(`Disconnected from server at ${this.serverIp}`) + } + } + + private async validateBackupDirectories(backupDirectoryPath: string): Promise { + try { + const validUserDirectories = await this.fetchUsersFromServer() + + if (!fs.existsSync(backupDirectoryPath)) { + this.log('Backup directory does not exist. No cleanup needed.') + return + } + + const existingDirectories = fs.readdirSync(backupDirectoryPath).filter((dir) => + fs.statSync(path.join(backupDirectoryPath, dir)).isDirectory() + ) + + for (const directory of existingDirectories) { + if (!validUserDirectories.has(directory)) { + this.log(`Deleting unrecognized backup directory: ${directory}`, 'warn') + fs.rmSync(path.join(backupDirectoryPath, directory), { recursive: true, force: true }) + } + } + + this.log('Backup directory validation and cleanup complete.') + } catch (error) { + // @ts-ignore + this.log(`Error validating backup directories: ${error.message}`, 'error') + } + } + private async sendFilesToUsers( - userName: string, + userInfo: UserInfoScheme, usersIp: string[], fileStructure: { [key: string]: string }, backupDirectoryPath: string, ): Promise { let unsentFiles = Object.keys(fileStructure) + let remainingUsers = [...usersIp] - for (const fileName of unsentFiles) { - const filePath = fileStructure[fileName] - const encryptedFileContent = this.encryptFile(filePath) - - if (!encryptedFileContent) { - this.log(`Failed to encrypt file: ${fileName}`, 'error') - continue - } - - const relativeFilePath = path.relative(backupDirectoryPath, filePath) - const metaInfo = { userName, relativeFilePath } - - for (const ip of usersIp) { - const tcpCommunicator = new TcpCommunicator(ip, this.clientPort) + while (unsentFiles.length > 0 && remainingUsers.length > 0) { + for (const ip of remainingUsers) { + const tcpCommunicator = new TcpCommunicator(ip, this.port) try { await tcpCommunicator.connect() this.log(`Connected to ${ip}`) - const sendSuccess = await tcpCommunicator.sendMessage( - operationCodes.BACKUP_FILE, - metaInfo, - Buffer.from(encryptedFileContent, 'base64'), - ) - if (!sendSuccess) { - throw new Error('Failed to send file content.') - } + const clearBackupMeta = { name: userInfo.name, departmentId: userInfo.departmentId } + await tcpCommunicator.sendMessage(operationCodes.CLEAR_BACKUP, clearBackupMeta) - const responseReceived = await this.waitForResponse(tcpCommunicator) - if (!responseReceived) { - throw new Error('Timeout waiting for the message response.') - } + for (const fileName of [...unsentFiles]) { + const filePath = fileStructure[fileName] + const encryptedFileContent = this.encryptFile(filePath) - this.log(`Successfully sent file: ${fileName} to ${ip}`) - unsentFiles = unsentFiles.filter((f) => f !== fileName) - break + if (!encryptedFileContent) { + this.log(`Failed to encrypt file: ${fileName}`, 'error') + continue + } + + const relativeFilePath = path.relative(backupDirectoryPath, filePath) + const metaInfo = { + name: userInfo.name, + departmentId: userInfo.departmentId, + relativeFilePath, + } + + const sendSuccess = await tcpCommunicator.sendMessage( + operationCodes.BACKUP_FILE, + metaInfo, + Buffer.from(encryptedFileContent, 'base64'), + ) + if (!sendSuccess) { + throw new Error('Failed to send file content.') + } + + const responseReceived = await this.waitForResponse(tcpCommunicator) + if (!responseReceived) { + throw new Error('Timeout waiting for the message response.') + } + + this.log(`Successfully sent file: ${fileName} to ${ip}`) + unsentFiles = unsentFiles.filter((f) => f !== fileName) + } } catch (error) { - this.log(`Failed to send file: ${fileName} to ${ip}. Error: ${error}`, 'error') + this.log(`Failed to send backup to ${ip}. Error: ${error}`, 'error') } finally { await tcpCommunicator.disconnect() this.log(`Disconnected from ${ip}`) } } + + // Update remaining users to retry + remainingUsers = usersIp.filter((ip) => !this.isBackupCompleteForIp(ip, unsentFiles)) + + if (remainingUsers.length === 0) { + this.log('Backup process retried for all users. Exiting retry loop.') + break + } } if (unsentFiles.length > 0) { @@ -132,6 +215,10 @@ export class BackupManager { } } + private isBackupCompleteForIp(ip: string, unsentFiles: string[]): boolean { + return unsentFiles.length === 0 + } + private async waitForResponse(tcpCommunicator: TcpCommunicator): Promise { return new Promise((resolve) => { const idResponseCheck = setInterval(() => { @@ -152,7 +239,6 @@ export class BackupManager { this.intervalId = null } - // Wait for any ongoing process to complete if busy while (this.isBusy) { await new Promise((resolve) => setTimeout(resolve, 100)) } @@ -160,7 +246,6 @@ export class BackupManager { console.log('[BackupManager] Stopped successfully.') } - // Unified logging function private log(message: string, level: 'log' | 'error' | 'warn' = 'log'): void { const prefix = '[BackupManager]' if (level === 'error') { diff --git a/User/src/helpers/backup_retrieval.ts b/User/src/helpers/backup_retrieval.ts index 6d63cc7..2db104d 100644 --- a/User/src/helpers/backup_retrieval.ts +++ b/User/src/helpers/backup_retrieval.ts @@ -5,6 +5,7 @@ import fs from 'fs' import crypto from 'crypto' import { ParsedMessage } from '../network/message_handler' import { Database } from '../database/database' +import { UserInfoScheme } from '../database/schemes/app_config_scheme' export class BackupRetrievalWorker { private db: Database @@ -41,7 +42,6 @@ export class BackupRetrievalWorker { const userInfo = data.app_config.user_info const encryptionKey = data.app_config.encryption_key - const userName = userInfo.name this.encryptionKey = Buffer.from(encryptionKey.key, 'base64') this.iv = Buffer.from(encryptionKey.iv, 'base64') @@ -51,7 +51,7 @@ export class BackupRetrievalWorker { } for (const lanUser of activeUsers) { - const success = await this.processBackupForIp(lanUser.ip, userName) + const success = await this.processBackupForIp(lanUser.ip, userInfo) if (!success) { throw new Error(`Failed to retrieve backup from ${lanUser.ip}`) } @@ -73,33 +73,37 @@ export class BackupRetrievalWorker { } } - private async processBackupForIp(ip: string, userName: string): Promise { + private async processBackupForIp(ip: string, userInfo: UserInfoScheme): Promise { this.tcpCommunicator = new TcpCommunicator(ip, this.clientPort) if (!(await this.tcpCommunicator.connect())) { this.log(`Failed to connect to ${ip}`, 'error') return true } - const backupExists = await this.checkIfBackupExists(userName) + const backupExists = await this.checkIfBackupExists(userInfo.name, userInfo.departmentId) if (!backupExists) { - this.log(`No backup found for user ${userName} on IP ${ip}`) + this.log(`No backup found for user ${userInfo.name} on IP ${ip}`) await this.tcpCommunicator.disconnect() return true } - const backupStructure = await this.requestBackupStructure(userName) + const backupStructure = await this.requestBackupStructure(userInfo.name, userInfo.departmentId) if (!backupStructure || Object.keys(backupStructure).length === 0) { - this.log(`No files found in backup structure for user ${userName} on IP ${ip}`) + this.log(`No files found in backup structure for user ${userInfo.name} on IP ${ip}`) await this.tcpCommunicator.disconnect() return true } for (const relativeFilePath of Object.keys(backupStructure)) { - const fileRequestSuccess = await this.requestBackupFile(userName, relativeFilePath) + const fileRequestSuccess = await this.requestBackupFile( + userInfo.name, + userInfo.departmentId, + relativeFilePath, + ) if (!fileRequestSuccess) { await this.tcpCommunicator.disconnect() throw new Error( - `Failed to retrieve file ${relativeFilePath} from backup for user ${userName} on IP ${ip}`, + `Failed to retrieve file ${relativeFilePath} from backup for user ${userInfo.name} on IP ${ip}`, ) } } @@ -108,10 +112,10 @@ export class BackupRetrievalWorker { return true } - private async checkIfBackupExists(userName: string): Promise { + private async checkIfBackupExists(name: string, departmentId: string): Promise { if (!this.tcpCommunicator) return false - const metaInfo = { name: userName } + const metaInfo = { name, departmentId } if (!(await this.tcpCommunicator.sendMessage(operationCodes.IS_BACKUP_CREATED, metaInfo))) return false @@ -119,10 +123,10 @@ export class BackupRetrievalWorker { return response?.metaInfo?.backupExists === true } - private async requestBackupStructure(userName: string): Promise { + private async requestBackupStructure(name: string, departmentId: string): Promise { if (!this.tcpCommunicator) return false - const metaInfo = { name: userName } + const metaInfo = { name, departmentId } if (!(await this.tcpCommunicator.sendMessage(operationCodes.GET_BACKUP_STRUCTURE, metaInfo))) return null @@ -130,10 +134,14 @@ export class BackupRetrievalWorker { return response?.metaInfo?.structure || null } - private async requestBackupFile(userName: string, relativeFilePath: string): Promise { + private async requestBackupFile( + name: string, + departmentId: string, + relativeFilePath: string, + ): Promise { if (!this.tcpCommunicator) return false - const metaInfo = { name: userName, relativeFilePath } + const metaInfo = { name, departmentId, relativeFilePath } if (!(await this.tcpCommunicator.sendMessage(operationCodes.REQ_FILE_FROM_BACKUP, metaInfo))) return false @@ -185,17 +193,6 @@ export class BackupRetrievalWorker { } } - async stop(): Promise { - this.stopRequested = true // Signal that stop is requested - - // Wait for any ongoing process to complete if busy - while (this.isBusy) { - await new Promise((resolve) => setTimeout(resolve, 100)) - } - - console.log('[BackupManager] Stopped successfully.') - } - private async waitForResponse(): Promise { return new Promise((resolve) => { const idResponseCheck = setInterval(async () => { diff --git a/User/src/helpers/department_sharer.ts b/User/src/helpers/department_sharer.ts index dbf5067..4fee53e 100644 --- a/User/src/helpers/department_sharer.ts +++ b/User/src/helpers/department_sharer.ts @@ -4,7 +4,7 @@ import { TcpCommunicator } from './tcp_communicator' import { operationCodes } from '../network/operation_codes' import { ParsedMessage } from '../network/message_handler' import { Database } from '../database/database' -import {NetworkUserScheme} from "../database/schemes/network_scheme"; +import { NetworkUserScheme } from '../database/schemes/network_scheme' export class DepartmentSharer { private readonly db: Database @@ -47,7 +47,7 @@ export class DepartmentSharer { const activeUsers = data.network.usersInLan this.departmentDirectory = departmentStructure.path - if(departmentStructure.path === ''){ + if (departmentStructure.path === '') { throw new Error('Department directory not set.') } diff --git a/User/src/helpers/network_scanner.ts b/User/src/helpers/network_scanner.ts index fb46f85..cb8b66b 100644 --- a/User/src/helpers/network_scanner.ts +++ b/User/src/helpers/network_scanner.ts @@ -113,7 +113,6 @@ export class NetworkScanner { // Save the filtered IPs to 'users_ip' await this.db.update((data) => { data.network.usersInLan = activeClients.map((client) => ({ - id: client.id, ip: client.ip, name: client.name, departmentId: client.departmentId, diff --git a/User/src/main/preload.ts b/User/src/main/preload.ts index c685bc9..160a4b6 100644 --- a/User/src/main/preload.ts +++ b/User/src/main/preload.ts @@ -52,5 +52,5 @@ contextBridge.exposeInMainWorld('uiAPI', { readAnnouncement: (): Promise => ipcRenderer.invoke('read-announcement'), closeAnnouncementWindow: (): Promise => ipcRenderer.invoke('close-announcement-window'), startBackupRetrieval: (destinationPath: string): Promise => - ipcRenderer.invoke('start-backup-retrieval', destinationPath), + ipcRenderer.invoke('start-backup-retrieval', destinationPath), }) diff --git a/User/src/network/operations_custom/general_operations.ts b/User/src/network/operations_custom/general_operations.ts index e72903e..36d4567 100644 --- a/User/src/network/operations_custom/general_operations.ts +++ b/User/src/network/operations_custom/general_operations.ts @@ -43,7 +43,6 @@ export class GeneralOperations implements OperationPlugin { if (data.app_config.logged_in) { response.name = data.app_config.user_info.name response.departmentId = data.app_config.user_info.departmentId - response.id = data.app_config.user_info.id } return { diff --git a/User/src/network/operations_custom/user_to_user_operations.ts b/User/src/network/operations_custom/user_to_user_operations.ts index 523c135..d260f02 100644 --- a/User/src/network/operations_custom/user_to_user_operations.ts +++ b/User/src/network/operations_custom/user_to_user_operations.ts @@ -64,7 +64,8 @@ export class UserToUserOperations implements OperationPlugin { public static async handleBackupFile(parsedMessage: ParsedMessage): Promise { if ( - !parsedMessage.metaInfo?.userName || + !parsedMessage.metaInfo?.name || + !parsedMessage.metaInfo.departmentId || !parsedMessage.metaInfo?.relativeFilePath || !parsedMessage.fileContent ) { @@ -74,8 +75,15 @@ export class UserToUserOperations implements OperationPlugin { } } - const { userName, relativeFilePath } = parsedMessage.metaInfo - const fullFilePath = path.join(__dirname, '..', '..', 'backups', userName, relativeFilePath) + const { name, departmentId, relativeFilePath } = parsedMessage.metaInfo + const fullFilePath = path.join( + __dirname, + '..', + '..', + 'backups', + `${name}-${departmentId}`, + relativeFilePath, + ) try { if (!(await UserToUserOperations.hasEnoughDiskSpace(path.dirname(fullFilePath), 25))) { @@ -103,17 +111,13 @@ export class UserToUserOperations implements OperationPlugin { } public static async handleClearBackup(parsedMessage: ParsedMessage): Promise { - if (!parsedMessage.metaInfo?.userName) { + if (!parsedMessage.metaInfo?.name || !parsedMessage.metaInfo?.departmentId) { return { operationCode: operationCodes.ERR, metaInfo: { message: 'Missing user name.' } } } - const userBackupDir = path.join( - __dirname, - '..', - '..', - 'backups', - parsedMessage.metaInfo.userName, - ) + const { name, departmentId } = parsedMessage.metaInfo + + const userBackupDir = path.join(__dirname, '..', '..', 'backups', `${name}-${departmentId}`) try { await fs.rm(userBackupDir, { recursive: true, force: true }) console.log(`Backup cleared: ${userBackupDir}`) @@ -146,8 +150,6 @@ export class UserToUserOperations implements OperationPlugin { const data = await database.read() const shareDirectory = data.local_resources.directory_schemes.shared.path - console.log(`\n\nShare directory: ${shareDirectory}\n\n`) - if (!shareDirectory || shareDirectory === '') { return { operationCode: operationCodes.ERR, @@ -267,14 +269,16 @@ export class UserToUserOperations implements OperationPlugin { } public static async handleIsBackupCreated(parsedMessage: ParsedMessage): Promise { - if (!parsedMessage.metaInfo?.name) { + if (!parsedMessage.metaInfo?.name || !parsedMessage.metaInfo?.departmentId) { return { operationCode: operationCodes.ERR, metaInfo: { message: 'Missing name in meta information.' }, } } - const userBackupDir = path.join(__dirname, '..', '..', 'backups', parsedMessage.metaInfo.name) + const { name, departmentId } = parsedMessage.metaInfo + + const userBackupDir = path.join(__dirname, '..', '..', 'backups', `${name}-${departmentId}`) try { const exists = await fs @@ -294,14 +298,16 @@ export class UserToUserOperations implements OperationPlugin { public static async handleGetBackupStructure( parsedMessage: ParsedMessage, ): Promise { - if (!parsedMessage.metaInfo?.name) { + if (!parsedMessage.metaInfo?.name || !parsedMessage.metaInfo?.departmentId) { return { operationCode: operationCodes.ERR, metaInfo: { message: 'Missing name in meta information.' }, } } - const userBackupDir = path.join(__dirname, '..', '..', 'backups', parsedMessage.metaInfo.name) + const { name, departmentId } = parsedMessage.metaInfo + + const userBackupDir = path.join(__dirname, '..', '..', 'backups', `${name}-${departmentId}`) try { const structure = await UserToUserOperations.buildDirectoryStructure(userBackupDir) @@ -333,15 +339,26 @@ export class UserToUserOperations implements OperationPlugin { public static async handleReqFileFromBackup( parsedMessage: ParsedMessage, ): Promise { - if (!parsedMessage.metaInfo?.name || !parsedMessage.metaInfo?.relativeFilePath) { + if ( + !parsedMessage.metaInfo?.name || + !parsedMessage.metaInfo?.departmentId || + !parsedMessage.metaInfo?.relativeFilePath + ) { return { operationCode: operationCodes.ERR, metaInfo: { message: 'Missing user name or file path in meta information.' }, } } - const { name, relativeFilePath } = parsedMessage.metaInfo - const fullFilePath = path.join(__dirname, '..', '..', 'backups', name, relativeFilePath) + const { name, departmentId, relativeFilePath } = parsedMessage.metaInfo + const fullFilePath = path.join( + __dirname, + '..', + '..', + 'backups', + `${name}-${departmentId}`, + relativeFilePath, + ) try { const fileContent = await fs.readFile(fullFilePath) diff --git a/User/src/network/udp/udp_client.ts b/User/src/network/udp/udp_client.ts index 44cdff2..618c0a1 100644 --- a/User/src/network/udp/udp_client.ts +++ b/User/src/network/udp/udp_client.ts @@ -1,9 +1,9 @@ import dgram from 'dgram' import ping from 'ping' -import {OperationHandler} from '../operations_base/operation_handler' -import {MessageHandler} from '../message_handler' -import {GeneralOperations} from '../operations_custom/general_operations' -import {operationCodes} from '../operation_codes' +import { OperationHandler } from '../operations_base/operation_handler' +import { MessageHandler } from '../message_handler' +import { GeneralOperations } from '../operations_custom/general_operations' +import { operationCodes } from '../operation_codes' import os from 'os' export class UdpClient { @@ -107,8 +107,7 @@ export class UdpClient { private dropConnection(ip: string): void { try { this.udpSocket.removeAllListeners('message') - } catch (err: any) { - } + } catch (err: any) {} } // Get the subnet (e.g., 192.168.1)