const fs = require('fs/promises') const path = require('path') const sharp = require('sharp') const { ensureData, closePool, listReferencedUploadUsage, replaceUploadSourceReferences, } = require('../src/db') const { writeOptimizedImage } = require('../src/lib/image-storage') 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' } function getOptimizationConfig(roles) { const roleSet = new Set(roles || []) if (roleSet.has('avatar')) { return { directory: 'avatars', width: 512, height: 512, fit: 'cover', quality: 82 } } if (roleSet.has('game-thumbnail') || roleSet.has('tierlist-thumbnail') || roleSet.has('template-thumbnail')) { return { directory: 'legacy-thumbnails', width: 1280, height: 1280, fit: 'inside', quality: 84 } } return { directory: 'legacy-items', width: 512, height: 512, fit: 'inside', quality: 84 } } async function createFileLike(src) { const absolutePath = path.join(BACKEND_ROOT, src.replace(/^\//, '')) 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 = {} } return { file: { originalname: path.basename(src), mimetype: inferMimeType(src, metadata), size: Number(stat.size || 0), buffer, }, absolutePath, } } async function main() { await ensureData() const usageEntries = await listReferencedUploadUsage() const legacyEntries = usageEntries.filter((entry) => entry.src && entry.src.startsWith('/uploads/') && !entry.src.startsWith('/uploads/assets/')) const summary = { scanned: legacyEntries.length, migrated: 0, reusedAsset: 0, unchanged: 0, missingFiles: 0, failed: 0, updatedRows: 0, } for (const entry of legacyEntries) { const config = getOptimizationConfig(entry.roles) try { const { file } = await createFileLike(entry.src) const optimized = await writeOptimizedImage({ file, ...config }) if (optimized.src === entry.src) { summary.unchanged += 1 continue } const replaced = await replaceUploadSourceReferences({ fromSrc: entry.src, toSrc: optimized.src }) summary.updatedRows += Number(replaced.updatedRows || 0) if (optimized.reused) summary.reusedAsset += 1 else summary.migrated += 1 } catch (error) { if (error?.code === 'ENOENT') { summary.missingFiles += 1 continue } summary.failed += 1 console.error('[migrate-legacy-uploads-to-assets] failed:', entry.src, error?.message || error) } } console.log(JSON.stringify(summary, null, 2)) } main() .catch((error) => { console.error(error) process.exitCode = 1 }) .finally(async () => { await closePool() })