108 lines
4.0 KiB
JavaScript
108 lines
4.0 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* Checks whether the Prisma client has the EventAttachment model and the table is queryable.
|
|
* @param {import('@prisma/client').PrismaClient} prisma
|
|
*/
|
|
async function canUseEventAttachment(prisma) {
|
|
const hasModel = !!(prisma && prisma.eventAttachment && typeof prisma.eventAttachment.findMany === 'function');
|
|
if (!hasModel) return { ok: false, reason: 'NO_MODEL' };
|
|
try {
|
|
await prisma.eventAttachment.findFirst({});
|
|
return { ok: true };
|
|
} catch (e) {
|
|
return { ok: false, reason: 'QUERY_ERROR', error: String(e?.message || e) };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads all manifest files for event attachments and inserts missing rows into DB.
|
|
* It is idempotent: it will skip entries that already exist by id. If id lookup fails,
|
|
* it tries to avoid duplicates using a composite check (eventId + filename + size).
|
|
*
|
|
* @param {import('@prisma/client').PrismaClient} prisma
|
|
* @param {{ dryRun?: boolean, removeManifestAfterImport?: boolean }} [options]
|
|
* @returns {Promise<{processedFiles:number, imported:number, skipped:number, errors:Array<{file:string,error:string}>, details:Array<{file:string, imported:number, skipped:number}>}>}
|
|
*/
|
|
async function syncManifestsToDb(prisma, options = {}) {
|
|
const { dryRun = false, removeManifestAfterImport = false } = options;
|
|
const baseDir = path.join(__dirname, '..', '..', 'public', 'uploads', 'event-files');
|
|
const result = { processedFiles: 0, imported: 0, skipped: 0, errors: [], details: [] };
|
|
|
|
const check = await canUseEventAttachment(prisma);
|
|
if (!check.ok) {
|
|
return { ...result, errors: [{ file: '*', error: `Attachments model not usable (${check.reason})${check.error ? ': ' + check.error : ''}` }] };
|
|
}
|
|
|
|
if (!fs.existsSync(baseDir)) return result;
|
|
const files = fs.readdirSync(baseDir).filter(f => f.endsWith('.attachments.json'));
|
|
|
|
for (const file of files) {
|
|
const manifestPath = path.join(baseDir, file);
|
|
result.processedFiles += 1;
|
|
let list = [];
|
|
try {
|
|
const raw = fs.readFileSync(manifestPath, 'utf-8');
|
|
list = JSON.parse(raw) || [];
|
|
} catch (e) {
|
|
result.errors.push({ file, error: 'Failed to parse JSON: ' + String(e?.message || e) });
|
|
continue;
|
|
}
|
|
|
|
let imported = 0;
|
|
let skipped = 0;
|
|
|
|
for (const entry of list) {
|
|
try {
|
|
// Check existing by id first
|
|
const existsById = entry?.id ? await prisma.eventAttachment.findUnique({ where: { id: entry.id } }) : null;
|
|
if (existsById) { skipped++; continue; }
|
|
|
|
// Check using composite heuristic to avoid duplicates
|
|
const maybeExisting = await prisma.eventAttachment.findFirst({
|
|
where: {
|
|
eventId: entry.eventId,
|
|
filename: entry.filename,
|
|
size: typeof entry.size === 'number' ? entry.size : undefined,
|
|
}
|
|
});
|
|
if (maybeExisting) { skipped++; continue; }
|
|
|
|
if (!dryRun) {
|
|
await prisma.eventAttachment.create({
|
|
data: {
|
|
id: entry.id || undefined,
|
|
eventId: entry.eventId,
|
|
originalName: entry.originalName || entry.filename || 'file',
|
|
filename: entry.filename,
|
|
mimeType: entry.mimeType || 'application/octet-stream',
|
|
size: typeof entry.size === 'number' ? entry.size : 0,
|
|
url: entry.url,
|
|
createdAt: entry.createdAt ? new Date(entry.createdAt) : undefined,
|
|
}
|
|
});
|
|
}
|
|
imported++;
|
|
} catch (e) {
|
|
result.errors.push({ file, error: 'Insert failed: ' + String(e?.message || e) });
|
|
}
|
|
}
|
|
|
|
result.imported += imported;
|
|
result.skipped += skipped;
|
|
result.details.push({ file, imported, skipped });
|
|
|
|
// Optionally remove manifest if all entries are in DB now
|
|
if (!dryRun && removeManifestAfterImport && imported > 0) {
|
|
try {
|
|
fs.unlinkSync(manifestPath);
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
module.exports = { syncManifestsToDb, canUseEventAttachment };
|