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

如何获取本地至AWS S3的createMultipartUpload上传进度统计

解决方案:AWS S3分片上传实时进度统计

哦,我明白你的问题了!你之前踩了个小坑——把Guzzle的进度回调加到createMultipartUpload里了,但这个方法其实只是创建分片上传的会话,根本没上传实际的文件内容,所以日志里的上传统计一直是0/0就很正常了。真正的文件上传是在uploadPart步骤,咱们得把回调移到那里才行,同时还要做一些小改造来统计全局进度。

核心思路

  1. 提前获取待上传文件的总大小,作为totalBytesToUpload的基准
  2. 在类中维护一个变量跟踪已上传的总字节数
  3. 将Guzzle的进度回调绑定到uploadPart操作(这才是真正传输文件数据的步骤)
  4. 实时计算已上传字节占总字节的比例,甚至可以估算剩余时间

修改后的完整代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 08:47:32