feat: add eager mode to persistMultipartImages
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { persistMultipartImages } from '../upload-images.js'
|
||||||
|
|
||||||
|
const UPLOADS_DIR = path.join(process.cwd(), 'uploads')
|
||||||
|
const TEST_PREFIX = 'upload-test-'
|
||||||
|
|
||||||
|
describe('persistMultipartImages with eager mode', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
const files = await fs.promises.readdir(UPLOADS_DIR).catch(() => [])
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.startsWith(TEST_PREFIX)) {
|
||||||
|
await fs.promises.unlink(path.join(UPLOADS_DIR, file)).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cacheDir = path.join(UPLOADS_DIR, '.cache')
|
||||||
|
await fs.promises.rm(cacheDir, { recursive: true, force: true }).catch(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns WebP URLs when eager=true', async () => {
|
||||||
|
const sharp = (await import('sharp')).default
|
||||||
|
const testImagePath = path.join(UPLOADS_DIR, `${TEST_PREFIX}original.png`)
|
||||||
|
await sharp({ create: { width: 100, height: 100, channels: 3, background: { r: 255, g: 0, b: 0 } } })
|
||||||
|
.png()
|
||||||
|
.toFile(testImagePath)
|
||||||
|
|
||||||
|
const filesBefore = await fs.promises.readdir(UPLOADS_DIR)
|
||||||
|
|
||||||
|
const mockRequest = {
|
||||||
|
isMultipart: () => true,
|
||||||
|
parts: async function* () {
|
||||||
|
const buffer = await fs.promises.readFile(testImagePath)
|
||||||
|
yield {
|
||||||
|
file: true,
|
||||||
|
filename: 'test.png',
|
||||||
|
toBuffer: async () => buffer,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const urls = await persistMultipartImages(mockRequest, {
|
||||||
|
maxFiles: 1,
|
||||||
|
maxFileBytes: 20 * 1024 * 1024,
|
||||||
|
subdir: '',
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(urls).toHaveLength(1)
|
||||||
|
expect(urls[0]).toMatch(/\/uploads\/[a-f0-9-]+\.webp$/)
|
||||||
|
|
||||||
|
// Verify the intermediate PNG file written by persistMultipartImages was deleted
|
||||||
|
const filesAfter = await fs.promises.readdir(UPLOADS_DIR)
|
||||||
|
const newPngFiles = filesAfter.filter((f) => !filesBefore.includes(f) && f.endsWith('.png'))
|
||||||
|
expect(newPngFiles).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns original format URLs when eager=false', async () => {
|
||||||
|
const sharp = (await import('sharp')).default
|
||||||
|
const testImagePath = path.join(UPLOADS_DIR, `${TEST_PREFIX}original2.png`)
|
||||||
|
await sharp({ create: { width: 100, height: 100, channels: 3, background: { r: 0, g: 255, b: 0 } } })
|
||||||
|
.png()
|
||||||
|
.toFile(testImagePath)
|
||||||
|
|
||||||
|
const mockRequest = {
|
||||||
|
isMultipart: () => true,
|
||||||
|
parts: async function* () {
|
||||||
|
const buffer = await fs.promises.readFile(testImagePath)
|
||||||
|
yield {
|
||||||
|
file: true,
|
||||||
|
filename: 'test.png',
|
||||||
|
toBuffer: async () => buffer,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const urls = await persistMultipartImages(mockRequest, {
|
||||||
|
maxFiles: 1,
|
||||||
|
maxFileBytes: 20 * 1024 * 1024,
|
||||||
|
subdir: '',
|
||||||
|
eager: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(urls).toHaveLength(1)
|
||||||
|
expect(urls[0]).toMatch(/\/uploads\/[a-f0-9-]+\.png$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cleans up original file on eager processing error', async () => {
|
||||||
|
const invalidBuffer = Buffer.from('not an image')
|
||||||
|
|
||||||
|
const filesBefore = await fs.promises.readdir(UPLOADS_DIR)
|
||||||
|
|
||||||
|
const mockRequest = {
|
||||||
|
isMultipart: () => true,
|
||||||
|
parts: async function* () {
|
||||||
|
yield {
|
||||||
|
file: true,
|
||||||
|
filename: 'test.png',
|
||||||
|
toBuffer: async () => invalidBuffer,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
persistMultipartImages(mockRequest, {
|
||||||
|
maxFiles: 1,
|
||||||
|
maxFileBytes: 20 * 1024 * 1024,
|
||||||
|
subdir: '',
|
||||||
|
eager: true,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow()
|
||||||
|
|
||||||
|
// The intermediate file written by persistMultipartImages should be cleaned up
|
||||||
|
const filesAfter = await fs.promises.readdir(UPLOADS_DIR)
|
||||||
|
const newFiles = filesAfter.filter((f) => !filesBefore.includes(f) && f !== '.cache')
|
||||||
|
expect(newFiles).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -14,7 +14,7 @@ export function uploadError(message, statusCode = 400) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function persistMultipartImages(request, { maxFiles = 10, maxFileBytes, subdir = '' }) {
|
export async function persistMultipartImages(request, { maxFiles = 10, maxFileBytes, subdir = '', eager = false }) {
|
||||||
if (!request.isMultipart()) {
|
if (!request.isMultipart()) {
|
||||||
throw uploadError('Ожидается multipart/form-data')
|
throw uploadError('Ожидается multipart/form-data')
|
||||||
}
|
}
|
||||||
@@ -40,10 +40,25 @@ export async function persistMultipartImages(request, { maxFiles = 10, maxFileBy
|
|||||||
throw uploadError('Разрешены только файлы: png, jpg, jpeg, webp')
|
throw uploadError('Разрешены только файлы: png, jpg, jpeg, webp')
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = `${crypto.randomUUID()}${ext}`
|
const uuid = crypto.randomUUID()
|
||||||
|
const fileName = `${uuid}${ext}`
|
||||||
const fullPath = path.join(targetDir, fileName)
|
const fullPath = path.join(targetDir, fileName)
|
||||||
await fs.promises.writeFile(fullPath, await part.toBuffer())
|
await fs.promises.writeFile(fullPath, await part.toBuffer())
|
||||||
urls.push(subdir ? `/uploads/${subdir}/${fileName}` : `/uploads/${fileName}`)
|
|
||||||
|
let finalUrl = subdir ? `/uploads/${subdir}/${fileName}` : `/uploads/${fileName}`
|
||||||
|
|
||||||
|
if (eager) {
|
||||||
|
try {
|
||||||
|
const { generateAllSizes, convertOriginalToWebp } = await import('./image-resize.js')
|
||||||
|
await generateAllSizes(uuid, subdir, fullPath)
|
||||||
|
finalUrl = await convertOriginalToWebp(uuid, subdir)
|
||||||
|
} catch (error) {
|
||||||
|
await fs.promises.unlink(fullPath).catch(() => {})
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urls.push(finalUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urls.length === 0) {
|
if (urls.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user