本文环境: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()
];
}
}
}