미사용 이미지 자산 정리 배치 추가
This commit is contained in:
@@ -28,6 +28,14 @@ function serializeJson(value) {
|
||||
return JSON.stringify(value || [])
|
||||
}
|
||||
|
||||
function collectUploadSrcsFromItems(items, bucket) {
|
||||
for (const item of items || []) {
|
||||
if (typeof item?.src === 'string' && item.src.startsWith('/uploads/')) {
|
||||
bucket.add(item.src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapUserRow(row) {
|
||||
if (!row) return null
|
||||
return {
|
||||
@@ -644,6 +652,59 @@ async function listRecentImageOptimizationJobs(limit = 20) {
|
||||
)
|
||||
return rows.map(mapImageOptimizationJobRow)
|
||||
}
|
||||
|
||||
async function listUnusedImageAssets({ limit = 100, minAgeHours = 24 } = {}) {
|
||||
const safeLimit = Math.max(1, Math.min(500, Number(limit) || 100))
|
||||
const safeMinAgeHours = Math.max(0, Number(minAgeHours) || 24)
|
||||
const cutoff = now() - safeMinAgeHours * 60 * 60 * 1000
|
||||
|
||||
const assets = (await query(
|
||||
`SELECT id, content_hash, src, mime_type, byte_size, original_byte_size, width, height, created_at FROM image_assets WHERE created_at <= ? ORDER BY created_at ASC LIMIT ${safeLimit}`,
|
||||
[cutoff]
|
||||
)).map(mapImageAssetRow)
|
||||
|
||||
if (!assets.length) return []
|
||||
|
||||
const referencedSrcs = new Set()
|
||||
|
||||
const [userRows, gameRows, gameItemRows, customItemRows, tierListRows, templateRequestRows] = await Promise.all([
|
||||
query("SELECT avatar_src FROM users WHERE avatar_src <> ''"),
|
||||
query("SELECT thumbnail_src FROM games WHERE thumbnail_src <> ''"),
|
||||
query("SELECT src FROM game_items WHERE src <> ''"),
|
||||
query("SELECT src FROM custom_items WHERE src <> ''"),
|
||||
query("SELECT thumbnail_src, pool_json FROM tierlists"),
|
||||
query("SELECT thumbnail_src_snapshot, items_json FROM template_requests"),
|
||||
])
|
||||
|
||||
for (const row of userRows) if (row.avatar_src) referencedSrcs.add(row.avatar_src)
|
||||
for (const row of gameRows) if (row.thumbnail_src) referencedSrcs.add(row.thumbnail_src)
|
||||
for (const row of gameItemRows) if (row.src) referencedSrcs.add(row.src)
|
||||
for (const row of customItemRows) if (row.src) referencedSrcs.add(row.src)
|
||||
|
||||
for (const row of tierListRows) {
|
||||
if (row.thumbnail_src) referencedSrcs.add(row.thumbnail_src)
|
||||
collectUploadSrcsFromItems(parseJson(row.pool_json, []), referencedSrcs)
|
||||
}
|
||||
|
||||
for (const row of templateRequestRows) {
|
||||
if (row.thumbnail_src_snapshot) referencedSrcs.add(row.thumbnail_src_snapshot)
|
||||
collectUploadSrcsFromItems(parseJson(row.items_json, []), referencedSrcs)
|
||||
}
|
||||
|
||||
return assets.filter((asset) => !referencedSrcs.has(asset.src))
|
||||
}
|
||||
|
||||
async function deleteImageAssets(ids) {
|
||||
const uniqueIds = Array.from(new Set((ids || []).filter(Boolean)))
|
||||
if (!uniqueIds.length) return []
|
||||
const placeholders = uniqueIds.map(() => '?').join(', ')
|
||||
const rows = await query(
|
||||
`SELECT id, content_hash, src, mime_type, byte_size, original_byte_size, width, height, created_at FROM image_assets WHERE id IN (${placeholders})`,
|
||||
uniqueIds
|
||||
)
|
||||
await query(`DELETE FROM image_assets WHERE id IN (${placeholders})`, uniqueIds)
|
||||
return rows.map(mapImageAssetRow)
|
||||
}
|
||||
async function createGameItem({ id, gameId, src, label }) {
|
||||
const createdAt = now()
|
||||
await query('INSERT INTO game_items (id, game_id, src, label, created_at) VALUES (?, ?, ?, ?, ?)', [
|
||||
@@ -1532,6 +1593,8 @@ module.exports = {
|
||||
findImageOptimizationJobById,
|
||||
updateImageOptimizationJobStatus,
|
||||
listRecentImageOptimizationJobs,
|
||||
listUnusedImageAssets,
|
||||
deleteImageAssets,
|
||||
createGameItem,
|
||||
updateGameItemLabel,
|
||||
deleteGameItem,
|
||||
|
||||
Reference in New Issue
Block a user