校验文件的类型实现
我们经常用到上传文件之前先校验下环境,不要觉得后台校验前端就不需要检验了,因为用户上传一个视频可能几十秒,等几十秒后再告诉他文件格式不合适,这时候用户有想要锤开发者狗脑袋的冲动。
文件上传的时候我们拿到file,里面有一个type类型,在一般情况下我们可以根据这个判断,但是它不是很准确。因为基于当前的实现,浏览器不会实际读取文件的字节流,来判断它的媒体类型。详见File.type
所以我们部分场景需要用到更精确的方法,这里我们使用读取二进制文件然后根据头部信息进行判断。这样就可以避免用户直接修改文件的后缀名,然后导致误判。
题外话:(我们测试就提出这个问题,为什么视频无法正常播放,我一看,map4我本地都可以啊,然后发现,上传的时候他把avi改成mp4后缀骗过了浏览器的file.type的判断)
实现
首先我们要获取文件二进制数据
var fileReader = new FileReader()
fileReader.onloadend = function({ target }) {
getHeaderValue(target.result) // 处理二进制数据
}
try {
fileReader.readAsArrayBuffer(blob)
} catch (error) {
console.log(error)
}
然后获取数据取到16进制
const getHeaderValue = (value, n) => {
const arr = (new Uint8Array(value))//.subarray(0, n)
let header = ""
for (let i = 0; i < arr.length; i++) {
const v = arr[i].toString(16)
header += v.length === 1 && v === '0' ? '0' + v : v
}
return header
}
这里本来是抄stackoverflow的代码,不过有点小问题,在mp4的时候数据有问题,所以上面header拼接的时候,是0的时候我转成两位数,这样可以和List_of_file_signatures的对得上
我们看下他的代码吧
大概是这样,比较完整的代码。不过我们需要加工下。因为我们需要对一些复杂格式进行处理(比如avi格式,前面格式不是完全固定的),这样就导致了我们需要些很多sase代码和if代码,不利于维护,我们转成json格式吧。
json格式扁平的,前面如果是固定的值,那么直接map获取,这样不需要去循环,如果格式的复杂的,比如AVI52 49 46 46 ?? ?? ?? ?? 41 56 49 20是这样的格式,那么我们的key的表达方式可以是52494646{8,4}4156,看下json代码
{
"47494638": "image/gif",
"89504e47": "image/png",
"ffd8ffe0": "image/jpeg",
"ffd8ffe1": "image/jpeg",
"ffd8ffe2": "image/jpeg",
"ffd8ffe3": "image/jpeg",
"ffd8ffe8": "image/jpeg",
"52494646{8,4}5745": "image/webp",
"52494646{8,4}4156": "video/avi",
"464C56": "video/flv",
"00000018": "video/mp4",
"00000020": "video/mp4",
"52494646,4143": "ani",
"52494646,4344": "cda",
"52494646,514c": "qcp"
}
处理函数
const handleFileType = (value16, change) => {
let header3 = value16.substring(0, 3 * 2)
,header4 = value16.substring(0, 4 * 2)
,header8 = value16.substring(0, 16 * 2)
let type = TYPEMAP[header4] || TYPEMAP[header3] || ''
if (type === '') {
for (let key of Object.keys(TYPEMAP)) {
let arr = key.split(/\{\d+,\d+\}/, 2)
if (!arr[1]) {
continue
}
if (header8.substring(0, arr[0].length) === arr[0]) {
const siteArr = key.match(/\{(\d+,\d+)\}/)[1].split(',').map(e => e | 0)
const startSite = arr[1].length + siteArr[0] + 2
if (header8.substring(startSite, startSite + siteArr[1]) === arr[1]) {
type = TYPEMAP[key]
break
}
} else {
continue
}
}
}
change && change(type)
}
好了,完成,晚上六点下班,耶稣也留不住我
完整代码
import TYPEMAP from './filetype.json'
const getHeaderValue = (value, n) => {
const arr = (new Uint8Array(value))
let header = ""
for (let i = 0; i < arr.length; i++) {
const v = arr[i].toString(16)
header += v.length === 1 && v === '0' ? '0' + v : v
}
return header
}
/**
* file header get type
* @param {*} value16
* @param {*} change
*/
const handleFileType = (value16, change) => {
let header3 = value16.substring(0, 3 * 2)
,header4 = value16.substring(0, 4 * 2)
,header8 = value16.substring(0, 16 * 2)
let type = TYPEMAP[header4] || TYPEMAP[header3] || ''
if (type === '') {
for (let key of Object.keys(TYPEMAP)) {
let arr = key.split(/\{\d+,\d+\}/, 2)
if (!arr[1]) {
continue
}
if (header8.substring(0, arr[0].length) === arr[0]) {
const siteArr = key.match(/\{(\d+,\d+)\}/)[1].split(',').map(e => e | 0)
const startSite = arr[1].length + siteArr[0] + 2
if (header8.substring(startSite, startSite + siteArr[1]) === arr[1]) {
type = TYPEMAP[key]
break
}
} else {
continue
}
}
}
change && change(type)
}
/**
* get file type
* @param {*} blob
* @param {(type: string) => void} change (type) => void
* @returns Promise<(type) => void>
*/
export const getFileType = (blob, change) => {
var fileReader = new FileReader()
return new Promise((resolve, reject) => {
fileReader.onloadend = function({ target }) {
handleFileType(getHeaderValue(target.result), (type) => {
resolve(type)
change && change(type)
})
}
try {
fileReader.readAsArrayBuffer(blob)
} catch (error) {
console.log(error)
reject(error)
}
})
}
filetype.json
{
"47494638": "image/gif",
"89504e47": "image/png",
"ffd8ffe0": "image/jpeg",
"ffd8ffe1": "image/jpeg",
"ffd8ffe2": "image/jpeg",
"ffd8ffe3": "image/jpeg",
"ffd8ffe8": "image/jpeg",
"52494646{8,4}5745": "image/webp",
"52494646{8,4}4156": "video/avi",
"464C56": "video/flv",
"00000018": "video/mp4",
"00000020": "video/mp4",
"52494646,4143": "ani",
"52494646,4344": "cda",
"52494646,514c": "qcp"
}
扩展更多参考二进制头对照表