113 lines
3.5 KiB
JavaScript
113 lines
3.5 KiB
JavaScript
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('topic-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()
|
|
})
|