const fs = require('fs/promises') const path = require('path') const crypto = require('crypto') const sharp = require('sharp') const { nanoid } = require('nanoid') const { ensureData, closePool, listReferencedUploadSources, findImageAssetBySrc, createImageAsset, } = require('../src/db') const BACKEND_ROOT = path.join(__dirname, '..') function inferMimeType(src, metadata) { const format = String(metadata?.format || '').toLowerCase() if (format === 'jpeg' || format === 'jpg') return 'image/jpeg' if (format === 'png') return 'image/png' if (format === 'gif') return 'image/gif' if (format === 'webp') return 'image/webp' if (format === 'svg' || format === 'svg+xml') return 'image/svg+xml' if (format === 'avif') return 'image/avif' const ext = path.extname(src || '').toLowerCase() if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg' if (ext === '.png') return 'image/png' if (ext === '.gif') return 'image/gif' if (ext === '.webp') return 'image/webp' if (ext === '.svg') return 'image/svg+xml' if (ext === '.avif') return 'image/avif' return 'application/octet-stream' } async function main() { await ensureData() const referencedSrcs = Array.from(new Set(await listReferencedUploadSources())) .filter((src) => typeof src === 'string' && src.startsWith('/uploads/')) .sort() const summary = { scanned: referencedSrcs.length, skippedExisting: 0, backfilled: 0, missingFiles: 0, failed: 0, } for (const src of referencedSrcs) { const existing = await findImageAssetBySrc(src) if (existing) { summary.skippedExisting += 1 continue } const absolutePath = path.join(BACKEND_ROOT, src.replace(/^\//, '')) try { const [buffer, stat] = await Promise.all([fs.readFile(absolutePath), fs.stat(absolutePath)]) let metadata = {} try { metadata = await sharp(buffer, { failOn: 'none' }).metadata() } catch (error) { metadata = {} } const rawHash = crypto.createHash('sha256').update(buffer).digest('hex') const contentHash = crypto.createHash('sha256').update(`${rawHash}|${src}`).digest('hex') await createImageAsset({ id: nanoid(), contentHash, src, mimeType: inferMimeType(src, metadata), byteSize: Number(stat.size || 0), originalByteSize: Number(stat.size || 0), width: Number(metadata.width || 0), height: Number(metadata.height || 0), }) summary.backfilled += 1 } catch (error) { if (error?.code === 'ENOENT') { summary.missingFiles += 1 continue } if (error?.code === 'ER_DUP_ENTRY') { summary.skippedExisting += 1 continue } summary.failed += 1 console.error('[backfill-legacy-image-assets] failed:', src, error?.message || error) } } console.log(JSON.stringify(summary, null, 2)) } main() .catch((error) => { console.error(error) process.exitCode = 1 }) .finally(async () => { await closePool() })