backup and restore

This commit is contained in:
2026-03-15 15:21:27 -05:00
parent e7cfff3eca
commit f858fe4785
16 changed files with 472 additions and 69 deletions
+68 -23
View File
@@ -1,4 +1,5 @@
import type { StartupValidationCheckDto } from "@mrp/shared";
import type { StartupValidationCheckDto, StartupValidationReportDto } from "@mrp/shared";
import { constants as fsConstants } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
@@ -6,11 +7,6 @@ import { env } from "../config/env.js";
import { paths } from "../config/paths.js";
import { prisma } from "./prisma.js";
interface StartupValidationReport {
status: "PASS" | "WARN" | "FAIL";
checks: StartupValidationCheckDto[];
}
async function pathExists(targetPath: string) {
try {
await fs.access(targetPath);
@@ -20,32 +16,70 @@ async function pathExists(targetPath: string) {
}
}
export async function collectStartupValidationReport(): Promise<StartupValidationReport> {
async function canWritePath(targetPath: string) {
try {
await fs.access(targetPath, fsConstants.W_OK);
return true;
} catch {
return false;
}
}
export async function collectStartupValidationReport(): Promise<StartupValidationReportDto> {
const startedAt = Date.now();
const checks: StartupValidationCheckDto[] = [];
const dataDirExists = await pathExists(paths.dataDir);
const uploadsDirExists = await pathExists(paths.uploadsDir);
const prismaDirExists = await pathExists(paths.prismaDir);
const databaseFilePath = path.join(paths.prismaDir, "app.db");
const databaseFileExists = await pathExists(databaseFilePath);
const clientBundlePath = path.join(paths.clientDistDir, "index.html");
const clientBundleExists = await pathExists(clientBundlePath);
const puppeteerPath = env.PUPPETEER_EXECUTABLE_PATH || "/usr/bin/chromium";
const puppeteerExists = await pathExists(puppeteerPath);
const dataDirWritable = dataDirExists && (await canWritePath(paths.dataDir));
const uploadsDirWritable = uploadsDirExists && (await canWritePath(paths.uploadsDir));
checks.push({
id: "data-dir",
label: "Data directory",
status: (await pathExists(paths.dataDir)) ? "PASS" : "FAIL",
message: (await pathExists(paths.dataDir)) ? `Data directory available at ${paths.dataDir}.` : `Data directory is missing: ${paths.dataDir}.`,
status: dataDirExists ? "PASS" : "FAIL",
message: dataDirExists ? `Data directory available at ${paths.dataDir}.` : `Data directory is missing: ${paths.dataDir}.`,
});
checks.push({
id: "uploads-dir",
label: "Uploads directory",
status: (await pathExists(paths.uploadsDir)) ? "PASS" : "FAIL",
message: (await pathExists(paths.uploadsDir))
? `Uploads directory available at ${paths.uploadsDir}.`
: `Uploads directory is missing: ${paths.uploadsDir}.`,
status: uploadsDirExists ? "PASS" : "FAIL",
message: uploadsDirExists ? `Uploads directory available at ${paths.uploadsDir}.` : `Uploads directory is missing: ${paths.uploadsDir}.`,
});
checks.push({
id: "prisma-dir",
label: "Prisma directory",
status: (await pathExists(paths.prismaDir)) ? "PASS" : "FAIL",
message: (await pathExists(paths.prismaDir))
? `Prisma data directory available at ${paths.prismaDir}.`
: `Prisma data directory is missing: ${paths.prismaDir}.`,
status: prismaDirExists ? "PASS" : "FAIL",
message: prismaDirExists ? `Prisma data directory available at ${paths.prismaDir}.` : `Prisma data directory is missing: ${paths.prismaDir}.`,
});
checks.push({
id: "database-file",
label: "Database file",
status: databaseFileExists ? "PASS" : env.NODE_ENV === "production" ? "FAIL" : "WARN",
message: databaseFileExists ? `SQLite database file found at ${databaseFilePath}.` : `SQLite database file is missing: ${databaseFilePath}.`,
});
checks.push({
id: "data-dir-write",
label: "Data directory writable",
status: dataDirWritable ? "PASS" : "FAIL",
message: dataDirWritable ? `Application can write to ${paths.dataDir}.` : `Application cannot write to ${paths.dataDir}.`,
});
checks.push({
id: "uploads-dir-write",
label: "Uploads directory writable",
status: uploadsDirWritable ? "PASS" : "FAIL",
message: uploadsDirWritable ? `Application can write to ${paths.uploadsDir}.` : `Application cannot write to ${paths.uploadsDir}.`,
});
try {
@@ -69,10 +103,8 @@ export async function collectStartupValidationReport(): Promise<StartupValidatio
checks.push({
id: "client-dist",
label: "Client bundle",
status: (await pathExists(path.join(paths.clientDistDir, "index.html"))) ? "PASS" : "FAIL",
message: (await pathExists(path.join(paths.clientDistDir, "index.html")))
? `Client bundle found at ${paths.clientDistDir}.`
: `Production client bundle is missing from ${paths.clientDistDir}.`,
status: clientBundleExists ? "PASS" : "FAIL",
message: clientBundleExists ? `Client bundle found at ${paths.clientDistDir}.` : `Production client bundle is missing from ${paths.clientDistDir}.`,
});
} else {
checks.push({
@@ -83,8 +115,6 @@ export async function collectStartupValidationReport(): Promise<StartupValidatio
});
}
const puppeteerPath = env.PUPPETEER_EXECUTABLE_PATH || "/usr/bin/chromium";
const puppeteerExists = await pathExists(puppeteerPath);
checks.push({
id: "puppeteer-runtime",
label: "PDF runtime",
@@ -104,6 +134,16 @@ export async function collectStartupValidationReport(): Promise<StartupValidatio
: `Client origin is configured as ${env.CLIENT_ORIGIN}.`,
});
checks.push({
id: "jwt-secret",
label: "JWT secret",
status: env.NODE_ENV === "production" && env.JWT_SECRET === "change-me" ? "WARN" : "PASS",
message:
env.NODE_ENV === "production" && env.JWT_SECRET === "change-me"
? "Production is still using the default JWT secret."
: "JWT secret is not using the default production value.",
});
checks.push({
id: "admin-password",
label: "Bootstrap admin password",
@@ -122,6 +162,11 @@ export async function collectStartupValidationReport(): Promise<StartupValidatio
return {
status,
generatedAt: new Date().toISOString(),
durationMs: Date.now() - startedAt,
passCount: checks.filter((check) => check.status === "PASS").length,
warnCount: checks.filter((check) => check.status === "WARN").length,
failCount: checks.filter((check) => check.status === "FAIL").length,
checks,
};
}