이미지 최적화 작업 큐 추가

This commit is contained in:
2026-03-31 17:44:03 +09:00
parent 4db1b21ad5
commit 0581de6c17
3 changed files with 191 additions and 22 deletions

View File

@@ -3,10 +3,19 @@ const path = require('path')
const crypto = require('crypto')
const sharp = require('sharp')
const { nanoid } = require('nanoid')
const { findImageAssetByHash, createImageAsset } = require('../db')
const {
findImageAssetByHash,
createImageAsset,
createImageOptimizationJob,
updateImageOptimizationJobStatus,
} = require('../db')
const UPLOAD_ROOT = path.join(__dirname, '..', '..', 'uploads')
const OPTIMIZED_DIR = 'assets'
const OPTIMIZATION_CONCURRENCY = Math.max(1, Number(process.env.IMAGE_OPTIMIZATION_CONCURRENCY || 1))
let activeCount = 0
const pendingJobs = []
function ensureImageMimeType(file) {
return typeof file?.mimetype === 'string' && file.mimetype.startsWith('image/')
@@ -26,26 +35,21 @@ function createMemoryUpload(multer, { fileSize = 6 * 1024 * 1024, maxCount } = {
})
}
async function writeOptimizedImage({
file,
directory,
width,
height,
fit = 'inside',
quality = 82,
}) {
if (!file?.buffer?.length) {
const error = new Error('file_required')
error.code = 'file_required'
throw error
}
if (!ensureImageMimeType(file)) {
const error = new Error('image_file_required')
error.code = 'image_file_required'
throw error
function scheduleQueue() {
while (activeCount < OPTIMIZATION_CONCURRENCY && pendingJobs.length) {
const job = pendingJobs.shift()
activeCount += 1
processQueuedJob(job)
.then(job.resolve)
.catch(job.reject)
.finally(() => {
activeCount = Math.max(0, activeCount - 1)
scheduleQueue()
})
}
}
async function optimizeAndPersist({ file, width, height, fit, quality }) {
const { data, info } = await sharp(file.buffer, { failOn: 'none' })
.rotate()
.resize({
@@ -68,7 +72,6 @@ async function writeOptimizedImage({
height: existing.height,
contentHash: existing.contentHash,
reused: true,
directory,
}
}
@@ -100,7 +103,6 @@ async function writeOptimizedImage({
height: asset.height,
contentHash: asset.contentHash,
reused: false,
directory,
}
} catch (error) {
try {
@@ -120,7 +122,6 @@ async function writeOptimizedImage({
height: asset.height,
contentHash: asset.contentHash,
reused: true,
directory,
}
}
}
@@ -129,8 +130,89 @@ async function writeOptimizedImage({
}
}
async function processQueuedJob(job) {
await updateImageOptimizationJobStatus({
id: job.jobId,
status: 'processing',
startedAt: Date.now(),
})
try {
const result = await optimizeAndPersist(job)
await updateImageOptimizationJobStatus({
id: job.jobId,
status: 'completed',
optimizedByteSize: result.size,
reusedAsset: result.reused,
finishedAt: Date.now(),
})
return result
} catch (error) {
await updateImageOptimizationJobStatus({
id: job.jobId,
status: 'failed',
errorMessage: error?.message || 'optimization_failed',
finishedAt: Date.now(),
})
throw error
}
}
async function writeOptimizedImage({
file,
directory,
width,
height,
fit = 'inside',
quality = 82,
}) {
if (!file?.buffer?.length) {
const error = new Error('file_required')
error.code = 'file_required'
throw error
}
if (!ensureImageMimeType(file)) {
const error = new Error('image_file_required')
error.code = 'image_file_required'
throw error
}
const jobId = nanoid()
await createImageOptimizationJob({
id: jobId,
sourceCategory: directory,
targetDirectory: OPTIMIZED_DIR,
originalByteSize: file.size || file.buffer.length,
})
return new Promise((resolve, reject) => {
pendingJobs.push({
jobId,
file,
directory,
width,
height,
fit,
quality,
resolve: (result) => resolve({ ...result, directory }),
reject,
})
scheduleQueue()
})
}
function getImageOptimizationQueueState() {
return {
concurrency: OPTIMIZATION_CONCURRENCY,
activeCount,
pendingCount: pendingJobs.length,
}
}
module.exports = {
createMemoryUpload,
ensureImageMimeType,
writeOptimizedImage,
getImageOptimizationQueueState,
}