浅析JavaScript如何检测文件的类型:区分文件类型的本质、关于魔数的理解、input accept文件检测存在的问题、如何检测修改后缀名后的文件类型、推荐一个文件检测JS库-file-type库及其检测原理
在日常工作中,文件上传是一个很常见的功能。在某些情况下,我们希望能限制文件上传的类型,比如限制只能上传 PNG 格式的图片。针对这个问题,我们会想到通过 input
元素的 accept
属性来限制上传的文件类型。这种方案虽然可以满足大多数场景,但如果用户把 JPEG 格式的图片后缀名更改为 .png
的话,就可以成功突破这个限制。那么应该如何解决这个问题呢?其实我们可以通过读取文件的二进制数据来识别正确的文件类型。
一、如何区分图片的类型
计算机并不是通过图片的后缀名来区分不同的图片类型,而是通过 “魔数”(Magic Number)来区分。 对于某一些类型的文件,起始的几个字节内容都是固定的,根据这几个字节的内容就可以判断文件的类型。
常见图片类型对应的魔数如下表所示:
由上图可知,PNG 类型的图片前 8 个字节是 0x89 50 4E 47 0D 0A 1A 0A。当你把 abao.png
文件修改为 abao.jpeg
后,再用编辑器打开查看图片的二进制内容,你会发现文件的前 8 个字节还是保持不变。但如果使用 input[type="file"]
输入框的方式来读取文件信息的话,将会输出以下结果:
文件后缀名及文件的 mime 类型均改变了。很明显通过 文件后缀名或文件的 MIME 类型 并不能识别出正确的文件类型。
二、如何检测图片的类型
1、定义 readBuffer 函数
在获取文件对象后,我们可以通过 FileReader API 来读取文件的内容。因为我们并不需要读取文件的完整信息,所以可以封装一个 readBuffer
函数,用于读取文件中指定范围的二进制数据。
function readBuffer(file, start = 0, end = 2) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsArrayBuffer(file.slice(start, end));
});
}
对于 PNG 类型的图片来说,该文件的前 8 个字节是 0x89 50 4E 47 0D 0A 1A 0A。因此,我们在检测已选择的文件是否为 PNG 类型的图片时,只需要读取前 8 个字节的数据,并逐一判断每个字节的内容是否一致。
2、定义 check 函数
为了实现逐字节比对并能够更好地实现复用,再定义了一个 check
函数:
function check(headers) {
return (buffers, options = { offset: 0 }) =>
headers.every(
(header, index) => header === buffers[options.offset + index]
);
}
3、检测 PNG 图片类型
基于前面定义的 readBuffer
和 check
函数,我们就可以实现检测 PNG 图片的功能:
// html 代码
<div>
选择文件:<input type="file" id="inputFile" accept="image/*"
onchange="handleChange(event)" />
<p id="realFileType"></p>
</div>
// JS 代码
const isPNG = check([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); // PNG图片对应的魔数
const realFileElement = document.querySelector("#realFileType");
async function handleChange(event) {
const file = event.target.files[0];
const buffers = await readBuffer(file, 0, 8);
const uint8Array = new Uint8Array(buffers);
realFileElement.innerText = `${file.name}文件的类型是:${
isPNG(uint8Array) ? "image/png" : file.type
}`;
}
以上示例成功运行后,对应的检测结果如下图所示
由上图可知,我们已经可以成功地检测出正确的图片格式。如果你要检测 JPEG 文件格式的话,你只需要定义一个 isJPEG
函数
const isJPEG = check([0xff, 0xd8, 0xff])
在实际工作中,遇到的文件类型是多种多样的,针对这种情形,你可以使用现成的第三库来实现文件检测的功能,比如 file-type 这个库。
三、文件检测 JS 库 - file-type
项目地址:https://github.com/sindresorhus/file-type
1、文件检测存在的问题
通常,我们的程序通过文件后缀名检测类型,这是最直接简洁的方式。但是在一些情况下,直接通过后缀名检测文件类型,不太合适或行不通,比如:
(1)只得到了数据流,但是没有文件名
(2)被重命名后缀名,或者去掉后缀储存的文件
(3)文件后缀和实际内容不匹配或后缀名不受信任
2、file-type 原理
file-type 可以直接检测一个Buffer数据流,得到这个Buffer数据的内容(文件)类型。
file-type 的原理是检测文件/数据的Magic Number。通常情况下,一些知名的文件类型,在其文件开头的几个字节用来标志其文件类型,这几个字节就叫做 Magic Number。比如,PDF文件开头的几个字节是 %PDF
(hex: 25
50
44
46
)。
file-type 现在已经支持的文件类型列表:
jpg png gif webp flif cr2 tif bmp jxr psd zip tar rar gz bz2 7z dmg mp4 m4v mid mkv webm mov avi wmv mpg mp3 m4a ogg opus flac wav amr pdf epub mobi exe swf rtf woff woff2 eot ttf otf ico flv ps xz sqlite nes crx xpi cab deb ar rpm Z lz msi mxf mts wasm blend bpg docx pptx xlsx 3gp jp2 jpm jpx mj2 aif odt ods odp xml
3、问题介绍及处理
最近做一个需求,只是单纯的图片上传,结果测试出现图片上传成功,但是放到产品里面黑了,而且只是两张图片会这样,后来是测试说不行的那两张图片是直接修改过后缀名的。上网查了查,原来每个文件的文件字节流开头内容都会有一个文件类型的标记,其实文件字节流就是这个文件,改了后缀名,这个文件字节流的文件类型标记是不会被修改的。
一般来说,前端上传都是 input 的 accept 那边限制一下,然后通过文件名的后缀再拦截一下,我是从来没有通过字节流去判断文件类型。有找到一个 file-type 的 npm 包,专门做这个的,下载试了一下,也可以去npm官网看看:https://www.npmjs.com/package/file-type
这个包找了一下,没有提供js引入的版本,看了看代码,core.js里面的_fromTokenizer把各个文件类型要检测的都提供了,参考里面的代码写了个图片png和jpg检测的demo:
<input type="file" onchange="handleChange(event)" />
function handleChange(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = () => {
let fileType = typeResult(reader.result);
console.log(fileType);
};
reader.readAsArrayBuffer(file);
}
function _check(buffer, headers) {
options = {
offset: 0
};
for (const [index, header] of headers.entries()) {
if (header !== buffer[index + options.offset]) {
return false;
}
}
return true;
}
function typeResult(arryBUffer) {
const buffer = new Uint8Array(arryBUffer);
const check = (header, options) => _check(buffer, header);
if (check([0xFF, 0xD8, 0xFF])) {
return {
ext: 'jpg',
mime: 'image/jpeg'
};
}
if (check([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])) {
return {
ext: 'png',
mime: 'image/png'
};
}
return undefined
}
测试是可以检测,因为没有深入了解字节流的含义,里面检测其他类型有很多不同的判断,png其实还有其他判断,这边给省略了:
case 'IDAT':
return {
ext: 'png',
mime: 'image/png'
};
case 'acTL':
return {
ext: 'apng',
mime: 'image/apng'
};
里面代码还有截取字节流,还有判断两张参数的,还有判断第几个开始的,看起来很复杂。
相对来说,用input的accept进行拦截应该是满足需求了,知道了这个,如果以后后台出现类型不符合或者需求需要,就不会没有一点概念了。
参考文章:
JavaScript 如何检测文件的类型:https://mp.weixin.qq.com/s/vie22Y2dfbeAKx81HX6Xsg
npm包file-type之文件类型:https://blog.csdn.net/wade3po/article/details/118676311