const fs = require('fs'); const path = require('path'); const { uploadTemplateFile, uploadTemplateAsset, templateAssetsBaseUrl, } = require('../../src/utils/s3MicrositeV2'); const { parseTemplateSchema } = require('../../src/utils/templateSchema'); require('../../src/models/Organization'); require('../../src/models/User'); const MicrositeV2Template = require('../../src/models/MicrositeV2Template'); const { getGlobalMicrositeV2Template } = require('../../src/config/globalMicrositeV2Templates'); const BACKEND_ROOT = path.join(__dirname, '..', '..'); const TEMPLATES_ROOT = path.join(BACKEND_ROOT, 'microsite-v2-templates'); function walkDir(dir, base = dir) { const files = []; if (!fs.existsSync(dir)) return files; for (const name of fs.readdirSync(dir)) { const full = path.join(dir, name); const stat = fs.statSync(full); if (stat.isDirectory()) { files.push(...walkDir(full, base)); } else { files.push({ full, rel: path.relative(base, full).replace(/\\/g, '/') }); } } return files; } function mimeFor(filePath) { if (filePath.endsWith('.css')) return 'text/css'; if (filePath.endsWith('.js')) return 'application/javascript'; if (filePath.endsWith('.woff')) return 'font/woff'; if (filePath.endsWith('.woff2')) return 'font/woff2'; if (filePath.endsWith('.ttf')) return 'font/ttf'; if (filePath.endsWith('.eot')) return 'application/vnd.ms-fontobject'; if (filePath.endsWith('.png')) return 'image/png'; if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) return 'image/jpeg'; if (filePath.endsWith('.svg')) return 'image/svg+xml'; if (filePath.endsWith('.gif')) return 'image/gif'; if (filePath.endsWith('.webp')) return 'image/webp'; return 'application/octet-stream'; } /** * Theme packs often bake cache-busters into filenames (LineIcons.woff?y2l643, * fontawesome-webfont.eot?v=4.7.0). Strip those so S3 keys match clean @font-face URLs. */ function normalizeAssetRelPath(rel) { const base = String(rel).replace(/\\/g, '/'); // Strip query suffix from any path segment (literal ? in filename or ?query). return base.replace(/(\.(?:woff2?|ttf|eot|svg|otf))\?[^/]*$/i, '$1'); } function svgPlaceholder(label, color) { const safeLabel = String(label || 'Template').slice(0, 40); const fill = String(color || '#5c6ac4'); return Buffer.from( `${safeLabel}`, ); } /** * Upload template files to S3 and upsert MicrositeV2Template row. * @param {{ slug: string, user: import('mongoose').Document, dbOnly?: boolean }} opts */ async function seedOneMicrositeV2Template({ slug, user, dbOnly = false }) { const def = getGlobalMicrositeV2Template(slug); if (!def) { throw new Error(`Unknown global microsite template slug: ${slug}`); } const templateDir = path.join(TEMPLATES_ROOT, def.slug); const templateAssets = path.join(templateDir, 'assets'); const htmlPath = path.join(templateDir, 'template.html'); const jsonPath = path.join(templateDir, 'template.json'); if (!fs.existsSync(htmlPath) || !fs.existsSync(jsonPath)) { throw new Error(`Missing template.html or template.json in ${templateDir}`); } const htmlBuf = fs.readFileSync(htmlPath); const jsonBuf = fs.readFileSync(jsonPath); const schema = parseTemplateSchema(jsonBuf); let htmlS3Key; let schemaS3Key; let thumbnailUrl; let assetsBaseUrl = templateAssetsBaseUrl(def.slug); if (!dbOnly && process.env.AWS_BUCKET) { console.log(`[${def.slug}] Uploading template.html...`); const htmlUp = await uploadTemplateFile(def.slug, 'template.html', htmlBuf, 'text/html; charset=utf-8'); htmlS3Key = htmlUp.s3Key; console.log(`[${def.slug}] Uploading template.json...`); const jsonUp = await uploadTemplateFile(def.slug, 'template.json', jsonBuf, 'application/json'); schemaS3Key = jsonUp.s3Key; for (const rel of def.thumbnailCandidates || []) { const tp = path.join(templateDir, rel); if (!fs.existsSync(tp)) continue; const ext = path.extname(tp).slice(1) || 'jpg'; const thumbUp = await uploadTemplateFile( def.slug, `thumbnail.${ext}`, fs.readFileSync(tp), `image/${ext === 'jpg' ? 'jpeg' : ext}`, ); thumbnailUrl = thumbUp.url; break; } if (!thumbnailUrl) { const thumbUp = await uploadTemplateFile( def.slug, 'thumbnail.svg', svgPlaceholder(def.placeholderSvgLabel || def.name, def.placeholderSvgColor), 'image/svg+xml', ); thumbnailUrl = thumbUp.url; } const assetRoots = []; if (fs.existsSync(templateAssets)) assetRoots.push(templateAssets); for (const externalRel of def.externalAssetRoots || []) { const externalRoot = path.isAbsolute(externalRel) ? externalRel : path.join(BACKEND_ROOT, '..', externalRel); if (fs.existsSync(externalRoot)) assetRoots.push(externalRoot); } for (const root of assetRoots) { console.log(`[${def.slug}] Uploading assets from ${root}...`); for (const { full, rel } of walkDir(root)) { const normalizedRel = normalizeAssetRelPath(rel); if (normalizedRel !== rel && fs.existsSync(path.join(root, normalizedRel))) { continue; } await uploadTemplateAsset(def.slug, normalizedRel, fs.readFileSync(full), mimeFor(normalizedRel)); } } assetsBaseUrl = templateAssetsBaseUrl(def.slug); console.log(`[${def.slug}] S3 upload complete.`); } else { console.log(`[${def.slug}] Skipping S3 upload (--db-only or AWS_BUCKET not set).`); } const existing = await MicrositeV2Template.findOne({ slug: def.slug }).select('_id htmlS3Key schemaS3Key thumbnailUrl').lean(); const doc = { slug: def.slug, name: def.name, description: def.description, thumbnailUrl: thumbnailUrl || existing?.thumbnailUrl || '', htmlS3Key: htmlS3Key || existing?.htmlS3Key || '', schemaS3Key: schemaS3Key || existing?.schemaS3Key || '', schema, assetsBaseUrl, assetsS3Prefix: `templates/${def.slug}/assets`, isActive: Boolean((htmlS3Key || existing?.htmlS3Key) && (schemaS3Key || existing?.schemaS3Key)), version: schema.version || 1, createdBy: user._id, }; const now = new Date(); const setFields = { ...doc, updatedAt: now, }; await MicrositeV2Template.collection.updateOne( { slug: def.slug }, { $set: setFields, $setOnInsert: { allowedOrganizations: [], visibility: 'public', createdAt: now, }, }, { upsert: true }, ); const saved = await MicrositeV2Template.findOne({ slug: def.slug }).select('_id slug').lean(); console.log( `[${def.slug}] ${existing ? 'Updated' : 'Created'} MicrositeV2Template: ${saved?._id}`, ); return saved; } module.exports = { seedOneMicrositeV2Template, };