feat: database management - auto/USB backup toggles, folder browser, cron jobs
This commit is contained in:
@@ -1,100 +1,134 @@
|
||||
import cron from "node-cron";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { storage } from "../storage";
|
||||
import { NotificationTypes } from "@repo/db/types";
|
||||
import { backupDatabaseToPath } from "../services/databaseBackupService";
|
||||
|
||||
/**
|
||||
* Daily cron job to check if users haven't backed up in 7 days
|
||||
* Creates a backup notification if overdue
|
||||
*/
|
||||
// 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";
|
||||
|
||||
function ensureLocalBackupDir() {
|
||||
if (!fs.existsSync(LOCAL_BACKUP_DIR)) {
|
||||
fs.mkdirSync(LOCAL_BACKUP_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function runForAllUsers(
|
||||
handler: (user: Awaited<ReturnType<typeof storage.getUsers>>[number]) => Promise<void>
|
||||
) {
|
||||
const batchSize = 100;
|
||||
let offset = 0;
|
||||
while (true) {
|
||||
const users = await storage.getUsers(batchSize, offset);
|
||||
if (!users || users.length === 0) break;
|
||||
for (const user of users) {
|
||||
if (user.id == null) continue;
|
||||
try {
|
||||
await handler(user);
|
||||
} catch (err) {
|
||||
console.error(`Error processing user ${user.id}:`, err);
|
||||
}
|
||||
}
|
||||
offset += batchSize;
|
||||
}
|
||||
}
|
||||
|
||||
export const startBackupCron = () => {
|
||||
cron.schedule("0 22 * * *", async () => {
|
||||
// Every calendar days, at 10 PM
|
||||
// cron.schedule("*/10 * * * * *", async () => { // Every 10 seconds (for Test)
|
||||
// ============================================================
|
||||
// 8 PM — Local automatic backup to apps/Backend/backups/
|
||||
// ============================================================
|
||||
cron.schedule("0 20 * * *", async () => {
|
||||
console.log("🔄 [8 PM] Running local auto-backup...");
|
||||
ensureLocalBackupDir();
|
||||
|
||||
console.log("🔄 Running backup check...");
|
||||
|
||||
const userBatchSize = 100;
|
||||
let userOffset = 0;
|
||||
|
||||
while (true) {
|
||||
// Fetch a batch of users
|
||||
const users = await storage.getUsers(userBatchSize, userOffset);
|
||||
if (!users || users.length === 0) break;
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
if (user.id == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const destination = await storage.getActiveBackupDestination(user.id);
|
||||
const lastBackup = await storage.getLastBackup(user.id);
|
||||
|
||||
// ==============================
|
||||
// CASE 1: Destination exists → auto backup
|
||||
// ==============================
|
||||
if (destination) {
|
||||
if (!fs.existsSync(destination.path)) {
|
||||
await storage.createNotification(
|
||||
user.id,
|
||||
"BACKUP",
|
||||
"❌ Automatic backup failed: external drive not connected."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const filename = `dental_backup_${Date.now()}.zip`;
|
||||
|
||||
await backupDatabaseToPath({
|
||||
destinationPath: destination.path,
|
||||
filename,
|
||||
});
|
||||
|
||||
await storage.createBackup(user.id);
|
||||
await storage.deleteNotificationsByType(user.id, "BACKUP");
|
||||
|
||||
console.log(`✅ Auto backup successful for user ${user.id}`);
|
||||
continue;
|
||||
} catch (err) {
|
||||
console.error(`Auto backup failed for user ${user.id}`, err);
|
||||
|
||||
await storage.createNotification(
|
||||
user.id,
|
||||
"BACKUP",
|
||||
"❌ Automatic backup failed. Please check your backup destination."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// CASE 2: No destination → fallback to reminder
|
||||
// ==============================
|
||||
|
||||
const daysSince = lastBackup?.createdAt
|
||||
? (Date.now() - new Date(lastBackup.createdAt).getTime()) /
|
||||
(1000 * 60 * 60 * 24)
|
||||
: Infinity;
|
||||
|
||||
if (daysSince >= 7) {
|
||||
await storage.createNotification(
|
||||
user.id,
|
||||
"BACKUP" as NotificationTypes,
|
||||
"⚠️ It has been more than 7 days since your last backup."
|
||||
);
|
||||
console.log(`Notification created for user ${user.id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error processing user ${user.id}:`, err);
|
||||
await runForAllUsers(async (user) => {
|
||||
if (!user.autoBackupEnabled) {
|
||||
// No local backup — check if a 7-day reminder is needed
|
||||
const lastBackup = await storage.getLastBackup(user.id);
|
||||
const daysSince = lastBackup?.createdAt
|
||||
? (Date.now() - new Date(lastBackup.createdAt).getTime()) / (1000 * 60 * 60 * 24)
|
||||
: Infinity;
|
||||
if (daysSince >= 7) {
|
||||
await storage.createNotification(
|
||||
user.id,
|
||||
"BACKUP" as NotificationTypes,
|
||||
"⚠️ It has been more than 7 days since your last backup."
|
||||
);
|
||||
console.log(`Reminder notification created for user ${user.id}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
userOffset += userBatchSize; // next user batch
|
||||
}
|
||||
try {
|
||||
const filename = `dental_backup_user${user.id}_${Date.now()}.zip`;
|
||||
await backupDatabaseToPath({ destinationPath: LOCAL_BACKUP_DIR, filename });
|
||||
await storage.createBackup(user.id);
|
||||
await storage.deleteNotificationsByType(user.id, "BACKUP");
|
||||
console.log(`✅ Local backup done for user ${user.id} → ${filename}`);
|
||||
} catch (err) {
|
||||
console.error(`Local backup failed for user ${user.id}`, err);
|
||||
await storage.createNotification(
|
||||
user.id,
|
||||
"BACKUP",
|
||||
"❌ Automatic backup failed. Please check the server backup folder."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("✅ Daily backup check completed.");
|
||||
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...");
|
||||
|
||||
await runForAllUsers(async (user) => {
|
||||
if (!user.usbBackupEnabled) return;
|
||||
|
||||
const destination = await storage.getActiveBackupDestination(user.id);
|
||||
if (!destination) {
|
||||
await storage.createNotification(
|
||||
user.id,
|
||||
"BACKUP",
|
||||
"❌ USB backup failed: no backup destination configured."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// The target is the "USB Backup" subfolder inside the configured drive path
|
||||
const usbBackupPath = path.join(destination.path, USB_BACKUP_FOLDER_NAME);
|
||||
|
||||
if (!fs.existsSync(usbBackupPath)) {
|
||||
await storage.createNotification(
|
||||
user.id,
|
||||
"BACKUP",
|
||||
`❌ USB backup failed: folder "${USB_BACKUP_FOLDER_NAME}" not found on the drive. Make sure the USB drive is connected and the folder exists.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const filename = `dental_backup_usb_${Date.now()}.zip`;
|
||||
await backupDatabaseToPath({ destinationPath: usbBackupPath, filename });
|
||||
await storage.createBackup(user.id);
|
||||
await storage.deleteNotificationsByType(user.id, "BACKUP");
|
||||
console.log(`✅ USB backup done for user ${user.id} → ${usbBackupPath}/${filename}`);
|
||||
} catch (err) {
|
||||
console.error(`USB backup failed for user ${user.id}`, err);
|
||||
await storage.createNotification(
|
||||
user.id,
|
||||
"BACKUP",
|
||||
"❌ USB backup failed. Please check the USB drive and try again."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("✅ [9 PM] USB backup complete.");
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user