107 lines
3.0 KiB
JavaScript
107 lines
3.0 KiB
JavaScript
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()
|
|
})
|