PHP合并上传文件分片导致文件损坏问题排查
分片上传后文件损坏的排查思路与解决方案
首先,我非常理解你遇到的困扰——分片上传看起来逻辑没问题,但合并后文件总是损坏,尤其是二进制文件(MP4、EXE),这种情况大多和二进制数据的编码/传输/存储过程中的损耗有关。结合你的尝试,我给你梳理下核心问题点和可行的解决方案:
核心问题分析
你目前尝试的两种方案里,Base64成功率稍高但不稳定,multipart完全失效,本质原因都是:
- Base64编码的边界与读取问题:Base64是3字节转4字符的编码,虽然你设置了3MB(刚好是3的倍数)的分片,但后端合并时如果用文本模式读取Base64文件(比如
fopen($base64File, 'r')),在Windows环境下会自动转换换行符(\n→\r\n),导致Base64字符串混入多余字符,解码后破坏二进制结构。 - 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合并的两个关键问题:
- 用二进制模式读取Base64文件:避免换行符转换
- 逐个分片解码写入:比整体解码更可靠,方便定位错误分片
修改后端合并代码:
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); }
排查步骤(定位具体问题)
如果还是有问题,可以按以下步骤排查:
- 对比分片大小:客户端切片的大小(除最后一个)应为3MB,后端保存的分片文件大小必须和客户端一致;如果是Base64分片,长度应为
(分片字节数 +2) //3 *4。 - 二进制对比文件:用Hex编辑器打开原始文件和合并后的文件,找到第一个差异位置,对应到具体分片,排查该分片的上传/存储过程。
- 单分片测试:把整个文件作为一个分片上传,看是否能正常保存,排除分片逻辑之外的问题。
- 检查存储介质:确保S3存储的分片没有被自动转码(设置Content-Type为
application/octet-stream)。
内容的提问来源于stack exchange,提问作者Aashay Kotasthane




