feat: change backup format to plain SQL, admin-only cron, backup now button, import restore UI

This commit is contained in:
ff
2026-04-11 23:37:43 -04:00
parent 4025ca45e0
commit 66a3d271f1
6 changed files with 329 additions and 269 deletions

View File

@@ -2,7 +2,6 @@ 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";
// Local backup folder in the app root (apps/Backend/backups)
@@ -17,24 +16,17 @@ function ensureLocalBackupDir() {
}
}
async function runForAllUsers(
handler: (user: Awaited<ReturnType<typeof storage.getUsers>>[number]) => Promise<void>
) {
async function getAdminUser() {
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);
}
}
const admin = users.find((u) => u.username === "admin");
if (admin) return admin;
offset += batchSize;
}
return null;
}
export const startBackupCron = () => {
@@ -45,39 +37,31 @@ export const startBackupCron = () => {
console.log("🔄 [8 PM] Running local auto-backup...");
ensureLocalBackupDir();
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;
}
const admin = await getAdminUser();
if (!admin) {
console.warn("No admin user found, skipping local backup.");
return;
}
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."
);
}
});
if (!admin.autoBackupEnabled) {
console.log("✅ [8 PM] Auto-backup is disabled for admin, skipped.");
return;
}
try {
const filename = `dental_backup_${Date.now()}.sql`;
await backupDatabaseToPath({ destinationPath: LOCAL_BACKUP_DIR, filename });
await storage.createBackup(admin.id);
await storage.deleteNotificationsByType(admin.id, "BACKUP");
console.log(`✅ Local backup done → ${filename}`);
} catch (err) {
console.error("Local backup failed:", err);
await storage.createNotification(
admin.id,
"BACKUP",
"❌ Automatic backup failed. Please check the server backup folder."
);
}
console.log("✅ [8 PM] Local backup complete.");
});
@@ -88,46 +72,52 @@ export const startBackupCron = () => {
cron.schedule("0 21 * * *", async () => {
console.log("🔄 [9 PM] Running USB backup...");
await runForAllUsers(async (user) => {
if (!user.usbBackupEnabled) return;
const admin = await getAdminUser();
if (!admin) {
console.warn("No admin user found, skipping USB backup.");
return;
}
const destination = await storage.getActiveBackupDestination(user.id);
if (!destination) {
await storage.createNotification(
user.id,
"BACKUP",
"❌ USB backup failed: no backup destination configured."
);
return;
}
if (!admin.usbBackupEnabled) {
console.log("✅ [9 PM] USB backup is disabled for admin, skipped.");
return;
}
// The target is the "USB Backup" subfolder inside the configured drive path
const usbBackupPath = path.join(destination.path, USB_BACKUP_FOLDER_NAME);
const destination = await storage.getActiveBackupDestination(admin.id);
if (!destination) {
await storage.createNotification(
admin.id,
"BACKUP",
"❌ USB backup failed: no backup destination configured."
);
return;
}
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;
}
const usbBackupPath = path.join(destination.path, USB_BACKUP_FOLDER_NAME);
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."
);
}
});
if (!fs.existsSync(usbBackupPath)) {
await storage.createNotification(
admin.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()}.sql`;
await backupDatabaseToPath({ destinationPath: usbBackupPath, filename });
await storage.createBackup(admin.id);
await storage.deleteNotificationsByType(admin.id, "BACKUP");
console.log(`✅ USB backup done → ${usbBackupPath}/${filename}`);
} catch (err) {
console.error("USB backup failed:", err);
await storage.createNotification(
admin.id,
"BACKUP",
"❌ USB backup failed. Please check the USB drive and try again."
);
}
console.log("✅ [9 PM] USB backup complete.");
});