代码改变世界

php 上传视频到阿里云OSS (不用SDK) 升级版本 断点续传

2025-12-01 17:30  天心PHP  阅读(1)  评论(0)    收藏  举报
<?php
header('Content-Type: text/html; charset=UTF-8');
/**
 * 使用原生HTTP请求上传文件到OSS
 * (大文件分片上传)
 */
 
class AliyunOSSUploader {
    private $accessKeyId;
    private $accessKeySecret;
    private $bucket;
    private $endpoint;
    private $partSize = 5 * 1024 * 1024; // 5MB 分片大小
    private $maxRetries = 3;
    
    public function __construct() {
        $this->accessKeyId = 'AccessKey ID';
        $this->accessKeySecret = 'AccessKey Secret';
        $this->bucket = 'Bucket名称';
        $this->endpoint = 'OSS Endpoint';
    }
    
    /**
     * 生成签名
     */
    private function sign($stringToSign) {
        return base64_encode(hash_hmac('sha1', $stringToSign, $this->accessKeySecret, true));
    }

    /**
     * 生成请求头
     */
    private function generateHeaders($method, $resource, $headers = []) {
        $date = gmdate('D, d M Y H:i:s \G\M\T');
        $contentType = isset($headers['Content-Type']) ? $headers['Content-Type'] : '';
        
        $stringToSign = $method . "\n" 
            . (isset($headers['Content-MD5']) ? $headers['Content-MD5'] : '') . "\n"
            . $contentType . "\n"
            . $date . "\n"
            . $resource;
        
        $signature = $this->sign($stringToSign);
        
        $defaultHeaders = [
            'Date' => $date,
            'Authorization' => 'OSS ' . $this->accessKeyId . ':' . $signature,
            'Content-Type' => $contentType
        ];
        
        return array_merge($defaultHeaders, $headers);
    }
    /**
     * 初始化分片上传
     */
    public function initiateMultipartUpload($objectKey) {
        $resource = '/' . $this->bucket . '/' . $objectKey . '?uploads';
        $url = 'http://' . $this->bucket . '.' . $this->endpoint . '/' . $objectKey . '?uploads';
        
        $headers = $this->generateHeaders('POST', $resource, [
            'Content-Type' => 'application/octet-stream'
        ]);
        
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => $this->formatHeaders($headers),
            CURLOPT_HEADER => true
        ]);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode == 200) {
            $xml = simplexml_load_string(substr($response, strpos($response, '<?xml')));
            return (string)$xml->UploadId;
        }
        
        throw new Exception('初始化分片上传失败: ' . $response);
    }
    
    /**
     * 上传分片
     */
    public function uploadPart($objectKey, $uploadId, $partNumber, $filePath, $offset, $partSize) {
        $resource = '/' . $this->bucket . '/' . $objectKey . '?partNumber=' . $partNumber . '&uploadId=' . $uploadId;
        $url = 'http://' . $this->bucket . '.' . $this->endpoint . '/' . $objectKey . '?partNumber=' . $partNumber . '&uploadId=' . $uploadId;
        
        // 读取文件分片
        $fileHandle = fopen($filePath, 'rb');
        fseek($fileHandle, $offset);
        $partData = fread($fileHandle, $partSize);
        fclose($fileHandle);
        
        // 计算 MD5
        $contentMD5 = base64_encode(md5($partData, true));
        
        $headers = $this->generateHeaders('PUT', $resource, [
            'Content-Type' => 'application/octet-stream',
            'Content-MD5' => $contentMD5,
            'Content-Length' => strlen($partData)
        ]);
        
        // 重试机制
        for ($retry = 0; $retry < $this->maxRetries; $retry++) {
            $ch = curl_init();
            curl_setopt_array($ch, [
                CURLOPT_URL => $url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_PUT => true,
                CURLOPT_HTTPHEADER => $this->formatHeaders($headers),
                CURLOPT_HEADER => true,
                CURLOPT_INFILE => fopen('data://application/octet-stream;base64,' . base64_encode($partData), 'r'),
                CURLOPT_INFILESIZE => strlen($partData)
            ]);
            
            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
            curl_close($ch);
            
            if ($httpCode == 200) {
                $headers = substr($response, 0, $headerSize);
                preg_match('/ETag: "([^"]+)"/', $headers, $matches);
                return isset($matches[1]) ? $matches[1] : '';
            }
            
            // 失败后等待重试
            if ($retry < $this->maxRetries - 1) {
                usleep(pow(2, $retry) * 100000); // 指数退避
            }
        }
    }
    
    /**
     * 格式化请求头
     */
    private function formatHeaders($headers) {
        $formatted = [];
        foreach ($headers as $key => $value) {
            $formatted[] = $key . ': ' . $value;
        }
        return $formatted;
    }
    
    /**
     * 完成分片上传
     */
    public function completeMultipartUpload($objectKey, $uploadId, $parts) {
        ksort($parts);
        $resource = '/' . $this->bucket . '/' . $objectKey . '?uploadId=' . $uploadId;
        $url = 'http://' . $this->bucket . '.' . $this->endpoint . '/' . $objectKey . '?uploadId=' . $uploadId;
        
        // 构建 XML 请求体
        $xml = '<?xml version="1.0" encoding="UTF-8"?>';
        $xml .= '<CompleteMultipartUpload>';
        foreach ($parts as $partNumber => $etag) {
            $xml .= '<Part>';
            $xml .= '<PartNumber>' . $partNumber . '</PartNumber>';
            $xml .= '<ETag>' . $etag . '</ETag>';
            $xml .= '</Part>';
        }
        $xml .= '</CompleteMultipartUpload>';
        
        $headers = $this->generateHeaders('POST', $resource, [
            'Content-Type' => 'application/xml',
            'Content-Length' => strlen($xml)
        ]);
        
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => $this->formatHeaders($headers),
            CURLOPT_POSTFIELDS => $xml,
            CURLOPT_HEADER => false
        ]);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode == 200) {
            return true;
        }
        
        throw new Exception('完成分片上传失败: ' . $response);
    }
    
    /**
     * 执行完整的分片上传
     */
    public function uploadLargeFile($filePath, $objectKey) {
        try {
            // 2. 计算分片数量
            $fileSize = filesize($filePath);
            $partCount = ceil($fileSize / $this->partSize);
            $parts = [];

            $resumeFile = 'oss_upload_' . md5($filePath . $objectKey) . '.json';
            $uploadState = [];

            if (file_exists($resumeFile)) {
                $uploadState = json_decode(file_get_contents($resumeFile), true);
                $uploadId = $uploadState['uploadId'];
                $completedParts = $uploadState['completedParts'];
            } else {
                // 初始化上传
                $uploadId = $this->initiateMultipartUpload($objectKey);
                $completedParts = [];
                $uploadState = [
                    'uploadId' => $uploadId,
                    'objectKey' => $objectKey,
                    'filePath' => $filePath,
                    'completedParts' => $completedParts
                ];
                file_put_contents($resumeFile, json_encode($uploadState));
            }
            // 3. 逐个上传分片
            for ($i = 1; $i <= $partCount; $i++) {
                if (isset($completedParts[$i])) {
                    $parts[$i] = $completedParts[$i];
                    continue; // 跳过已上传的分片
                }
                $offset = ($i - 1) * $this->partSize;
                $currentPartSize = ($i == $partCount) ? ($fileSize - $offset) : $this->partSize;
                
                $etag = $this->uploadPart($objectKey, $uploadId, $i, $filePath, $offset, $currentPartSize);
                $parts[$i] = $etag;
                $uploadState['completedParts'][$i] = $etag;
                file_put_contents($resumeFile, json_encode($uploadState));
                echo "上传分片 {$i}/{$partCount} 完成\n";
            }
            // 4. 完成分片上传
            $this->completeMultipartUpload($objectKey, $uploadId, $parts);
            
            // 清理进度文件
            if (file_exists($resumeFile)) {
                unlink($resumeFile);
            }
            
            return true;
            
        } catch (Exception $e) {
            echo "上传失败: " . $e->getMessage() . "\n";
            return false;
        }
    }
    
    /**
     * 列出已上传的分片
     */
    public function listParts($objectKey, $uploadId) {
        $resource = '/' . $this->bucket . '/' . $objectKey.'?uploadId='.$uploadId;
        $url = "http://{$this->bucket}.{$this->endpoint}/{$objectKey}?uploadId={$uploadId}";
        
        $headers = $this->generateHeaders('GET', $resource,['Host' => $this->bucket . '.' . $this->endpoint]);
        
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $this->formatHeaders($headers)
        ]);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode == 200) {
            $xml = simplexml_load_string($response);
            $parts = [];
            foreach ($xml->Part as $part) {
                $parts[(int)$part->PartNumber] = (string)$part->ETag;
            }
            return $parts;
        }
        return [];
    }
    /**
     * 上传小文件
     */
    public function uploadFile($file_path,$file_type, $oss_path) {
        set_time_limit(0);
        $resource = '/' . $this->bucket . '/' . $oss_path;
        $url = 'http://' . $this->bucket . '.' . $this->endpoint . '/' . $oss_path;
        $file_size = filesize($file_path);
        // 构建请求头        
        $headers = $this->generateHeaders('PUT', $resource, [
            'Host' => $this->bucket . '.' . $this->endpoint,
            'Content-Type' => $file_type,
            'Content-Disposition' => 'inline',
            'Content-Length' => $file_size
        ]);
        // 发送PUT请求
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_PUT, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER,$this->formatHeaders($headers));
        curl_setopt($ch, CURLOPT_INFILE, fopen($file_path, 'r'));
        curl_setopt($ch, CURLOPT_INFILESIZE, $file_size);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        return array('code'=>$http_code,'url'=>$url,'response'=>$response);
    }
    /**
     * 删除文件
     */
    public function deleteOssObject($objectKey) {
        $resource = '/' . $this->bucket . '/' . $objectKey;
        $url = "https://{$this->bucket}.{$this->endpoint}/" . ($objectKey);
        $headers = $this->generateHeaders('DELETE', $resource);
        // 初始化cURL
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
        curl_setopt($ch, CURLOPT_HTTPHEADER, $this->formatHeaders($headers));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        // 执行请求
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        // 检查响应
        if ($httpCode == 204) {
            return true; // 删除成功
        } else {
            return "删除失败,HTTP状态码: {$httpCode},响应: {$response}";
        }
    } 
}
$uploader = new AliyunOSSUploader();

// 上传小文件
if($_FILES && $_FILES['video_file'] && $_FILES['video_file']['size']<100*1024*1024){
    $upload_file = $_FILES['video_file'];
    $oss_filename = 'videos/' . date('Ymd') . '/' . uniqid() . '.' . 
                   pathinfo($upload_file['name'], PATHINFO_EXTENSION);
    $res = $uploader->uploadFile($upload_file['tmp_name'],$upload_file['type'], $oss_filename);
    print_r('<pre>');
    print_r($res);
    print_r('</pre>');
    exit();  
}

// 上传大文件
if($_FILES && $_FILES['video_file'] && $_FILES['video_file']['size']>100*1024*1024){
    $upload_file = $_FILES['video_file'];
    $oss_filename = 'videos/' . date('Ymd') . '/' . uniqid() . '.' . 
                   pathinfo($upload_file['name'], PATHINFO_EXTENSION);
print_r('<pre>');
print_r($upload_file['tmp_name']);
print_r('</pre>');
print_r('<pre>');
print_r($oss_filename);
print_r('</pre>');
exit();
    $result = $uploader->uploadLargeFile($upload_file['tmp_name'], $oss_filename); // 5MB 分片
    if ($result){
        echo "文件上传成功!\n";
        echo "文件地址: http://" . $config['bucket'] . '.' . $config['endpoint'] . '/' . $objectKey . "\n";
    } else{
        echo "文件上传失败!\n";
    }
}

//删除文件
//$res = $uploader->deleteOssObject('videos/20251129/692a87ab21581.mp4');


// 创建上传实例
$uploader = new AliyunOSSUploader();

//列出已上传的分片
/*$res = $uploader->listParts('uploads/DE-GS99864.zip','F198E9334004453FA2E92E2C073ABB62');
print_r('<pre>');
print_r($res);
print_r('</pre>');
exit();*/

// 上传大文件
$filePath = 'DE-GS99864.zip';
$objectKey = 'uploads/DE-GS99864.zip'; // OSS 中的文件路径

$result = $uploader->uploadLargeFile($filePath, $objectKey); // 5MB 分片

if ($result) {
    echo "文件上传成功!\n";
    echo "文件地址: http://" . $config['bucket'] . '.' . $config['endpoint'] . '/' . $objectKey . "\n";
} else {
    echo "文件上传失败!\n";
}
exit();
?>

<form method="post" enctype="multipart/form-data">
    <input type="file" name="video_file" accept="video/*">
    <input type="submit" value="上传视频">
</form>