Files
DentalManagementMH06/apps/Backend/src/cron/backupCheck.ts
ff 70b5e2ba47 feat: add configurable backup time for local and USB backups
Replace hardcoded 8 PM / 9 PM backup schedules with user-selectable
hour dropdowns. Adds autoBackupHour and usbBackupHour fields to User
model. Cron jobs now check every hour against the configured time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 23:37:34 -04:00

234 lines
9.0 KiB
TypeScript
Executable File

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}`
);
}
}
});
};