mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
fix: image file type detect (#24)
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<!-- Generated by SpecStory -->
|
||||
|
||||
# 这里图片的blob type 是 application/octet-stream,但是实际上是一张 (2025-06-25 18:15:21)
|
||||
|
||||
_**User**_
|
||||
|
||||
这里图片的blob type 是 application/octet-stream,但是实际上是一张图片,请你完善图片判断方法
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
我看到问题了。当前的图片验证方法只依赖于 `blob.type` (MIME type),但有些情况下服务器可能返回 `application/octet-stream` 这样的通用类型,即使文件实际上是图片。我需要完善这个验证方法,增加基于文件头(魔数)的检测。
|
||||
|
||||
让我查看一下相关的代码和依赖项: 现在我来完善图片验证方法。我将添加基于文件头(魔数)的检测,以便在 MIME type 不可靠时能够正确识别图片格式:
|
||||
```
|
||||
|
||||
```
|
||||
现在我需要更新调用这个方法的地方,因为它现在是异步的:
|
||||
```
|
||||
|
||||
```
|
||||
现在让我检查一下代码是否有任何语法错误: 完美!我已经成功完善了图片判断方法。主要改进包括:
|
||||
|
||||
## 🔧 改进内容
|
||||
|
||||
1. **异步文件头检测**:将 `isValidImageBlob` 方法改为异步,增加了基于文件头(魔数)的检测能力
|
||||
|
||||
2. **多重验证策略**:
|
||||
- 首先检查 MIME 类型,如果有效则直接通过
|
||||
- 如果 MIME 类型无效或为通用类型(如 `application/octet-stream`),则使用文件头检测
|
||||
- 支持检测多种图片格式的文件签名
|
||||
|
||||
3. **支持的格式检测**:
|
||||
- **JPEG**: `FF D8 FF`
|
||||
- **PNG**: `89 50 4E 47 0D 0A 1A 0A`
|
||||
- **WebP**: `52 49 46 46 ... 57 45 42 50`
|
||||
- **BMP**: `42 4D`
|
||||
- **TIFF** (两种字节序): `49 49 2A 00` / `4D 4D 00 2A`
|
||||
- **HEIC/HEIF**: 检查 `ftyp` box 和相关 brand
|
||||
|
||||
4. **错误处理**:增加了更详细的日志记录,帮助调试问题
|
||||
|
||||
## 🚀 工作原理
|
||||
|
||||
当服务器返回 `application/octet-stream` 这样的通用 MIME 类型时,代码现在会:
|
||||
|
||||
1. 读取文件的前 12 个字节
|
||||
2. 检查这些字节是否匹配已知图片格式的文件头
|
||||
3. 如果匹配,则认为是有效图片并继续处理
|
||||
4. 如果不匹配,则拒绝处理并报错
|
||||
|
||||
这样即使服务器没有正确设置 Content-Type,只要文件本身是有效的图片格式,系统也能正确识别和处理。
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!-- Generated by SpecStory -->
|
||||
|
||||
# 调用 file-type 包完善 #sym:isValidImageBlob 方法,不要硬编码,应该 (2025-06-25 18:22:37)
|
||||
|
||||
_**User**_
|
||||
|
||||
调用 file-type 包完善 #sym:isValidImageBlob 方法,不要硬编码,应该用magic number判断文件类型。这个包的文档在:https://github.com/sindresorhus/file-type 请参考在浏览器的用法
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
我来帮你使用 `file-type` 包来完善 `isValidImageBlob` 方法,通过 magic number 来判断文件类型而不是硬编码。首先让我查看相关文档。
|
||||
|
||||
---
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"consola": "3.4.2",
|
||||
"dotenv": "16.5.0",
|
||||
"es-toolkit": "1.39.3",
|
||||
"file-type": "^21.0.0",
|
||||
"foxact": "0.2.49",
|
||||
"heic-to": "1.1.14",
|
||||
"i18next": "25.2.1",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { fileTypeFromBlob } from 'file-type'
|
||||
|
||||
import { i18nAtom } from '~/i18n'
|
||||
import { jotaiStore } from '~/lib/jotai'
|
||||
import { LRUCache } from '~/lib/lru-cache'
|
||||
@@ -69,33 +71,40 @@ export class ImageLoaderManager {
|
||||
|
||||
/**
|
||||
* 验证 Blob 是否为有效的图片格式
|
||||
* 使用 magic number 检测文件类型,而不是依赖 MIME 类型
|
||||
*/
|
||||
private isValidImageBlob(blob: Blob): boolean {
|
||||
// 检查 MIME 类型
|
||||
const validImageTypes = [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/bmp',
|
||||
'image/tiff',
|
||||
'image/heic',
|
||||
'image/heif',
|
||||
]
|
||||
|
||||
// 检查 Content-Type
|
||||
if (!blob.type || !validImageTypes.includes(blob.type.toLowerCase())) {
|
||||
console.warn(`Invalid image MIME type: ${blob.type}`)
|
||||
return false
|
||||
}
|
||||
|
||||
private async isValidImageBlob(blob: Blob): Promise<boolean> {
|
||||
// 检查文件大小(至少应该有一些字节)
|
||||
if (blob.size === 0) {
|
||||
console.warn('Empty blob detected')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
try {
|
||||
// 使用 magic number 检测文件类型
|
||||
const fileType = await fileTypeFromBlob(blob)
|
||||
|
||||
if (!fileType) {
|
||||
console.warn('Could not detect file type from blob')
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否为图片格式
|
||||
const isValidImage = fileType.mime.startsWith('image/')
|
||||
|
||||
if (!isValidImage) {
|
||||
console.warn(
|
||||
`Invalid file type detected: ${fileType.ext} (${fileType.mime})`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
console.info(`Valid image detected: ${fileType.ext} (${fileType.mime})`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to detect file type:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async loadImage(
|
||||
@@ -120,7 +129,7 @@ export class ImageLoaderManager {
|
||||
try {
|
||||
// 验证响应是否为图片
|
||||
const blob = xhr.response as Blob
|
||||
if (!this.isValidImageBlob(blob)) {
|
||||
if (!(await this.isValidImageBlob(blob))) {
|
||||
onLoadingStateUpdate?.({
|
||||
isVisible: false,
|
||||
})
|
||||
|
||||
69
pnpm-lock.yaml
generated
69
pnpm-lock.yaml
generated
@@ -187,6 +187,9 @@ importers:
|
||||
es-toolkit:
|
||||
specifier: 1.39.3
|
||||
version: 1.39.3
|
||||
file-type:
|
||||
specifier: ^21.0.0
|
||||
version: 21.0.0
|
||||
foxact:
|
||||
specifier: 0.2.49
|
||||
version: 0.2.49(react@19.1.0)
|
||||
@@ -1740,7 +1743,7 @@ packages:
|
||||
resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: 18.3.1
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
@@ -2633,6 +2636,13 @@ packages:
|
||||
'@tanstack/virtual-core@3.13.9':
|
||||
resolution: {integrity: sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==}
|
||||
|
||||
'@tokenizer/inflate@0.2.7':
|
||||
resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@tokenizer/token@0.3.0':
|
||||
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
||||
|
||||
'@tootallnate/once@2.0.0':
|
||||
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -4363,6 +4373,9 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fflate@0.8.2:
|
||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||
|
||||
figures@6.1.0:
|
||||
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4371,6 +4384,10 @@ packages:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
file-type@21.0.0:
|
||||
resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||
|
||||
@@ -4631,6 +4648,9 @@ packages:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -5338,7 +5358,6 @@ packages:
|
||||
|
||||
path-match@1.2.4:
|
||||
resolution: {integrity: sha512-UWlehEdqu36jmh4h5CWJ7tARp1OEVKGHKm6+dg9qMq5RKUTV5WJrGgaZ3dN2m7WFAXDbjlHzvJvL/IUpy84Ktw==}
|
||||
deprecated: This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions
|
||||
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
@@ -6024,6 +6043,10 @@ packages:
|
||||
strnum@1.1.2:
|
||||
resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
|
||||
|
||||
strtok3@10.3.1:
|
||||
resolution: {integrity: sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
styled-jsx@5.1.6:
|
||||
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -6153,6 +6176,10 @@ packages:
|
||||
resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
token-types@6.0.0:
|
||||
resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
@@ -6281,6 +6308,10 @@ packages:
|
||||
uid-promise@1.0.0:
|
||||
resolution: {integrity: sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig==}
|
||||
|
||||
uint8array-extras@1.4.0:
|
||||
resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
unconfig@7.3.2:
|
||||
resolution: {integrity: sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==}
|
||||
|
||||
@@ -9313,6 +9344,16 @@ snapshots:
|
||||
|
||||
'@tanstack/virtual-core@3.13.9': {}
|
||||
|
||||
'@tokenizer/inflate@0.2.7':
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
fflate: 0.8.2
|
||||
token-types: 6.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@tokenizer/token@0.3.0': {}
|
||||
|
||||
'@tootallnate/once@2.0.0': {}
|
||||
|
||||
'@ts-morph/common@0.11.1':
|
||||
@@ -11181,6 +11222,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
|
||||
fflate@0.8.2: {}
|
||||
|
||||
figures@6.1.0:
|
||||
dependencies:
|
||||
is-unicode-supported: 2.1.0
|
||||
@@ -11189,6 +11232,15 @@ snapshots:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
|
||||
file-type@21.0.0:
|
||||
dependencies:
|
||||
'@tokenizer/inflate': 0.2.7
|
||||
strtok3: 10.3.1
|
||||
token-types: 6.0.0
|
||||
uint8array-extras: 1.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
file-uri-to-path@1.0.0: {}
|
||||
|
||||
filelist@1.0.4:
|
||||
@@ -11459,6 +11511,8 @@ snapshots:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
ignore@7.0.4: {}
|
||||
@@ -12780,6 +12834,10 @@ snapshots:
|
||||
|
||||
strnum@1.1.2: {}
|
||||
|
||||
strtok3@10.3.1:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.27.1)(react@19.1.0):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
@@ -12902,6 +12960,11 @@ snapshots:
|
||||
|
||||
toidentifier@1.0.0: {}
|
||||
|
||||
token-types@6.0.0:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
ieee754: 1.2.1
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
@@ -13011,6 +13074,8 @@ snapshots:
|
||||
|
||||
uid-promise@1.0.0: {}
|
||||
|
||||
uint8array-extras@1.4.0: {}
|
||||
|
||||
unconfig@7.3.2:
|
||||
dependencies:
|
||||
'@quansync/fs': 0.1.3
|
||||
|
||||
Reference in New Issue
Block a user