updated backup manager to also clean
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
export interface NetworkUserScheme {
|
||||
id: string
|
||||
ip: string
|
||||
name: string
|
||||
departmentId: string
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user