webuploader分片上传示例,服务端上传文件到腾讯云CDN Teo 应用示例
本文环境: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 和 nameuploader.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()];}}
}