用<input type='file' />上传rar、zip文件,rar、zip对应的MINE TYPE在不同电脑、不同浏览器之间的差异

93 阅读3分钟

又遇到一个文件上传MINE TYPE的坑,这次是压缩文件-rar、zip

一、背景:产品在一次迭代中新增了上传文件类型,支持zip和rar文件

OS:这还不简单,网上搜了一下这两个文件对应的MIME TYPE,搜索结果如下:

// .rar
application/vnd.rar

//.zip
application/zip

image.png

image.png

于是在原来旧的MIMT TYPE对象加键值对:

// Mine Type 映射
export const MINE_TYPE = {
  COMMON: 'application/octet-stream',
  PDF: 'application/pdf',
  DOC: 'application/msword',
  DOCX: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  XLS: 'application/vnd.ms-excel',
  XLSX: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  PNG: 'image/png',
  JPG: 'image/jpeg', // jpg和jpeg都是这个
  GIF: 'image/gif',
  BMP: 'image/bmp',
  APK: 'application/vnd.android.package-archive',
  TXT: 'text/plain',
  HTML: 'text/html',
  CSV: 'text/csv',
  MP3: 'audio/mpeg',
  RAR: 'application/vnd.rar', // 新加
  ZIP: 'application/zip', // 新加
};
import {  invert } from 'lodash';
// 获取文件名后缀
const getFileSuffix = (filename: string) => {
  const index = filename.lastIndexOf('.');
  if (index !== -1) {
    return filename.substr(index + 1);
  }
  return false;
};

// 检查上传文件类型
export const checkTypes = (file: RcFile, typeList: string[], forceSuffix: boolean = false) => {
  const type = typeList;
  if (type && file && file.type && !forceSuffix) {
    return type.includes(file.type);
  }

  const suffix = getFileSuffix(file.name);
  if (suffix && file && file.name) {
    let pass = false;
    const mine = invert(MINE_TYPE);
    typeList.forEach((item) => {
      if (!pass) {
        pass = suffix.toLocaleUpperCase() === mine[item];
      }
    });
    return pass;
  }
  return false;
};

// 轉換文件為相應的單位
export const transSize = (limit: number, base: string = 'bytes', isBit: boolean = true) => {
  const baseByte = isBit ? 1024 : 1000;

  let bytes;
  switch (base.toLowerCase()) {
    case 'mb':
      bytes = limit * baseByte * baseByte;
      break;
    default:
      bytes = limit;
      break;
  }

  if (bytes === 0) return '0 B';
  const k = baseByte;
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return `${(bytes / k ** i).toPrecision(3)} ${sizes[i]}`;
};

// 获取限制文件大小
export const checkMaxSize = (size: number, maxSize: number) => {
  return size / 1024 / 1024 < maxSize;
};

export const asyncBeforeUpload = async (file: RcFile, $_props: UploadPlusProps) => {
  let mathType = true;
  let ltSize = true;
  if ($_props.mineType) {
    mathType = checkTypes(file, $_props.mineType);
    if (!mathType) {
      notifiy.error('文件類型不符合, 請檢查後重試');
    }
  }
  if ($_props.maxSize) {
    ltSize = checkMaxSize(file.size, $_props.maxSize);
    if (!ltSize) {
      notifiy.error('文件大小必須小於'+transSize($_props.maxSize || 5, 'mb'))
    }
  }

  return ltSize && mathType;
};

二、发现异常

1、上传rar文件type不符预期

上传时rar文件,在asyncBeforeUpload方法中,我的电脑拿到file的type是type: '',根本没办法用includes这样的方式去判断文件类型,所以进行改造:

export const asyncBeforeUpload = async (file: RcFile, $_props: UploadPlusProps) => {
  let mathType = true;
  let ltSize = true;
  if ($_props.mineType) {
    // 處理type為空的文件類型
    const fileSuffix = file.name.replace(/.+\./, '');
    // const fileSuffix = getFileSuffix(file.name);
    const special_file_list: Array<keyof typeof MIME_TYPE> = ['RAR'];
    const isSpecialFile = (special_file_list as string[]).includes(fileSuffix.toLocaleUpperCase());
    // 特殊文件用文件名后缀判断
    mathType = checkTypes(file, $_props.mineType, isSpecialFile);
    if (!mathType) {
      notifiy.error('文件類型不符合, 請檢查後重試');
    }
  }
  if ($_props.maxSize) {
    ltSize = checkMaxSize(file.size, $_props.maxSize);
    if (!ltSize) {
      notifiy.error(notifiy.error('文件大小必須小於'+transSize($_props.maxSize || 5, 'mb')));
    }
  }

  return ltSize && mathType;
};

后面让同事本地调试了一下下,发现他的电脑可以拿到type的值!!!所以又在MIME_TYPE加了一下注释,以警示后人

// Mine Type 映射
export const MINE_TYPE = {
  COMMON: 'application/octet-stream',
  PDF: 'application/pdf',
  DOC: 'application/msword',
  DOCX: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  XLS: 'application/vnd.ms-excel',
  XLSX: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  PNG: 'image/png',
  JPG: 'image/jpeg', // jpg和jpeg都是这个
  GIF: 'image/gif',
  BMP: 'image/bmp',
  APK: 'application/vnd.android.package-archive',
  TXT: 'text/plain',
  HTML: 'text/html',
  CSV: 'text/csv',
  MP3: 'audio/mpeg',
  // RAR类型不准确,仅做标识使用,例如我的电脑会直接返回空字符串
  // RAR类型还有两个值是application/x-rar-compressed、application/x-compressed
  RAR: 'application/vnd.rar',
  ZIP: 'application/zip',
  ZIP2: 'application/x-zip-compressed',
};

2、上传zip文件type我电脑符合预期,但是测试的电脑不符合预期

我的电脑拿到的type是application/zip,但是测试的电脑拿到的是application/x-zip-compressed,由于在我电脑没发现这个问题,导致上测试出现bug,所以要对MIME_TYPE做以下调整:

// Mine Type 映射
export const MINE_TYPE = {
  COMMON: 'application/octet-stream',
  PDF: 'application/pdf',
  DOC: 'application/msword',
  DOCX: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  XLS: 'application/vnd.ms-excel',
  XLSX: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  PNG: 'image/png',
  JPG: 'image/jpeg', // jpg和jpeg都是这个
  GIF: 'image/gif',
  BMP: 'image/bmp',
  APK: 'application/vnd.android.package-archive',
  TXT: 'text/plain',
  HTML: 'text/html',
  CSV: 'text/csv',
  MP3: 'audio/mpeg',
  RAR: 'application/vnd.rar',
  // ZIP文件类型要一起用,不用系统、不同内核浏览器类型不一样
  ZIP: 'application/zip',
  ZIP2: 'application/x-zip-compressed',
};

使用时:

// 传入支持的类型:
mineType={[
  // ...
  MINE_TYPE.RAR,
  MINE_TYPE.ZIP,
  MINE_TYPE.ZIP2,
]}

三、总结

不同电脑、不同浏览器获取的file.type可能不一样!!!
rar文件:
以下是通义千问的回答: image.png rar文件上传type可能的值:

// 1.空
''
// 2
application/x-rar-compressed
// 3
application/vnd.rar

zip文件:
以下是通义千问的回答: image.png zip文件上传type可能的值:

// 1
application/x-zip
// 2
application/x-zip-compressed