fix: 明确提示超大 PNG 像素限制
This commit is contained in:
@@ -90,7 +90,7 @@ const formatBytes = (bytes: number) => bytes < 1024 * 1024 ? `${Math.ceil(bytes
|
|||||||
<div v-if="loading" class="admin-loading">正在加载素材…</div>
|
<div v-if="loading" class="admin-loading">正在加载素材…</div>
|
||||||
<div v-else class="media-admin-body">
|
<div v-else class="media-admin-body">
|
||||||
<section v-if="view === 'active'" class="media-upload-panel editor-panel">
|
<section v-if="view === 'active'" class="media-upload-panel editor-panel">
|
||||||
<div><PhUploadSimple :size="30" /><strong>上传新图片</strong><small>支持 JPG、PNG、WebP;最大 8MB、4000 万像素</small></div>
|
<div><PhUploadSimple :size="30" /><strong>上传新图片</strong><small>支持 JPG、PNG、WebP;最大 8MB、4000 万像素。Logo 建议使用透明 PNG,宽度 600–1600px</small></div>
|
||||||
<div class="media-upload-meta"><input v-model="uploadMeta.altText" placeholder="ALT 描述(建议填写)"><input v-model="uploadMeta.source" placeholder="素材来源"><input v-model="uploadMeta.copyright" placeholder="版权/授权说明"><select v-model="uploadMeta.authorizationStatus"><option value="pending">待核验</option><option value="authorized">已授权</option><option value="restricted">限制使用</option></select></div>
|
<div class="media-upload-meta"><input v-model="uploadMeta.altText" placeholder="ALT 描述(建议填写)"><input v-model="uploadMeta.source" placeholder="素材来源"><input v-model="uploadMeta.copyright" placeholder="版权/授权说明"><select v-model="uploadMeta.authorizationStatus"><option value="pending">待核验</option><option value="authorized">已授权</option><option value="restricted">限制使用</option></select></div>
|
||||||
<label class="media-upload-button"><input type="file" accept="image/jpeg,image/png,image/webp" :disabled="busy" @change="upload">{{ busy ? '处理中…' : '选择并上传图片' }}</label>
|
<label class="media-upload-button"><input type="file" accept="image/jpeg,image/png,image/webp" :disabled="busy" @change="upload">{{ busy ? '处理中…' : '选择并上传图片' }}</label>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { mkdir, rm, writeFile } from 'node:fs/promises'
|
|||||||
import { extname } from 'node:path'
|
import { extname } from 'node:path'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { MAX_IMAGE_BYTES, MAX_IMAGE_HEIGHT, MAX_IMAGE_PIXELS, MAX_IMAGE_WIDTH, isSharpPixelLimitError } from '../../utils/image-validation'
|
||||||
import { mediaUrl, safeUploadPath, serializeMedia, type MediaAssetRow } from '../../utils/media'
|
import { mediaUrl, safeUploadPath, serializeMedia, type MediaAssetRow } from '../../utils/media'
|
||||||
import { useDatabase } from '../../utils/db'
|
import { useDatabase } from '../../utils/db'
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const parts = await readMultipartFormData(event)
|
const parts = await readMultipartFormData(event)
|
||||||
const file = parts?.find(part => part.name === 'file' && part.filename)
|
const file = parts?.find(part => part.name === 'file' && part.filename)
|
||||||
if (!file?.data?.length) throw createError({ statusCode: 400, statusMessage: '请选择图片文件' })
|
if (!file?.data?.length) throw createError({ statusCode: 400, statusMessage: '请选择图片文件' })
|
||||||
if (file.data.length > 8 * 1024 * 1024) throw createError({ statusCode: 413, statusMessage: '单张图片不能超过 8MB' })
|
if (file.data.length > MAX_IMAGE_BYTES) throw createError({ statusCode: 413, statusMessage: '单张图片不能超过 8MB' })
|
||||||
|
|
||||||
const detectedType = detectImageType(file.data)
|
const detectedType = detectImageType(file.data)
|
||||||
if (!detectedType || !(detectedType in allowedTypes)) throw createError({ statusCode: 415, statusMessage: '仅支持 JPG、PNG、WebP 图片' })
|
if (!detectedType || !(detectedType in allowedTypes)) throw createError({ statusCode: 415, statusMessage: '仅支持 JPG、PNG、WebP 图片' })
|
||||||
@@ -39,11 +40,16 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (!typeConfig.inputExtensions.includes(inputExtension as never)) throw createError({ statusCode: 415, statusMessage: '文件扩展名与实际图片类型不一致' })
|
if (!typeConfig.inputExtensions.includes(inputExtension as never)) throw createError({ statusCode: 415, statusMessage: '文件扩展名与实际图片类型不一致' })
|
||||||
|
|
||||||
let imageMetadata
|
let imageMetadata
|
||||||
try { imageMetadata = await sharp(file.data, { limitInputPixels: 40_000_000 }).metadata() }
|
try { imageMetadata = await sharp(file.data, { limitInputPixels: MAX_IMAGE_PIXELS }).metadata() }
|
||||||
catch { throw createError({ statusCode: 415, statusMessage: '图片内容损坏或无法解析' }) }
|
catch (error) {
|
||||||
|
if (isSharpPixelLimitError(error)) {
|
||||||
|
throw createError({ statusCode: 413, statusMessage: '图片像素过大,请压缩至 10000×10000 且不超过 4000 万像素后上传' })
|
||||||
|
}
|
||||||
|
throw createError({ statusCode: 415, statusMessage: '图片内容损坏或无法解析' })
|
||||||
|
}
|
||||||
const width = imageMetadata.width || 0
|
const width = imageMetadata.width || 0
|
||||||
const height = imageMetadata.height || 0
|
const height = imageMetadata.height || 0
|
||||||
if (!width || !height || width > 10_000 || height > 10_000 || width * height > 40_000_000) {
|
if (!width || !height || width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT || width * height > MAX_IMAGE_PIXELS) {
|
||||||
throw createError({ statusCode: 413, statusMessage: '图片尺寸不能超过 10000×10000 或 4000 万像素' })
|
throw createError({ statusCode: 413, statusMessage: '图片尺寸不能超过 10000×10000 或 4000 万像素' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
server/utils/image-validation.ts
Normal file
8
server/utils/image-validation.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const MAX_IMAGE_BYTES = 8 * 1024 * 1024
|
||||||
|
export const MAX_IMAGE_WIDTH = 10_000
|
||||||
|
export const MAX_IMAGE_HEIGHT = 10_000
|
||||||
|
export const MAX_IMAGE_PIXELS = 40_000_000
|
||||||
|
|
||||||
|
export function isSharpPixelLimitError(error: unknown) {
|
||||||
|
return error instanceof Error && /exceeds pixel limit/i.test(error.message)
|
||||||
|
}
|
||||||
13
tests/image-validation.test.ts
Normal file
13
tests/image-validation.test.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { MAX_IMAGE_PIXELS, isSharpPixelLimitError } from '../server/utils/image-validation'
|
||||||
|
|
||||||
|
describe('图片上传校验', () => {
|
||||||
|
it('识别 Sharp 像素上限错误', () => {
|
||||||
|
expect(isSharpPixelLimitError(new Error('Input image exceeds pixel limit'))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('不把普通解析错误误判为像素过大', () => {
|
||||||
|
expect(isSharpPixelLimitError(new Error('Input buffer contains unsupported image format'))).toBe(false)
|
||||||
|
expect(MAX_IMAGE_PIXELS).toBe(40_000_000)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user