如何获取本地至AWS S3的createMultipartUpload上传进度统计
解决方案:AWS S3分片上传实时进度统计
哦,我明白你的问题了!你之前踩了个小坑——把Guzzle的进度回调加到createMultipartUpload里了,但这个方法其实只是创建分片上传的会话,根本没上传实际的文件内容,所以日志里的上传统计一直是0/0就很正常了。真正的文件上传是在uploadPart步骤,咱们得把回调移到那里才行,同时还要做一些小改造来统计全局进度。
核心思路
- 提前获取待上传文件的总大小,作为
totalBytesToUpload的基准 - 在类中维护一个变量跟踪已上传的总字节数
- 将Guzzle的进度回调绑定到
uploadPart操作(这才是真正传输文件数据的步骤) - 实时计算已上传字节占总字节的比例,甚至可以估算剩余时间
修改后的完整代码
private $uploadedBytes = 0; // 新增:跟踪全局已上传字节数 private $uploadStartTime; // 新增:记录上传开始时间用于估算剩余时间 public function uploadMultipart($file, $path, $metaDataValues = []) { ini_set('max_execution_time', 0); $parts = array(); $totalBytes = filesize($file); // 获取文件总大小 $partSize = 500 * 1024 * 1024; // 分片大小,保持你原来的设置 $this->uploadStartTime = time(); // 初始化上传开始时间 // 初始化分片上传会话(这里不需要进度回调) $result = $this->s3Client->createMultipartUpload([ 'Bucket' => $this->bucket, 'Key' => $path, 'StorageClass' => 'STANDARD', 'ACL' => 'public-read', 'Metadata' => $metaDataValues ]); $uploadId = $result['UploadId']; // 上传文件分片 try { $fileHandle = fopen($file, 'r'); $partNumber = 1; while (!feof($fileHandle)) { $partData = fread($fileHandle, $partSize); $currentPartSize = strlen($partData); // 记录当前分片的实际大小(最后一个分片可能更小) $result = $this->s3Client->uploadPart([ 'Bucket' => $this->bucket, 'Key' => $path, 'UploadId' => $uploadId, 'PartNumber' => $partNumber, 'Body' => $partData, '@http' => [ 'progress' => function ($downloadTotalSize, $downloadSizeSoFar, $uploadTotalSize, $uploadSizeSoFar) use ($totalBytes, $partNumber, $partSize) { // 计算已完成分片的总大小 + 当前分片已上传的字节 $completedPartsTotal = ($partNumber - 1) * $partSize; $currentTotalUploaded = $completedPartsTotal + $uploadSizeSoFar; // 计算进度百分比 $progressPercent = round(($currentTotalUploaded / $totalBytes) * 100, 2); // 估算剩余时间(假设当前上传速度稳定) $elapsedSeconds = time() - $this->uploadStartTime; $uploadedBytesPerSecond = $elapsedSeconds > 0 ? $currentTotalUploaded / $elapsedSeconds : 0; $remainingBytes = $totalBytes - $currentTotalUploaded; $remainingSeconds = $uploadedBytesPerSecond > 0 ? $remainingBytes / $uploadedBytesPerSecond : 0; $this->loggerService->logError( "上传进度: {$currentTotalUploaded}/{$totalBytes} 字节 ({$progressPercent}%)", [ "剩余时间估算" => gmdate("H:i:s", $remainingSeconds), "当前分片进度" => "{$uploadSizeSoFar}/{$uploadTotalSize} 字节" ] ); } ] ]); // 分片上传完成后,更新全局已上传字节数 $this->uploadedBytes += $currentPartSize; $parts['Parts'][$partNumber] = [ 'PartNumber' => $partNumber, 'ETag' => $result['ETag'], ]; $partNumber++; } fclose($fileHandle); } catch (S3Exception $e) { $this->s3Client->abortMultipartUpload([ 'Bucket' => $this->bucket, 'Key' => $path, 'UploadId' => $uploadId ]); $this->logger->error("Error multipart uploading: " . $e->getMessage() . " on " . $e->getLine() . " for " . $e->getFile()); $this->uploadedBytes = 0; // 重置进度 throw $e; // 重新抛出异常,方便上层处理 } // 完成分片上传 $result = $this->s3Client->completeMultipartUpload([ 'Bucket' => $this->bucket, 'Key' => $path, 'UploadId' => $uploadId, 'MultipartUpload' => $parts, ]); $this->uploadedBytes = 0; // 上传完成后重置进度 return $result['Location']; }
关键细节说明
- 为什么回调要放在
uploadPart?:createMultipartUpload只是向S3发送一个初始化请求,不传输任何文件数据,所以进度回调不会触发有效统计。而uploadPart才是真正把分片数据发送到S3的步骤,每个分片上传时都会触发Guzzle的进度回调。 - 分片大小的注意事项:你设置的500M分片大小其实超出了AWS的建议范围(官方建议5MB-100MB),过大的分片可能会增加重试成本,如果网络不稳定容易导致整个分片上传失败,建议根据实际网络情况调整。
- 前端获取进度的方案:如果需要把进度展示给前端,你可以:
- 把
$uploadedBytes和$totalBytes存储到Redis等缓存中 - 前端通过AJAX轮询一个接口,从缓存中读取实时进度
- 或者使用WebSocket实现主动推送进度
- 把
补充:关于之前的JavaScript方案
你尝试的XHR上传进度只能统计用户浏览器到你的服务器的传输进度,而你的服务器才是和S3交互的主体,所以前端无法直接获取服务器到S3的上传进度,必须在服务器端统计后再传递给前端。
内容的提问来源于stack exchange,提问作者Kamuffel
相关产品推荐
相关产品推荐

