From ed81f2489215b028969372e6462063f8d5775ee1 Mon Sep 17 00:00:00 2001
From: Nelson <1475262689@qq.com>
Date: Thu, 13 Aug 2026 19:01:44 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E6=98=8E=E7=A1=AE=E6=8F=90=E7=A4=BA?=
=?UTF-8?q?=E8=B6=85=E5=A4=A7=20PNG=20=E5=83=8F=E7=B4=A0=E9=99=90=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/pages/admin/media.vue | 2 +-
server/api/admin/media.post.ts | 14 ++++++++++----
server/utils/image-validation.ts | 8 ++++++++
tests/image-validation.test.ts | 13 +++++++++++++
4 files changed, 32 insertions(+), 5 deletions(-)
create mode 100644 server/utils/image-validation.ts
create mode 100644 tests/image-validation.test.ts
diff --git a/app/pages/admin/media.vue b/app/pages/admin/media.vue
index f2def15..1fefc0d 100644
--- a/app/pages/admin/media.vue
+++ b/app/pages/admin/media.vue
@@ -90,7 +90,7 @@ const formatBytes = (bytes: number) => bytes < 1024 * 1024 ? `${Math.ceil(bytes
diff --git a/server/api/admin/media.post.ts b/server/api/admin/media.post.ts
index eca980f..61d123a 100644
--- a/server/api/admin/media.post.ts
+++ b/server/api/admin/media.post.ts
@@ -3,6 +3,7 @@ import { mkdir, rm, writeFile } from 'node:fs/promises'
import { extname } from 'node:path'
import sharp from 'sharp'
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 { useDatabase } from '../../utils/db'
@@ -30,7 +31,7 @@ export default defineEventHandler(async (event) => {
const parts = await readMultipartFormData(event)
const file = parts?.find(part => part.name === 'file' && part.filename)
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)
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: '文件扩展名与实际图片类型不一致' })
let imageMetadata
- try { imageMetadata = await sharp(file.data, { limitInputPixels: 40_000_000 }).metadata() }
- catch { throw createError({ statusCode: 415, statusMessage: '图片内容损坏或无法解析' }) }
+ try { imageMetadata = await sharp(file.data, { limitInputPixels: MAX_IMAGE_PIXELS }).metadata() }
+ catch (error) {
+ if (isSharpPixelLimitError(error)) {
+ throw createError({ statusCode: 413, statusMessage: '图片像素过大,请压缩至 10000×10000 且不超过 4000 万像素后上传' })
+ }
+ throw createError({ statusCode: 415, statusMessage: '图片内容损坏或无法解析' })
+ }
const width = imageMetadata.width || 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 万像素' })
}
diff --git a/server/utils/image-validation.ts b/server/utils/image-validation.ts
new file mode 100644
index 0000000..462a1b3
--- /dev/null
+++ b/server/utils/image-validation.ts
@@ -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)
+}
diff --git a/tests/image-validation.test.ts b/tests/image-validation.test.ts
new file mode 100644
index 0000000..6fd77e2
--- /dev/null
+++ b/tests/image-validation.test.ts
@@ -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)
+ })
+})