89 lines
3.0 KiB
JavaScript
89 lines
3.0 KiB
JavaScript
import crypto from 'node:crypto'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
export function safeImageExt(filename) {
|
|
const ext = path.extname(String(filename || '')).toLowerCase()
|
|
const allowed = new Set(['.png', '.jpg', '.jpeg', '.webp'])
|
|
return allowed.has(ext) ? ext : null
|
|
}
|
|
|
|
export function uploadError(message, statusCode = 400) {
|
|
const err = new Error(message)
|
|
err.statusCode = statusCode
|
|
return err
|
|
}
|
|
|
|
export async function persistMultipartImages(request, { maxFiles = 10, maxFileBytes, subdir = '', eager = false }) {
|
|
if (!request.isMultipart()) {
|
|
throw uploadError('Ожидается multipart/form-data')
|
|
}
|
|
|
|
const uploadsDir = path.join(process.cwd(), 'uploads')
|
|
const targetDir = subdir ? path.join(uploadsDir, subdir) : uploadsDir
|
|
await fs.promises.mkdir(targetDir, { recursive: true })
|
|
|
|
const urls = []
|
|
const parts = request.parts({
|
|
limits: {
|
|
fileSize: maxFileBytes,
|
|
files: maxFiles,
|
|
},
|
|
})
|
|
for await (const part of parts) {
|
|
if (!part.file) continue
|
|
if (urls.length >= maxFiles) {
|
|
throw uploadError(`Можно загрузить не более ${maxFiles} файл(ов)`)
|
|
}
|
|
const ext = safeImageExt(part.filename)
|
|
if (!ext) {
|
|
throw uploadError('Разрешены только файлы: png, jpg, jpeg, webp')
|
|
}
|
|
|
|
const uuid = crypto.randomUUID()
|
|
const fileName = `${uuid}${ext}`
|
|
const fullPath = path.join(targetDir, fileName)
|
|
await fs.promises.writeFile(fullPath, await part.toBuffer())
|
|
|
|
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) {
|
|
throw uploadError(
|
|
'Файлы не получены. Проверьте, что запрос multipart/form-data и поля — файлы изображений (png, jpg, webp).',
|
|
)
|
|
}
|
|
|
|
return urls
|
|
}
|
|
|
|
/** Сохранить один буфер изображения в uploads/, вернуть путь `/uploads/...`. */
|
|
export async function saveImageBufferToUploads(originalFilename, buffer, subdir = '') {
|
|
const ext = safeImageExt(originalFilename)
|
|
if (!ext) {
|
|
throw uploadError('Разрешены только файлы: png, jpg, jpeg, webp')
|
|
}
|
|
|
|
const uploadsDir = path.join(process.cwd(), 'uploads')
|
|
const targetDir = subdir ? path.join(uploadsDir, subdir) : uploadsDir
|
|
await fs.promises.mkdir(targetDir, { recursive: true })
|
|
|
|
const fileName = `${crypto.randomUUID()}${ext}`
|
|
const fullPath = path.join(targetDir, fileName)
|
|
await fs.promises.writeFile(fullPath, buffer)
|
|
return subdir ? `/uploads/${subdir}/${fileName}` : `/uploads/${fileName}`
|
|
}
|