JS - 校验文件的类型实现(二进制)

3,153 阅读3分钟

校验文件的类型实现

我们经常用到上传文件之前先校验下环境,不要觉得后台校验前端就不需要检验了,因为用户上传一个视频可能几十秒,等几十秒后再告诉他文件格式不合适,这时候用户有想要锤开发者狗脑袋的冲动。

文件上传的时候我们拿到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的对得上

我们看下他的代码吧

image.png

大概是这样,比较完整的代码。不过我们需要加工下。因为我们需要对一些复杂格式进行处理(比如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"
}

扩展更多参考二进制头对照表