浏览器端上传文件限制文件编码,禁止二进制文件等非文本上传。

284 阅读6分钟

要实现上传文件内容的校验,包含判断逻辑和上传限制。

判断文件是否为二进制的思路

  1. 文件大小校验:确保文件不超过 40MB。
  2. 读取文件内容:读取文件的前几 KB(例如 4KB)以优化性能。
  3. 尝试解码:使用 TextDecoder 尝试将文件内容解码为 UTF-8 文本。
  4. 检查字符合法性
    • 合法字符包括:可打印字符(字母、数字、标点符号、空格等)、回车(\r)、换行(\n)、制表符(\t)等。
    • 不可读字符:除上述外的控制字符(如 U+0000U+001F 中除 \r\n\t 外的字符,以及 U+007F)。
    • 如果不可读字符比例过高(例如 >10%),认为文件是二进制。
  5. 上传限制:仅允许非二进制(可读文本)文件上传,并提供用户提示。

完整代码

以下代码实现了文件类型判断、40MB 大小限制,并禁止二进制文件上传,同时在界面上显示提示信息。

// 最大文件大小:40MB (40 * 1024 * 1024 字节)
const MAX_FILE_SIZE = 40 * 1024 * 1024;

async function isReadableText(file) {
  // 检查文件大小
  if (file.size > MAX_FILE_SIZE) {
    return { isValid: false, message: `文件大小 ${Math.round(file.size / 1024 / 1024)}MB,超过最大限制 40MB` };
  }

  try {
    // 读取文件前 4KB(优化性能)
    const buffer = await file.slice(0, 4096).arrayBuffer();
    
    // 使用 TextDecoder 解码为 UTF-8 文本
    const decoder = new TextDecoder('utf-8', { fatal: false });
    const text = decoder.decode(buffer);
    
    // 检查不可打印字符的比例
    let unreadableCount = 0;
    for (let i = 0; i < text.length; i++) {
      const code = text.charCodeAt(i);
      // 仅将除 \r (0x0D)、\n (0x0A)、\t (0x09) 外的控制字符 (0x00-0x1F) 和 0x7F 视为不可读
      if ((code < 0x20 && code !== 0x0D && code !== 0x0A && code !== 0x09) || code === 0x7F) {
        unreadableCount++;
      }
    }
    
    // 如果不可读字符比例超过阈值(10%),认为是二进制文件
    const unreadableRatio = unreadableCount / text.length;
    const threshold = 0.1; // 可根据需求调整
    if (unreadableRatio >= threshold) {
      return { isValid: false, message: '文件包含过多不可读字符,可能是二进制文件' };
    }
    
    return { isValid: true, message: '文件是可读文本' };
  } catch (e) {
    // 解码失败,认为是二进制文件
    return { isValid: false, message: '无法解码文件,可能是二进制文件' };
  }
}

// 处理文件上传
document.getElementById('fileInput').addEventListener('change', async (event) => {
  const file = event.target.files[0];
  const messageElement = document.getElementById('message');
  
  if (!file) {
    messageElement.textContent = '请选择一个文件';
    return;
  }

  // 校验文件
  const result = await isReadableText(file);
  messageElement.textContent = result.message;

  if (result.isValid) {
    // 允许上传(此处模拟上传逻辑)
    messageElement.style.color = 'green';
    console.log('可以上传文件:', file.name);
    // 实际上传逻辑,例如使用 FormData 发送到服务器
    // const formData = new FormData();
    // formData.append('file', file);
    // fetch('/upload', { method: 'POST', body: formData });
  } else {
    // 禁止上传
    messageElement.style.color = 'red';
    console.log('禁止上传:', result.message);
    // 清空输入框,防止用户提交
    event.target.value = '';
  }
});

HTML 示例

<!DOCTYPE html>
<html>
<head>
  <title>文件上传校验</title>
  <style>
    #message { margin-top: 10px; font-size: 16px; }
  </style>
</head>
<body>
  <input type="file" id="fileInput" />
  <div id="message"></div>
  <script src="script.js"></script>
</body>
</html>

关键点说明

  1. 二进制文件判断

    • 使用 TextDecoder 解码文件内容,若解码失败(例如遇到无效 UTF-8 序列),直接判定为二进制。
    • 检查不可读字符比例,排除合法字符(\r \n\t、空格、标点符号等)。如果不可读字符比例超过 10%,认为是二进制文件。
    • 不可读字符定义为:控制字符(U+0000U+001F 中除 \r\n\t 外的字符)以及 U+007F
  2. 文件大小限制

    • 限制文件大小不超过 40MB(40 * 1024 * 1024 字节)。
    • 超大文件直接返回错误提示,并禁止上传。
  3. 性能优化

    • 只读取文件前 4KB(file.slice(0, 4096)),足以判断文件是否为二进制,且对大文件友好。
  4. 用户体验

    • 通过 <div id="message"> 显示校验结果,成功为绿色,失败为红色。
    • 如果文件不合法(二进制或超大),清空 <input> 元素(event.target.value = ''),防止用户提交。
    • 返回对象 { isValid, message } 提供详细的错误原因,便于用户理解。
  5. 合法字符支持

    • 回车(\r)、换行(\n)、制表符(\t)、空格以及标点符号(如 .,!?;:'")都被视为合法字符,不影响文本可读性判断。
    • 标点符号通常位于 ASCII 范围(0x20 及以上)或 Unicode 标点范围,不会触发不可读条件。

上传逻辑

  • 当前代码仅模拟上传逻辑(打印到控制台)。实际应用中,可以在 result.isValidtrue 时,使用 FormDatafetch 向服务器发送文件,例如:
    if (result.isValid) {
      const formData = new FormData();
      formData.append('file', file);
      try {
        const response = await fetch('/upload', { method: 'POST', body: formData });
        console.log('上传成功:', await response.json());
      } catch (e) {
        console.error('上传失败:', e);
        messageElement.textContent = '上传失败,请重试';
        messageElement.style.color = 'red';
      }
    }
    
  • 服务器端也应校验文件大小和类型,以防止绕过前端限制。

注意事项

  1. 编码支持

    • 代码假设文件为 UTF-8 编码。如果需要支持其他编码(如 GBK、UTF-16),可以使用 jschardet 检测编码并动态设置 TextDecoder 参数。
    • 示例:new TextDecoder(detectedEncoding)
  2. 阈值调整

    • 不可读字符比例阈值(threshold = 0.1)可能需要根据文件类型调整。例如,某些日志文件可能包含较多制表符或换行符,需放宽阈值。
  3. 文件类型辅助校验

    • 可以通过 file.type(MIME 类型)或文件扩展名(如 .txt.csv)辅助判断。例如,限制只允许 text/* 类型:
      if (!file.type.startsWith('text/') && !['.txt', '.csv', '.json'].includes(file.name.slice(file.name.lastIndexOf('.')))) {
        return { isValid: false, message: '不支持的文件类型,仅允许文本文件' };
      }
      
    • 但 MIME 类型和扩展名可能被伪造,因此仍需内容校验。
  4. 边缘情况

    • 空文件或极小文件:代码会正确处理(text.length 为 0 时,unreadableRatio 为 0)。
    • 二进制文件伪装为文本:某些二进制文件可能包含少量可读字符,阈值 0.1 能有效识别大部分情况。
    • 特殊文本文件:如包含大量 Unicode 字符(如中文标点),不会被误判为二进制。
  5. 安全性

    • 前端校验可以被绕过,服务器端必须重复校验文件大小和内容。
    • 建议在服务器端使用类似逻辑(读取文件头,检查不可读字符)或专用库(如 Python 的 chardet)。

测试用例

  1. 合法文本文件.txt 文件,包含字母、数字、空格、回车、换行、标点符号。
  2. 带制表符的文件.csv 文件,包含 \t 分隔符。
  3. 二进制文件.jpg.pdf.exe,应被拒绝。
  4. 超大文件:超过 40MB 的文件,应被拒绝。
  5. 空文件:空 .txt 文件,应通过校验。
  6. 伪装文件:二进制文件重命名为 .txt,应被识别为二进制。

总结

通过 TextDecoder 解码和不可读字符比例检查(排除 \r\n\t 等合法字符),代码能有效判断文件是否为二进制,并禁止二进制文件上传。40MB 大小限制和前 4KB 读取优化确保了性能。