import cron from "node-cron"; import fs from "fs"; import path from "path"; import { storage } from "../storage"; import { backupDatabaseToPath } from "../services/databaseBackupService"; import { cronJobLogStorage } from "../storage/cron-job-log-storage"; import { readSyncConfig, writeSyncConfig } from "../services/networkSyncConfigService"; import { runNetworkSync, runNetworkFilesSync } from "../services/networkSyncService"; import { readRcloneConfig, writeRcloneConfig } from "../services/rcloneConfigService"; import { runRclonePull } from "../services/rcloneService"; // Local backup folder in the app root (apps/Backend/backups) const LOCAL_BACKUP_DIR = path.resolve(process.cwd(), "backups"); // Name of the USB backup subfolder the user creates on their drive const USB_BACKUP_FOLDER_NAME = "USB Backup"; const MAX_BACKUPS = 15; function ensureLocalBackupDir() { if (!fs.existsSync(LOCAL_BACKUP_DIR)) { fs.mkdirSync(LOCAL_BACKUP_DIR, { recursive: true }); } } function pruneOldBackups(dir: string) { try { const files = fs.readdirSync(dir) .filter((f) => f.endsWith(".sql") || f.endsWith(".zip")) .map((f) => ({ name: f, mtime: fs.statSync(path.join(dir, f)).mtimeMs })) .sort((a, b) => b.mtime - a.mtime); const toDelete = files.slice(MAX_BACKUPS); for (const file of toDelete) { fs.unlinkSync(path.join(dir, file.name)); console.log(`🗑️ Pruned old backup: ${file.name}`); } } catch (err) { console.warn("Failed to prune old backups:", err); } } async function getAdminUser() { const batchSize = 100; let offset = 0; while (true) { const users = await storage.getUsers(batchSize, offset); if (!users || users.length === 0) break; const admin = users.find((u) => u.username === "admin"); if (admin) return admin; offset += batchSize; } return null; } export const startBackupCron = () => { // ============================================================ // Every hour — Local automatic backup (runs when hour matches setting) // ============================================================ cron.schedule("0 * * * *", async () => { const admin = await getAdminUser(); if (!admin) return; if (!admin.autoBackupEnabled) return; const currentHour = new Date().getHours(); const backupHour = admin.autoBackupHour ?? 20; if (currentHour !== backupHour) return; console.log(`🔄 [${backupHour}:00] Running local auto-backup...`); ensureLocalBackupDir(); const startedAt = new Date(); const log = await cronJobLogStorage.createJobLog("local-backup", startedAt); try { const filename = `dental_backup_${Date.now()}.zip`; await backupDatabaseToPath({ destinationPath: LOCAL_BACKUP_DIR, filename }); pruneOldBackups(LOCAL_BACKUP_DIR); await storage.createBackup(admin.id); await storage.deleteNotificationsByType(admin.id, "BACKUP"); await cronJobLogStorage.completeJobLog(log.id, "success", new Date()); console.log(`✅ Local backup done → ${filename}`); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); console.error("Local backup failed:", err); await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage); await storage.createNotification( admin.id, "BACKUP", "❌ Automatic backup failed. Please check the server backup folder." ); } }); // ============================================================ // Every hour — USB backup (runs when hour matches setting) // ============================================================ cron.schedule("0 * * * *", async () => { const admin = await getAdminUser(); if (!admin) return; if (!admin.usbBackupEnabled) return; const currentHour = new Date().getHours(); const backupHour = admin.usbBackupHour ?? 21; if (currentHour !== backupHour) return; console.log(`🔄 [${backupHour}:00] Running USB backup...`); const startedAt = new Date(); const log = await cronJobLogStorage.createJobLog("usb-backup", startedAt); const destination = await storage.getActiveBackupDestination(admin.id); if (!destination) { const errorMessage = "No backup destination configured."; await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage); await storage.createNotification( admin.id, "BACKUP", "❌ USB backup failed: no backup destination configured." ); return; } if (!fs.existsSync(destination.path)) { const errorMessage = "Backup destination drive not found or disconnected."; await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage); await storage.createNotification( admin.id, "BACKUP", "❌ USB backup failed: backup drive not found. Make sure the USB drive is connected." ); return; } const usbBackupPath = path.join(destination.path, USB_BACKUP_FOLDER_NAME); if (!fs.existsSync(usbBackupPath)) { fs.mkdirSync(usbBackupPath, { recursive: true }); } try { const filename = `dental_backup_usb_${Date.now()}.zip`; await backupDatabaseToPath({ destinationPath: usbBackupPath, filename }); pruneOldBackups(usbBackupPath); await storage.createBackup(admin.id); await storage.deleteNotificationsByType(admin.id, "BACKUP"); await cronJobLogStorage.completeJobLog(log.id, "success", new Date()); console.log(`✅ USB backup done → ${usbBackupPath}/${filename}`); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); console.error("USB backup failed:", err); await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage); await storage.createNotification( admin.id, "BACKUP", "❌ USB backup failed. Please check the USB drive and try again." ); } }); // ============================================================ // Every hour — Rclone backup (runs only when hour matches config) // ============================================================ cron.schedule("0 * * * *", async () => { const rcloneConfig = readRcloneConfig(); if (!rcloneConfig.receiverEnabled || !rcloneConfig.sourceIp) return; const currentHour = new Date().getHours(); if (currentHour !== rcloneConfig.receiverSyncHour) return; console.log(`[${rcloneConfig.receiverSyncHour}:00] Running rclone pull from ${rcloneConfig.sourceIp}...`); const admin = await getAdminUser(); const startedAt = new Date(); const log = await cronJobLogStorage.createJobLog("rclone-backup", startedAt); try { await runRclonePull(); writeRcloneConfig({ lastSyncAt: new Date().toISOString(), lastSyncStatus: "success", lastSyncError: null }); await cronJobLogStorage.completeJobLog(log.id, "success", new Date()); console.log(`Rclone backup complete.`); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); console.error("Rclone backup failed:", err); writeRcloneConfig({ lastSyncAt: new Date().toISOString(), lastSyncStatus: "failed", lastSyncError: errorMessage }); await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage); if (admin) { await storage.createNotification( admin.id, "BACKUP", `Rclone backup failed: ${errorMessage}` ); } } }); // ============================================================ // Every hour — Network sync (runs only when hour matches config) // ============================================================ cron.schedule("0 * * * *", async () => { const config = readSyncConfig(); if (!config.enabled || !config.sourceUrl || !config.apiKey) return; const currentHour = new Date().getHours(); if (currentHour !== config.syncHour) return; console.log(`🔄 [${config.syncHour}:00] Running network sync from ${config.sourceUrl}...`); const admin = await getAdminUser(); const startedAt = new Date(); const log = await cronJobLogStorage.createJobLog("network-sync", startedAt); try { await runNetworkSync(config.sourceUrl, config.apiKey); await runNetworkFilesSync(config.sourceUrl, config.apiKey); writeSyncConfig({ lastSyncAt: new Date().toISOString(), lastSyncStatus: "success", lastSyncError: null }); await cronJobLogStorage.completeJobLog(log.id, "success", new Date()); console.log(`✅ Network sync complete (database + uploads).`); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); console.error("Network sync failed:", err); writeSyncConfig({ lastSyncAt: new Date().toISOString(), lastSyncStatus: "failed", lastSyncError: errorMessage }); await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage); if (admin) { await storage.createNotification( admin.id, "BACKUP", `❌ Network sync failed: ${errorMessage}` ); } } }); };