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"; // 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 = () => { // ============================================================ // 8 PM — Local automatic backup to apps/Backend/backups/ // ============================================================ cron.schedule("0 20 * * *", async () => { console.log("🔄 [8 PM] Running local auto-backup..."); ensureLocalBackupDir(); const admin = await getAdminUser(); if (!admin) { console.warn("No admin user found, skipping local backup."); return; } if (!admin.autoBackupEnabled) { console.log("✅ [8 PM] Auto-backup is disabled for admin, skipped."); const startedAt = new Date(); const log = await cronJobLogStorage.createJobLog("local-backup", startedAt); await cronJobLogStorage.completeJobLog(log.id, "skipped", new Date()); await storage.deleteNotificationsByType(admin.id, "BACKUP"); return; } 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." ); } console.log("✅ [8 PM] Local backup complete."); }); // ============================================================ // 9 PM — USB backup to the "USB Backup" folder on the drive // ============================================================ cron.schedule("0 21 * * *", async () => { console.log("🔄 [9 PM] Running USB backup..."); const admin = await getAdminUser(); if (!admin) { console.warn("No admin user found, skipping USB backup."); return; } if (!admin.usbBackupEnabled) { console.log("✅ [9 PM] USB backup is disabled for admin, skipped."); const startedAt = new Date(); const log = await cronJobLogStorage.createJobLog("usb-backup", startedAt); await cronJobLogStorage.completeJobLog(log.id, "skipped", new Date()); await storage.deleteNotificationsByType(admin.id, "BACKUP"); return; } 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." ); } console.log("✅ [9 PM] USB backup complete."); }); // ============================================================ // 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}` ); } } }); };