feat(storage): complete phase 3 branding foundation

This commit is contained in:
poyrazavsever
2026-07-16 17:05:59 +03:00
parent b155132acf
commit 5d863280bf
31 changed files with 5302 additions and 57 deletions
+65
View File
@@ -1,3 +1,4 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { ensureDataLayout, getDataConfig } from "./lib/data-dir.mjs";
@@ -18,11 +19,14 @@ const config = ensureDataLayout(getDataConfig(targetEnv));
const backupDir = path.resolve(args.from);
const backupDbPath = path.join(backupDir, "neta.db");
const backupUploadsDir = path.join(backupDir, "uploads");
const manifestPath = path.join(backupDir, "manifest.json");
if (!fs.existsSync(backupDbPath)) {
throw new Error(`Backup database not found: ${backupDbPath}`);
}
verifyManifest(backupDir, manifestPath);
if (fs.existsSync(config.databasePath) && !args.force) {
throw new Error(`Target database exists: ${config.databasePath}. Pass --force to overwrite.`);
}
@@ -70,3 +74,64 @@ function copyDirectory(sourceDir, targetDir) {
}
}
}
function verifyManifest(rootDir, manifestFile) {
if (!fs.existsSync(manifestFile)) {
throw new Error(`Backup manifest not found: ${manifestFile}`);
}
const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
if (!Array.isArray(manifest.files) || manifest.files.length === 0) {
throw new Error("Backup manifest has no file entries.");
}
const normalizedRoot = path.resolve(rootDir);
const verifiedPaths = new Set();
for (const entry of manifest.files) {
if (
!entry ||
typeof entry.path !== "string" ||
typeof entry.bytes !== "number" ||
typeof entry.sha256 !== "string" ||
!/^[0-9a-f]{64}$/i.test(entry.sha256)
) {
throw new Error("Backup manifest contains an invalid file entry.");
}
const filePath = path.resolve(normalizedRoot, entry.path);
if (!filePath.startsWith(`${normalizedRoot}${path.sep}`)) {
throw new Error(`Backup manifest path escapes backup root: ${entry.path}`);
}
const stat = fs.lstatSync(filePath);
if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== entry.bytes) {
throw new Error(`Backup file metadata mismatch: ${entry.path}`);
}
const actualHash = crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(actualHash, "hex"), Buffer.from(entry.sha256, "hex"))) {
throw new Error(`Backup checksum mismatch: ${entry.path}`);
}
verifiedPaths.add(entry.path.replace(/\\/g, "/"));
}
const actualPaths = collectBackupFiles(normalizedRoot, normalizedRoot);
if (
actualPaths.length !== verifiedPaths.size ||
actualPaths.some((filePath) => !verifiedPaths.has(filePath))
) {
throw new Error("Backup contains files that are missing from the checksum manifest.");
}
}
function collectBackupFiles(rootDir, currentDir) {
const files = [];
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
const entryPath = path.join(currentDir, entry.name);
if (entryPath === path.join(rootDir, "manifest.json")) continue;
if (entry.isSymbolicLink()) throw new Error(`Backup contains a symbolic link: ${entry.name}`);
if (entry.isDirectory()) {
files.push(...collectBackupFiles(rootDir, entryPath));
} else if (entry.isFile()) {
files.push(path.relative(rootDir, entryPath).replace(/\\/g, "/"));
}
}
return files;
}