45 lines
2.4 KiB
TypeScript
45 lines
2.4 KiB
TypeScript
import "dotenv/config";
|
|
import { PrismaClient } from "@prisma/client";
|
|
import path from "node:path";
|
|
import { readFile, realpath } from "node:fs/promises";
|
|
import { createHash } from "node:crypto";
|
|
import { fileTypeFromBuffer } from "file-type";
|
|
import { saveFile, removeFile, storagePath, MAX_UPLOAD } from "../src/lib/storage";
|
|
const db = new PrismaClient();
|
|
const sha = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
|
|
async function main() {
|
|
const root = path.resolve(process.env.LEGACY_UPLOAD_DIR || "public/uploads");
|
|
const files = await db.file.findMany();
|
|
async function source(file: typeof files[number]) {
|
|
if (!file.path.startsWith("/uploads/")) return storagePath(file.path);
|
|
const base = await realpath(root);
|
|
const resolved = await realpath(path.resolve(base, file.path.slice("/uploads/".length)));
|
|
if (!resolved.startsWith(base + path.sep)) throw new Error(`Anhang ${file.id}: Pfad außerhalb des Upload-Verzeichnisses`);
|
|
return resolved;
|
|
}
|
|
// Validate every referenced file before converting any paths. Never silently skip data.
|
|
for (const file of files) {
|
|
const bytes = await readFile(await source(file));
|
|
if (bytes.length !== file.size) throw new Error(`Anhang ${file.id}: Dateigröße stimmt nicht mit der Datenbank überein`);
|
|
const type = await fileTypeFromBuffer(bytes);
|
|
if (!bytes.length || bytes.length > MAX_UPLOAD || !type || !["pdf", "jpg", "png", "docx", "xlsx"].includes(type.ext))
|
|
throw new Error(`Anhang ${file.id}: Inhalt oder Größe nicht zulässig; Original bleibt erhalten`);
|
|
}
|
|
let migrated = 0;
|
|
for (const file of files.filter(f => f.path.startsWith("/uploads/"))) {
|
|
const bytes = await readFile(await source(file));
|
|
const saved = await saveFile(bytes);
|
|
try {
|
|
if (sha(await readFile(storagePath(saved.key))) !== sha(bytes)) throw new Error(`Anhang ${file.id}: Prüfsummenabweichung`);
|
|
await db.file.update({ where: { id: file.id }, data: { path: saved.key, mimeType: saved.mime } });
|
|
} catch (e) {
|
|
await removeFile(saved.key);
|
|
throw e;
|
|
}
|
|
// Keep originals for recovery. The volume is private and never served statically.
|
|
migrated++;
|
|
}
|
|
console.log(`${files.length} Anhänge geprüft, ${migrated} migriert. Originaldateien bleiben erhalten; öffentliche /uploads/-Auslieferung sperren.`);
|
|
}
|
|
main().catch(error => { console.error(error); process.exitCode = 1; }).finally(() => db.$disconnect());
|