You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

PHP合并上传文件分片导致文件损坏问题排查

分片上传后文件损坏的排查思路与解决方案

首先,我非常理解你遇到的困扰——分片上传看起来逻辑没问题,但合并后文件总是损坏,尤其是二进制文件(MP4、EXE),这种情况大多和二进制数据的编码/传输/存储过程中的损耗有关。结合你的尝试,我给你梳理下核心问题点和可行的解决方案:

核心问题分析

你目前尝试的两种方案里,Base64成功率稍高但不稳定,multipart完全失效,本质原因都是:

  1. Base64编码的边界与读取问题:Base64是3字节转4字符的编码,虽然你设置了3MB(刚好是3的倍数)的分片,但后端合并时如果用文本模式读取Base64文件(比如fopen($base64File, 'r')),在Windows环境下会自动转换换行符(\n\r\n),导致Base64字符串混入多余字符,解码后破坏二进制结构。
  2. Multipart方案的二进制处理错误:你之前用Storage::put()file_put_contents保存分片时,可能没有正确获取上传文件的原始二进制内容,而是误处理成了文本格式,导致二进制数据丢失。

优先推荐:放弃Base64,直接传输二进制分片

Base64会增加33%的传输体积,还容易引入编码错误,直接传输二进制是最可靠的方案。下面是具体的代码修改:

客户端(Vue.js + Axios)修改

直接用FormData发送Blob切片,避免任何编码转换:

upload(file, start = 0, request = 0) { 
  const chunkSize = 1024 * 1024 * 3; 
  const end = Math.min(start + chunkSize, file.fileObject.size); 
  const slice = file.fileObject.slice(start, end); 
  
  // 用FormData封装分片数据,直接传Blob
  const formData = new FormData();
  formData.append('fileName', file.fileObject.name);
  formData.append('chunkNumber', request + 1);
  formData.append('totalChunks', Math.ceil(file.fileObject.size / chunkSize));
  formData.append('chunk', slice); // 直接传入Blob,Axios自动处理multipart
  
  axios({ 
    url: `/api/admin/batch-sessions/${this.batchSessionId}/files`, 
    method: 'POST', 
    data: formData,
    // Axios会自动设置正确的Content-Type,无需手动指定
  }) 
  .then(res => { 
    start += chunkSize; 
    request++; 
    if (start < file.fileObject.size) { 
      this.upload(file, start, request); 
    } else {
      // 所有分片上传完成,触发后端合并
      axios.post(`/api/admin/batch-sessions/${this.batchSessionId}/files/merge`, {
        fileName: file.fileObject.name,
        directory: `${this.batchSessionId}/${file.fileObject.name}` // 告知后端分片存储目录
      });
    }
  }) 
  .catch(err => { 
    console.error('分片上传失败,可重试:', err.message); 
    // 建议添加重试逻辑,提升稳定性
  }); 
}

服务端(Laravel)修改

1. 接收并保存二进制分片

public function uploadChunk(Request $request) {
    $request->validate([
        'fileName' => 'required|string',
        'chunkNumber' => 'required|integer|min:1',
        'totalChunks' => 'required|integer|min:1',
        'chunk' => 'required|file'
    ]);

    $fileName = $request->fileName;
    $chunkNumber = $request->chunkNumber;
    // 按会话+文件名创建独立目录,避免分片冲突
    $chunkDir = "upload_chunks/{$this->batchSessionId}/{$fileName}";
    Storage::disk('s3-upload-queue')->makeDirectory($chunkDir);

    // 直接保存原始二进制分片,用分片号命名保证顺序
    $request->file('chunk')->storeAs(
        $chunkDir,
        "chunk_{$chunkNumber}",
        's3-upload-queue'
    );

    return response()->json(['success' => true]);
}

2. 按顺序合并二进制分片

public function mergeChunks(Request $request) {
    $request->validate([
        'fileName' => 'required|string',
        'directory' => 'required|string'
    ]);

    $fileName = $request->fileName;
    $chunkDir = $request->directory;
    $mergedPath = "merged_files/{$this->batchSessionId}/{$fileName}";

    // 获取所有分片并按编号排序(关键:必须保证顺序正确)
    $chunkFiles = Storage::disk('s3-upload-queue')->files($chunkDir);
    usort($chunkFiles, function($a, $b) {
        $numA = (int)preg_replace('/.*chunk_(\d+)/', '$1', $a);
        $numB = (int)preg_replace('/.*chunk_(\d+)/', '$1', $b);
        return $numA - $numB;
    });

    // 二进制模式写入合并文件
    $mergedHandle = fopen(Storage::disk('s3-upload-queue')->path($mergedPath), 'wb');
    foreach ($chunkFiles as $chunkFile) {
        // 读取分片的原始二进制内容
        $chunkContent = Storage::disk('s3-upload-queue')->get($chunkFile);
        fwrite($mergedHandle, $chunkContent);
    }
    fclose($mergedHandle);

    // 合并完成后清理分片目录
    Storage::disk('s3-upload-queue')->deleteDirectory($chunkDir);

    return response()->json(['success' => true, 'filePath' => $mergedPath]);
}

如果你坚持用Base64方案(不推荐)

如果暂时不想改二进制方案,可以修复Base64合并的两个关键问题:

  1. 用二进制模式读取Base64文件:避免换行符转换
  2. 逐个分片解码写入:比整体解码更可靠,方便定位错误分片

修改后端合并代码:

public function handle() { 
  $chunkDir = $this->directory;
  $mergedFile = Storage::disk('s3-upload-queue')->path($chunkDir.'/'.basename($chunkDir)); 
  
  // 按分片号排序
  $chunkFiles = Storage::disk('s3-upload-queue')->files($chunkDir);
  usort($chunkFiles, function($a, $b) {
      $numA = (int)preg_replace('/.*chunk_(\d+)/', '$1', $a);
      $numB = (int)preg_replace('/.*chunk_(\d+)/', '$1', $b);
      return $numA - $numB;
  });

  $mfs = fopen($mergedFile, 'wb');
  foreach ($chunkFiles as $chunkFile) {
      // 二进制模式读取Base64分片
      $b64Content = file_get_contents(Storage::disk('s3-upload-queue')->path($chunkFile), false, null, 0, filesize(Storage::disk('s3-upload-queue')->path($chunkFile)));
      $binaryContent = base64_decode($b64Content, true); // 开启严格模式,解码失败返回false
      if ($binaryContent === false) {
          fclose($mfs);
          throw new Exception("分片{$chunkFile}Base64解码失败,请检查上传过程");
      }
      fwrite($mfs, $binaryContent);
  }
  fclose($mfs); 
}

排查步骤(定位具体问题)

如果还是有问题,可以按以下步骤排查:

  1. 对比分片大小:客户端切片的大小(除最后一个)应为3MB,后端保存的分片文件大小必须和客户端一致;如果是Base64分片,长度应为 (分片字节数 +2) //3 *4
  2. 二进制对比文件:用Hex编辑器打开原始文件和合并后的文件,找到第一个差异位置,对应到具体分片,排查该分片的上传/存储过程。
  3. 单分片测试:把整个文件作为一个分片上传,看是否能正常保存,排除分片逻辑之外的问题。
  4. 检查存储介质:确保S3存储的分片没有被自动转码(设置Content-Type为application/octet-stream)。

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

火山引擎 最新活动