updated backup manager to also clean

This commit is contained in:
andrei-mihnea-cerbu
2025-02-05 12:22:44 +02:00
parent a4ee64b3d1
commit 5579c7c281
9 changed files with 190 additions and 95 deletions
@@ -1,5 +1,4 @@
export interface NetworkUserScheme {
id: string
ip: string
name: string
departmentId: string
+103 -18
View File
@@ -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<void> {
@@ -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,15 +81,84 @@ export class BackupManager {
return this.fileEncryptor.encryptFileToBase64(filePath)
}
private async fetchUsersFromServer(): Promise<Set<string>> {
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<string>()
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<string>()
} finally {
await tcpCommunicator.disconnect()
this.log(`Disconnected from server at ${this.serverIp}`)
}
}
private async validateBackupDirectories(backupDirectoryPath: string): Promise<void> {
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<void> {
let unsentFiles = Object.keys(fileStructure)
let remainingUsers = [...usersIp]
for (const fileName of unsentFiles) {
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 clearBackupMeta = { name: userInfo.name, departmentId: userInfo.departmentId }
await tcpCommunicator.sendMessage(operationCodes.CLEAR_BACKUP, clearBackupMeta)
for (const fileName of [...unsentFiles]) {
const filePath = fileStructure[fileName]
const encryptedFileContent = this.encryptFile(filePath)
@@ -90,14 +168,11 @@ export class BackupManager {
}
const relativeFilePath = path.relative(backupDirectoryPath, filePath)
const metaInfo = { userName, relativeFilePath }
for (const ip of usersIp) {
const tcpCommunicator = new TcpCommunicator(ip, this.clientPort)
try {
await tcpCommunicator.connect()
this.log(`Connected to ${ip}`)
const metaInfo = {
name: userInfo.name,
departmentId: userInfo.departmentId,
relativeFilePath,
}
const sendSuccess = await tcpCommunicator.sendMessage(
operationCodes.BACKUP_FILE,
@@ -115,14 +190,22 @@ export class BackupManager {
this.log(`Successfully sent file: ${fileName} to ${ip}`)
unsentFiles = unsentFiles.filter((f) => f !== fileName)
break
}
} 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<ParsedMessage | null> {
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') {
+23 -26
View File
@@ -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<boolean> {
private async processBackupForIp(ip: string, userInfo: UserInfoScheme): Promise<boolean> {
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<boolean> {
private async checkIfBackupExists(name: string, departmentId: string): Promise<boolean> {
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<any> {
private async requestBackupStructure(name: string, departmentId: string): Promise<any> {
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<boolean> {
private async requestBackupFile(
name: string,
departmentId: string,
relativeFilePath: string,
): Promise<boolean> {
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<void> {
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<ParsedMessage | null> {
return new Promise((resolve) => {
const idResponseCheck = setInterval(async () => {
+2 -2
View File
@@ -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.')
}
-1
View File
@@ -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,
@@ -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 {
@@ -64,7 +64,8 @@ export class UserToUserOperations implements OperationPlugin {
public static async handleBackupFile(parsedMessage: ParsedMessage): Promise<ParsedMessage> {
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<ParsedMessage> {
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<ParsedMessage> {
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<ParsedMessage> {
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<ParsedMessage> {
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)
+5 -6
View File
@@ -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)