webuploader分片上传示例,服务端上传文件到腾讯云CDN Teo 应用示例

发布于:2025-06-15 ⋅ 阅读:(17) ⋅ 点赞:(0)

本文环境:php7.3.4 CI3.0框架

一、大概步骤:

(1)利用百度的webuploader插件,将大文件分片上传的自己的服务器

(2)利用腾讯云接口从本服务器上传到腾讯云

二、详细代码:

1、进入前端页面的控制器

upload.php 从packageupload函数进入Html页面

class Upload {
	// 进入上传母包页面
	function packageupload(){
		$data['appid']=$this->input->get('appid', TRUE);
		// $data['appid']='iostt';
		if (!empty($data['appid'])) {
			$data['game_info'] = $this->mapp->getApp($data['appid']);
			$this->load->view("bapp/upload_form",$data);
		}else{
			echo '<script>alert("参数错误");</script>';
		}
		
	}
	// 前端分片上传包体 组合完成再统一上传(服务端到CDN传送文件)
	function upload_appid_package() {
		// 禁用输出缓冲区,确保响应立即发送
		if (ob_get_level()) {
			ob_end_clean();
		}
		header('Content-Type: application/json'); // 在函数开头设置 JSON 头部
	
		$appid = $this->input->post('appid');
		if (empty($appid)) {
			echo json_encode(['code' => 400, 'error' => 'App ID is required.']);
			exit;
		}
	
		$file = $_FILES['userfile'];
		$chunk = $this->input->post('chunk', TRUE); // 当前分片索引
		$chunks = $this->input->post('chunks', TRUE); // 总分片数
		$fileName = $this->input->post('name', TRUE); // 原始文件名
		$uniqueId = $this->input->post('uniqueId', TRUE); // 文件唯一标识
	
		if (!$fileName || !$uniqueId) {
			echo json_encode(['code' => 400, 'error' => 'Invalid file or unique ID.']);
			exit;
		}
	    //临时存放的服务器文件夹 确保有存放权限
		$tmpDir = FCPATH . 'uploads/tmp/' . $uniqueId . '/';
		if (!is_dir($tmpDir)) {
			mkdir($tmpDir, 0777, TRUE);
		}
	
		$chunkPath = $tmpDir . $chunk;
		if (!move_uploaded_file($file['tmp_name'], $chunkPath)) {
			echo json_encode(['code' => 400, 'error' => 'Failed to save chunk.']);
			exit;
		}
	
		// 为每个分片返回成功响应
		// echo json_encode(['code' => 200, 'msg' => '分片上传成功', 'chunk' => $chunk]);
		
		// 处理最后一个分片并合并
		if ($chunk == $chunks - 1) {
			$finalPath = FCPATH . 'uploads/' . uniqid() . '_' . $fileName;
			$out = @fopen($finalPath, 'wb');
			if (!$out) {
				echo json_encode(['code' => 400, 'error' => '无法创建最终文件']);
				exit;
			}
	
			// 合并分片
			for ($i = 0; $i < $chunks; $i++) {
				$chunkFile = $tmpDir . $i;
				$in = @fopen($chunkFile, 'rb');
				if ($in) {
					while ($buff = fread($in, 8192)) {
						fwrite($out, $buff);
					}
					fclose($in);
				}
			}
			fclose($out);
	
			
	
			$game_info = $this->mapp->getApp($appid);
			if (empty($game_info)) {
				echo json_encode(['code' => 400, 'data' => null, 'msg' => '游戏不存在']);
				exit;
			}
	
			$extension = pathinfo($fileName, PATHINFO_EXTENSION);
			$version = !empty($game_info['version']) ? number_format($game_info['version'] + 0.1, 1) : '1.0';
			$cpid=$game_info['cpid'];
	
			$cos_path = 'test/' . $cpid . '/' . $cpid . '_' . $version . '.' . $extension;
			if ($game_info['plat'] == 1) {				
				$cos_path = 'package/ios/' . $cpid . '/' . $cpid . '_' . $version . '.' . $extension;
			}
			
				// 腾讯云上传
				$ten=new TencentCosUploader();
				$result = $ten->uploadFileFromPath($finalPath, $cos_path);
			
			

			log_message('error', json_encode($result, JSON_UNESCAPED_UNICODE));
			
			
			if (!empty($result['status'])) {
				//处理成功之后的逻辑

				// 清理临时目录
				$this->cleanDirectory();
	
				echo json_encode(['code' => 0, 'data' => 'success', 'msg' => '上传成功']);
				exit;
			} else {
				// 删除合并后的文件
				if (file_exists($finalPath)) {
					unlink($finalPath);
				}
				// 清理临时目录
				$this->cleanDirectory();
				echo json_encode(['code' => 400, 'data' => $result['error'], 'msg' => '上传失败']);
				exit;
			}
		}
	}

	// 清空临时文件夹
	public function cleanDirectory() {
		// 使用默认路径如果未指定
		$dir = rtrim($dir ?: FCPATH . 'uploads/tmp', DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    
		// 记录开始清理
		log_message('info', '开始清空临时目录: ' . $dir);
		
		// 检查目录是否存在
		if (!is_dir($dir)) {
			log_message('error', '临时目录不存在: ' . $dir);
			return true;
		}
		
		try {
			// 打开目录
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
				RecursiveIteratorIterator::CHILD_FIRST
			);
			
			$success = true;
			
			// 遍历目录
			foreach ($iterator as $item) {
				$path = $item->getPathname();
				
				if ($item->isDir()) {
					// 删除空目录
					if (@rmdir($path) === false) {
						log_message('error', '无法删除目录: ' . $path);
						$success = false;
					}
				} else {
					// 删除文件
					if (@unlink($path) === false) {
						log_message('error', '无法删除文件: ' . $path);
						$success = false;
					}
				}
			}
			
			log_message('info', '临时目录清理完成,状态: ' . ($success ? '成功' : '部分失败'));
			return $success;
			
		} catch (Exception $e) {
			log_message('error', '清理临时目录时发生异常: ' . $e->getMessage());
			return false;
		}	
		
	}

	//获取appid信息,比对version确定是否更新成功
	function getappidinfo(){
		$appid = $this->input->get("appid", true);
		$version = $this->input->get("version", true);
		$data = $this->mapp->getApp($appid);
		$res=['code'=>0,'data'=>$data['version'],'msg'=>'success'];
		if($data['version'] == $version){
			$res['code'] = 1;
		}
		echo json_encode($res);
	} 
}

2、前端代码:upload_form.html
请先下载webuploader相关插件:地址:https://github.com/fex-team/webuploader
这个兄弟也有源码:GitCode - 全球开发者的开源社区,开源代码托管平台

如果都嫌麻烦,也可以通过这个链接下载:https://download.csdn.net/download/weixin_45143733/91008568

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Upload to Tencent COS</title>
    <!-- Bootstrap CSS -->
    <link href="/statics/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
    <!-- WebUploader CSS -->
    <link rel="stylesheet" type="text/css" href="/statics/js/plugins/ueditor/third-party/webuploader/webuploader.css" />
    <!-- jQuery -->
    <script src="/statics/js/jquery-2.1.1.js"></script>
    <script src="/assets/libs/fastadmin-layer/dist/layer.js"></script>
    <style>
        body {
            background-color: #f8f9fa;
        }
        .upload-container {
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .upload-form {
            background-color: white;
            padding: 2rem;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            width: 100%;
            max-width: 500px;
        }
        .btn-upload {
            padding: 10px 20px;
            font-size: 1.1rem;
        }
        #picker {
            display: inline-block;
        }
        .progress-container {
            display: none;
            margin-top: 1rem;
        }
        .error-message {
            display: none;
        }
    </style>
</head>
<body>
    <div class="upload-container">
        <div class="upload-form">
            <h3 class="text-center mb-4">上传母包</h3>
            <div class="alert alert-danger error-message"></div>
            <form id="uploadForm">
                <div class="mb-3">
                    <label for="appid" class="form-label">Appid</label>
                    <input type="text" id="appid" name="appid" class="form-control" readonly value="<?php echo htmlspecialchars($game_info['appid']); ?>">
                </div>   
                <div class="mb-3">
                    <label for="game_name" class="form-label">游戏名</label>
                    <input type="text" class="form-control" readonly value="<?php echo htmlspecialchars($game_info['name']); ?>">
                </div>    
                <div class="mb-3">
                    <label for="userfile" class="form-label">现有版本号</label>
                    <input type="text" id="version" class="form-control" readonly value="<?php echo $game_info['version']?:'1.0'?>">
                </div>  
                <div class="mb-3" style="margin: 1rem;height: 5rem;">
                    <div id="picker">选择文件上传</div>
                </div>
                <div class="d-flex align-items-center">
                    <button type="button" id="uploadButton" class="btn btn-primary btn-upload" disabled>开始上传</button>
                </div>
                <div class="progress-container">
                    <div class="progress">
                        <div class="progress-bar" role="progressbar" style="width: 0%;" id="progressBar">0%</div>
                    </div>
                </div>
            </form>
        </div>
    </div>

    <!-- Bootstrap JS -->
    <script src="/statics/js/bootstrap.bundle.min.js"></script>
    <!-- WebUploader JS -->
    <script src="/statics/js/plugins/ueditor/third-party/webuploader/webuploader.js"></script>
    <script>
        $(document).ready(function() {
            try {
                //console.log('Initializing WebUploader...');
                var uploader = WebUploader.create({
                    auto: false,
                    swf: '/statics/js/plugins/ueditor/third-party/webuploader/Uploader.swf',
                    //处理分片合并的后端函数
                    server: '<?php echo site_url('bapp/upload_appid_package'); ?>',
                    pick: '#picker',
                    chunked: true,
                    chunkSize: 5 * 1024 * 1024, // 5MB 分片
                    threads: 8,
                    fileVal: 'userfile',
                    formData: {
                        appid: $('#appid').val()
                    }
                });

                // 存储服务端返回数据
                // let serverResponses = [];

                //console.log('WebUploader initialized successfully');

                // 更新 appid
                $('#appid').on('change', function() {
                    //console.log('Updating appid:', $(this).val());
                    uploader.option('formData', {
                        appid: $(this).val()
                    });
                });

                // 文件选择前的校验
            uploader.on('beforeFileQueued', function(file) {
                var appid = $('#appid').val().toLowerCase();
                var isIos = appid.indexOf('ios') !== -1;
                var ext = file.ext.toLowerCase();

                if (isIos && ext !== 'ipa') {
                    layer.msg('苹果包,请选择 .ipa 文件!', {icon: 5, shade: [0.3, '#393D49'], time: 1500});
                    return false;
                } else if (!isIos && ext !== 'apk') {
                    layer.msg('安卓包:请选择 .apk 文件!', {icon: 5, shade: [0.3, '#393D49'], time: 1500});
                    return false;
                }
                return true;
            });

                // 文件选择后
                uploader.on('fileQueued', function(file) {
                    console.log('File queued:', file.name);
                    $('#uploadButton').prop('disabled', false);
                    $('.webuploader-pick').text('已选择文件');
                    // 更新 formData 以包含 uniqueId 和 name
                    uploader.option('formData', {
                        appid: $('#appid').val(),
                        uniqueId: file.uniqueId,
                        name: file.name
                    });
                });

                // 分片上传前,添加唯一标识
                uploader.on('beforeFileQueued', function(file) {
                    //console.log('Generating unique ID for file:', file.name);
                    file.uniqueId = WebUploader.Base.guid();
                });

                // 每个分片上传前,确保 formData 包含必要参数
                uploader.on('uploadBeforeSend', function(block, data) {
                    //console.log('Sending chunk:', block.chunk, 'of', block.chunks);
                    data.appid = $('#appid').val();
                    data.uniqueId = block.file.uniqueId;
                    data.name = block.file.name;
                });

                // 上传进度
                uploader.on('uploadProgress', function(file, percentage) {
                    var percent = Math.round(percentage * 100);
                    // //console.log('Upload progress:', percent + '%');
                    $('#progressBar').css('width', percent + '%').text(percent + '%');
                    $('.progress-container').show();
                });

                // 上传成功
                uploader.on('uploadSuccess', function(file, response) {              
                    if (response && typeof response === 'object') {
                        if (response.code === 0) {
                            //这里的成功捕捉不太准确,建议在uploadComplete处理
                            
                        } else if (response.code === 200) {
                            // 单个分片上传成功
                            console.log('分片 ' + response.chunk + ' 上传成功');
                        } else if (response.code ===400) {
                            // 错误响应
                            $('#uploadButton').prop('disabled', false).text('上传失败:'+response.msg);
                            $('.progress-container').hide();
                            $('.error-message').text('上传失败,重试中···').show();
                            resetUploader();
                        }
                    } else {
                        // 无效响应
                        $('#uploadButton').prop('disabled', false).text('上传失败');
                        $('.progress-container').hide();
                        $('.error-message').text('服务器响应无效').show();
                        resetUploader();
                    }
                });
                // 上传错误
                uploader.on('uploadError', function(file, reason) {
                    layer.msg('上传失败', {icon: 5, shade: [0.3, '#393D49'], time: 1500});
                    $('.error-message').text('Upload failed: ' + reason).show();
                    resetUploader();
                });

                // 上传完成
                uploader.on('uploadComplete', function(file, response) {    
                  
                    $.getJSON('<?php echo site_url('bapp/getappidinfo'); ?>', {appid: $('#appid').val(), version: $('#version').val()}, function(data) {
                       if(data.code==0){
                            var newversion='成功升级为:'+data.data;
                            $('#version').val(newversion);
                           // 上传成功
                           layer.msg('上传成功', { icon: 6, shade: [0.3, '#393D49'], time: 2000 });
                           $('.webuploader-pick').css('display', 'none');
                           $('.error-message').css('display', 'none');
                           // 最终上传成功
                           $('#uploadButton').prop('disabled', true).text('上传成功');
                       }else{
                           // 错误响应
                            $('#uploadButton').prop('disabled', false).text('上传失败');
                            $('.progress-container').hide();
                            $('.error-message').text('上传失败').show();
                       }
                    })                    
                   
                    $('#uploadButton').prop('disabled', true).text('上传完成');
                });

                // 开始上传
                $('#uploadButton').on('click', function() {
                    //console.log('Upload button clicked');
                    if (!uploader.getFiles().length) {
                        $('.error-message').text('请选择文件.').show();
                        console.warn('No file selected');
                        return;
                    }
                    if (!$('#appid').val()) {
                        $('.error-message').text('appid 未填写.').show();
                        console.warn('No App ID provided');
                        return;
                    }
                    $(this).prop('disabled', true).html('<span style="color:red">处理中,请勿关闭页面</span>');
                    $('.error-message').hide();
                    uploader.upload();
                });

                function resetUploader() {
                    //console.log('Resetting uploader');
                    $('#progressBar').css('width', '0%').text('');
                    $('.progress-container').hide();
                    uploader.reset();
                }
            } catch (e) {
                console.error('JavaScript error:', e.message, e.stack);
                $('.error-message').text('Initialization failed: ' + e.message).show();
            }
        });
    </script>
</body>
</html>

3、引用Teo接口上传前,请先安装腾讯云的插件

composer命令:composer require qcloud/cos-sdk-v5:^2.6

里面的参数请到腾讯云的管理后台去拿

Tencent.php

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

require_once APPPATH . 'libraries/third_party/vendor/autoload.php';

use Qcloud\Cos\Client;

class TencentCosUploader {
    private $CI;
    private $cosClient;
    private $config;

    public function __construct() {
        $this->CI =& get_instance();      
        $this->initializeCosClient();
    }

    /**
     * 统一配置腾讯云 COS 参数
     * @return array 配置数组
     */
    private function getCosConfig() {
        return [
            'region' => '', // 存储桶地域,如 ap-guangzhou
            'schema' => 'http', // 协议头,http 或 https
            'credentials' => [
                'secretId' => '', // 腾讯云 Secret ID
                'secretKey' => '' // 腾讯云 Secret Key
            ],
            'bucket' => '', // 存储桶名称,格式:BucketName-APPID
            'timeout' => 600, // 请求超时时间(秒)
            'connect_timeout' => 60 // 连接超时时间(秒)
        ];
    }

    /**
     * 初始化 COS 客户端
     */
    private function initializeCosClient() {
        $this->config = $this->getCosConfig();
        $this->cosClient = new Client([
            'region' => $this->config['region'],
            'schema' => $this->config['schema'],
            'credentials' => $this->config['credentials'],
            'timeout' => $this->config['timeout'],
            'connect_timeout' => $this->config['connect_timeout']
        ]);
    }
    /**
     * 从本地路径上传文件到腾讯云 COS
     * @param string $local_file_path 本地文件路径
     * @param string $cos_path 文件在 COS 上的存储路径
     * @return array 上传结果
     */
    public function uploadFileFromPath($local_file_path, $cos_path) {
        if (!file_exists($local_file_path)) {
            return [
                'status' => FALSE,
                'error' => 'Local file does not exist.'
            ];
        }

        // 获取文件信息
        $file_name = basename($local_file_path);
        $extension = pathinfo($file_name, PATHINFO_EXTENSION);
        $extension = $extension ? strtolower($extension) : '';

        // 检查文件大小(3GB 限制)
        $file_size = filesize($local_file_path);
        if ($file_size > 3*1024 * 1024 * 1024) {
            return [
                'status' => FALSE,
                'error' => 'File size exceeds 1GB limit.'
            ];
        }

        // 确保文件可写
        if (!is_writable($local_file_path)) {
            chmod($local_file_path, 0666);
        }

       
        try {
            // 根据文件大小选择上传方式
            $file_handle = fopen($local_file_path, 'rb');
            if ($file_size > 50 * 1024 * 1024) {
                $result = $this->cosClient->upload(
                    $this->config['bucket'],
                    $cos_path,
                    $file_handle,
                    ['PartSize' => 10 * 1024 * 1024]
                );
            } else {
                $result = $this->cosClient->putObject([
                    'Bucket' => $this->config['bucket'],
                    'Key' => $cos_path,
                    'Body' => $file_handle
                ]);
            }

            // 显式关闭文件句柄
            if (is_resource($file_handle)) {
                fclose($file_handle);
            }
             // 删除本地临时文件
             if (!unlink($local_file_path)) {
                log_message('error', 'Failed to delete local file: ' . $local_file_path . ' | Error: ' . error_get_last()['message']);
            }

            return [
                'status' => TRUE,
                'data' => [
                    'file_name' => $file_name,
                    'cos_path' => $cos_path,
                    'url' => $result['Location'],
                    'extension' => $extension,
                ]
            ];
        } catch (\Exception $e) {
            // 显式关闭文件句柄
            if (is_resource($file_handle)) {
                fclose($file_handle);
            }

            return [
                'status' => FALSE,
                'error' => 'COS Upload Failed: ' . $e->getMessage()
            ];
        }
    }

 /**
     * 上传文件到腾讯云 COS
     * @param string $field_name 表单文件字段名
     * @param string $cos_path 文件在 COS 上的存储路径
     * @return array 上传结果
     */
    public function uploadFile($field_name, $cos_path) {
        $upload_path = FCPATH . 'uploads/';

        // 确保上传目录存在
        if (!is_dir($upload_path)) {
            mkdir($upload_path, 0777, TRUE);
        }

        // 验证文件是否存在
        if (!isset($_FILES[$field_name]) || $_FILES[$field_name]['error'] === UPLOAD_ERR_NO_FILE) {
            return [
                'status' => FALSE,
                'error' => 'No file uploaded.'
            ];
        }

        // 检查上传错误
        if ($_FILES[$field_name]['error'] !== UPLOAD_ERR_OK) {
            $errors = [
                UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize.',
                UPLOAD_ERR_FORM_SIZE => 'File exceeds form size limit.',
                UPLOAD_ERR_PARTIAL => 'File only partially uploaded.',
                UPLOAD_ERR_NO_TMP_DIR => 'Temporary directory missing.',
                UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
                UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload.'
            ];
            return [
                'status' => FALSE,
                'error' => isset($errors[$_FILES[$field_name]['error']]) ? $errors[$_FILES[$field_name]['error']] : 'Unknown upload error.'
            ];
        }

        // 检查文件大小(3GB 限制)
        if ($_FILES[$field_name]['size'] > 3*1024 * 1024 * 1024) {
            return [
                'status' => FALSE,
                'error' => 'File size exceeds 1GB limit.'
            ];
        }

        // 获取文件信息
        $original_name = $_FILES[$field_name]['name'];
        $tmp_path = $_FILES[$field_name]['tmp_name'];
        $extension = pathinfo($original_name, PATHINFO_EXTENSION);
        $extension = $extension ? strtolower($extension) : '';

        // 生成本地存储路径
        $local_file_name = uniqid('upload_', true) . ($extension ? '.' . $extension : '');
        $local_file_path = $upload_path . $local_file_name;

        // 移动文件到本地目录
        if (!move_uploaded_file($tmp_path, $local_file_path)) {
            return [
                'status' => FALSE,
                'error' => 'Failed to move uploaded file.'
            ];
        }

        // 确保文件可写
        if (!is_writable($local_file_path)) {
            chmod($local_file_path, 0666);
        }

        try {
            // 根据文件大小选择上传方式
            $file_size = filesize($local_file_path);
            $file_handle = fopen($local_file_path, 'rb');
            if ($file_size > 50 * 1024 * 1024) { // 大于 50MB 使用分片上传
                $result = $this->cosClient->upload(
                    $this->config['bucket'],
                    $cos_path,
                    $file_handle,
                    ['PartSize' => 10 * 1024 * 1024]
                );
            } else {
                $result = $this->cosClient->putObject([
                    'Bucket' => $this->config['bucket'],
                    'Key' => $cos_path,
                    'Body' => $file_handle
                ]);
            }

            // 显式关闭文件句柄
            if (is_resource($file_handle)) {
                fclose($file_handle);
            }

            // 删除本地临时文件
            if (!unlink($local_file_path)) {
                log_message('error', 'Failed to delete local file: ' . $local_file_path . ' | Error: ' . error_get_last()['message']);
            }

            return [
                'status' => TRUE,
                'data' => [
                    'file_name' => $original_name,
                    'cos_path' => $cos_path,
                    'url' => $result['Location'],
                    'extension' => $extension
                ]
            ];
        } catch (\Exception $e) {
            // 显式关闭文件句柄
            if (is_resource($file_handle)) {
                fclose($file_handle);
            }

            // 删除本地临时文件
            if (file_exists($local_file_path) && !unlink($local_file_path)) {
                log_message('error', 'Failed to delete local file: ' . $local_file_path . ' | Error: ' . error_get_last()['message']);
            }

            return [
                'status' => FALSE,
                'error' => 'COS Upload Failed: ' . $e->getMessage()
            ];
        }
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到