You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:向Node.js发送二进制文件分片后重构文件异常问题

解决文件分片上传后重构格式错误的问题

嘿,我之前也踩过这个坑!文件分片重构后格式不对,十有八九是后端写入时的编码处理出了问题——毕竟Node.js的fs.createWriteStream默认如果没明确配置,很容易把二进制数据当成字符串来处理,直接破坏了原始文件的字节结构。下面是具体的排查和解决方法:

核心问题分析

前端用file.slice切出的是Blob(二进制数据),浏览器发送的也是原始字节流,但后端如果在写入时错误地指定了编码(比如默认的utf8),会把Buffer转换成字符串再写入,这会导致二进制文件(比如图片、视频、压缩包)的字节被篡改,最终文件损坏。

解决方案步骤

1. 前端确保二进制发送正确

发送分片时直接传递file.slice得到的Blob,不要做额外的编码转换,同时设置正确的请求头:

const file = document.querySelector('input[type="file"]').files[0];
const chunkSize = 1024 * 1024; // 1MB分片大小
const totalChunks = Math.ceil(file.size / chunkSize);

async function uploadChunks() {
  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);

    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/upload-chunk', true);
    // 明确告诉后端这是二进制流
    xhr.setRequestHeader('Content-Type', 'application/octet-stream');
    // 传递分片索引、总数、文件名等元信息
    xhr.setRequestHeader('Chunk-Index', i);
    xhr.setRequestHeader('Total-Chunks', totalChunks);
    xhr.setRequestHeader('File-Name', file.name);

    await new Promise((resolve, reject) => {
      xhr.onload = () => resolve();
      xhr.onerror = () => reject(new Error('分片上传失败'));
      xhr.send(chunk); // 直接发送Blob,无需转换
    });
  }
}

// 触发上传
document.querySelector('#upload-btn').addEventListener('click', uploadChunks);

2. 后端正确接收并写入二进制数据

关键是不要设置编码,让fs.createWriteStream直接处理Buffer。以Express为例:

const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();

// 用raw中间件接收二进制数据,不要用json/urlencoded中间件
app.use(express.raw({ type: 'application/octet-stream', limit: '10mb' }));

// 临时存储分片的目录
const tempChunkDir = path.join(__dirname, 'temp-chunks');
const uploadDir = path.join(__dirname, 'uploads');

// 创建必要的目录
[tempChunkDir, uploadDir].forEach(dir => {
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
});

app.post('/upload-chunk', (req, res) => {
  const chunkIndex = parseInt(req.headers['chunk-index']);
  const totalChunks = parseInt(req.headers['total-chunks']);
  const fileName = req.headers['file-name'];

  // 写入分片到临时文件,关键:encoding设为null,直接处理Buffer
  const chunkPath = path.join(tempChunkDir, `${fileName}.chunk${chunkIndex}`);
  const writeStream = fs.createWriteStream(chunkPath, { encoding: null });

  writeStream.write(req.body); // req.body是原始Buffer
  writeStream.end();

  writeStream.on('finish', async () => {
    // 检查所有分片是否已上传
    const allChunksUploaded = Array.from({ length: totalChunks }, (_, i) => {
      return fs.existsSync(path.join(tempChunkDir, `${fileName}.chunk${i}`));
    }).every(Boolean);

    if (allChunksUploaded) {
      // 合并所有分片到最终文件
      const finalFilePath = path.join(uploadDir, fileName);
      const finalWriteStream = fs.createWriteStream(finalFilePath, { encoding: null });

      for (let i = 0; i < totalChunks; i++) {
        const chunkBuffer = fs.readFileSync(path.join(tempChunkDir, `${fileName}.chunk${i}`));
        finalWriteStream.write(chunkBuffer);
        // 删除临时分片
        fs.unlinkSync(path.join(tempChunkDir, `${fileName}.chunk${i}`));
      }

      finalWriteStream.end();
      finalWriteStream.on('finish', () => {
        res.status(200).send('文件上传并合并完成!');
      });
    } else {
      res.status(200).send('分片上传成功,等待其余分片');
    }
  });

  writeStream.on('error', err => {
    console.error('分片写入失败:', err);
    res.status(500).send('分片上传失败');
  });
});

app.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

额外注意事项

  • 禁止转换编码:绝对不要把req.body转成字符串(比如req.body.toString()),直接写入Buffer。
  • 分片顺序要严格:合并时必须按分片索引从小到大写入,否则文件会乱序损坏。
  • 边界处理:前端file.slice的end值要取Math.min(start + chunkSize, file.size),避免最后一个分片出现空字节。
  • 大文件优化:如果处理超大文件,建议用fs.createReadStream读取分片再写入,避免一次性读取Buffer占用过多内存。

内容的提问来源于stack exchange,提问作者k1r0s

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 10:02:52