前端代码
<!--文件断点、分片上传-->
<template>
<div class="component-FileSliceUpload">
<el-upload
v-if="showBtn"
:show-file-list="false"
:limit="1"
action
class="mirror-upload"
:http-request="checkUploadProgress"
ref="upload">
<el-button type="primary">点击上传</el-button>
</el-upload>
<div v-else style="margin: 20px auto;">
上传中:<el-progress :percentage="progress" :indeterminate="true" />
</div>
</div>
</template>
<script>
import md5 from './md5.min'
import axios from "axios";
export default {
name: "FileSliceUpload",
components: {},
props: {
//分片的大小,默认50MB
sliceSize:{
type:Number,
default:1024 * 1024 * 50
},
//文件大小限制,默认2GB
sizeLimit:{
type:Number,
default: 1024 * 1024 * 1024 * 2
},
//接口地址
actionUrl:{
type:String,
default:'/fz/module/upload'
}
},
data() {
return {
showBtn:true,
progress:0,
controller:null,
fileCodes:[]
}
},
created() {
},
mounted() {
const storageCodes = window.localStorage.getItem('FZ_UPLOAD_FILE_CODE');
if(storageCodes) this.fileCodes = JSON.parse(storageCodes);
},
filters: {},
watch: {},
computed: {},
methods: {
checkUploadProgress(res){
if(res.file.size > this.sizeLimit){
this.showBtn = true;
this.$popTip.warning('文件大小超出2G,取消上传!')
return
}
const reader = new FileReader();
const fileUniStr = [res.file.name,res.file.size,res.file.type,res.file.lastModified].toString()
this.$emit('processFile',true)
reader.addEventListener('load',e=>{
const fileCode = md5(fileUniStr);
const storageCode = this.fileCodes?.find(i=>i.code===fileCode);
if(storageCode){
this.uploadInit(res,storageCode['lastSlice']+1,fileCode);
}else{
this.setUploadProgress(fileCode,0);
this.uploadInit(res,1,fileCode);
}
});
reader.readAsArrayBuffer(res.file);
},
setUploadProgress(fileCode,sliceIndex){
if(sliceIndex === 0){
this.fileCodes.push({code:fileCode,lastSlice:sliceIndex})
}else if(sliceIndex === -1){
this.fileCodes.splice(this.fileCodes.findIndex(i=>i.code===fileCode),1)
}else{
this.fileCodes.find(i=>i.code===fileCode)['lastSlice'] = sliceIndex
}
window.localStorage.setItem('FZ_UPLOAD_FILE_CODE',JSON.stringify(this.fileCodes))
},
uploadInit(file,startSlice,fileCode){
const blob = file.file;
const fileSize = blob.size;
const fileFullName = blob.name;
const totalSlice = Math.ceil(fileSize / this.sliceSize);
this.showBtn = false;
this.controller = this.uploadController(totalSlice,blob,fileFullName,fileSize,startSlice,fileCode)
this.controller.next()
},
* uploadController(totalSlice,blob,fileFullName,fileSize,startSlice,fileCode){
for (let i = startSlice - 1; i < totalSlice; i++) {
let start = i * this.sliceSize;
let formData = new FormData()
formData.append('file',blob.slice(start, Math.min(fileSize, start + this.sliceSize)));
formData.append('fileName',fileFullName.replace(/(.*)\..*/,'$1'))
formData.append('fileExt',fileFullName.replace(/.*\.(.*)/,'$1'))
formData.append('page',(i+1).toString())
formData.append('totalPage',totalSlice.toString())
const reset = yield this.handleSliceUpload(formData,i+1,totalSlice,fileCode);
if(reset) {
i = totalSlice
this.progress = 0
this.showBtn = true
}
}
},
handleSliceUpload(formData,sliceIndex,totalSlice,fileCode){
return this.sliceUploadApi(formData).then(res=>{
if(res.data.errorcode === 0){
this.progress = Math.round(((sliceIndex)/totalSlice).toFixed(2) * 100);
setTimeout(async ()=>{
if(res.data.status===2){
this.progress = 100
this.showBtn = true
this.setUploadProgress(fileCode,-1)
this.$message.success('上传成功!');
this.$emit('success',res)
this.$emit('processFile',false)
}else if(res.data.status===3){
console.log('分片丢失:',res);
this.$emit('processFile',false)
}else{
this.setUploadProgress(fileCode,sliceIndex)
this.controller.next()
}
},500)
}else{
this.$confirm('发生了一些错误,是否重试?').then(()=>{
this.handleSliceUpload(formData,sliceIndex,totalSlice,fileCode)
}).catch(()=>{
this.controller.next(true)
this.$message.info('已取消上传!');
this.$emit('processFile',false)
})
}
}).catch(e=>{
this.$confirm('服务器未响应,请检查网络连接与服务器状态后重试!','请求失败',
{type:'error',confirmButtonText:'重试',cancelButtonText:'停止上传'}
).then(()=>{
this.handleSliceUpload(formData,sliceIndex,totalSlice,fileCode)
}).catch(()=>{
console.log(e);
this.controller.next(true)
this.$message.info('已取消上传!');
this.$emit('processFile',false)
})
})
},
sliceUploadApi(formData){
const service = axios.create({
baseURL: this.getApiUrl(), // api的base_url
timeout: 1000 * 10 // 请求超时时间
})
return service.post(this.actionUrl,formData)
}
},
}
</script>
<style lang="scss" scoped>
.component-FileSliceUpload {
}
</style>
php代码
/**
* 分片上传
* @param $file
* @param $piece_info
* @return array
* @throws \Exception
*/
public static function piecewiseUpload($file, $piece_info): array
{
if (empty($file)) {
throw new \Exception("请先上传文件");
}
$fileName = $piece_info['fileName'];
$fileExt = $piece_info['fileExt'];
$page = $piece_info['page'];
$totalPage = $piece_info['totalPage'];
$fileTmpName = $file['file']['tmp_name'];
$piece_info['md5'] = md5($fileName . "." . $fileExt);
if ($fileName == '' || $fileExt == '' || $page == '' || $totalPage == '' || $fileTmpName == '') {
return ['status' => 500];
}
$status = 1;
$downUrl = '';
$uploadPath = getConst::getConst('FILE_URL') . '/tmp/piece/';
if (!is_dir($uploadPath)) {
mkdir($uploadPath);
}
$piece_path = getConst::getConst('FILE_URL') . '/tmp/piece/' . md5($fileName) . '/';
if (!is_dir($piece_path)) {
mkdir($piece_path);
}
$upload_path = getConst::getConst('FILE_URL') . '/tmp/upload/';
if (!is_dir($upload_path)) {
mkdir($upload_path);
}
// 上传文件要保存的路径
$fname = sprintf("$piece_path%s-%s.%s", $fileName, $page, $fileExt);
\SysTools::SysLog("分片已上传, 分片地址:" . $fname . " 当前分片数是:$page,总的分片数是:$totalPage");
$data = file_get_contents($fileTmpName);
@file_put_contents($fname, $data);
$service = new FileService();
$missing_piece = [];
// 最后一片文件
if ($totalPage == $page) {
$status = 2;
$uploadFileName = sprintf("$upload_path%s.%s", $fileName, $fileExt);
// 合并文件,删除分片文件
for ($i = 1; $i <= $totalPage; $i++) {
$tmp = sprintf("$piece_path%s-%s.%s", $fileName, $i, $fileExt);
if (!is_file($tmp)) {
$missing_piece[] = $i;
\SysTools::SysLog("分片缺失, 缺失的分片是第{$i}片,分片地址");
}
}
if (count($missing_piece) == 0) {
for ($i = 1; $i <= $totalPage; $i++) {
$tmp = sprintf("$piece_path%s-%s.%s", $fileName, $i, $fileExt);
$data = file_get_contents($tmp);
@file_put_contents($uploadFileName, $data, FILE_APPEND);
@unlink($tmp);
}
$service->rmrf($piece_path);
} else {
$status = 3;
}
$downUrl = "http://" . getConst::getConst('FILE_DOMAIN') . $uploadFileName;
}
// 返回上传状态
return ['status' => $status, 'downUrl' => $downUrl, 'missing_piece' => $missing_piece];
}
/**
* @return array
* @throws \RedisException
* @throws \Exception
*/
public static function checkPieceIntact(): array
{
$redis = \CacheTools::get_redis(14);
$piece_key = 'fz_piece_upload_single_file';
if (!$redis->get($piece_key)) {
throw new \Exception("当前分片丢失,请重新上传");
}
$piece_info = json_decode($redis->get($piece_key), true);
$fileName = $piece_info['fileName'];
$fileExt = $piece_info['fileExt'];
$page = $piece_info['page'];
$totalPage = $piece_info['totalPage'];
$upload_path = getConst::getConst('FILE_URL') . '/tmp/upload/';
if (!is_dir($upload_path)) {
mkdir($upload_path);
}
$uploadFileName = sprintf("$upload_path%s.%s", $fileName, $fileExt);
if (is_file($uploadFileName)) {
return [];
}
$piece_path = getConst::getConst('FILE_URL') . '/tmp/piece/' . md5($fileName) . '/';
if (!is_dir($piece_path)) {
mkdir($piece_path);
}
$intact = [];
for ($i = 1; $i <= $totalPage; $i++) {
$tmp = sprintf("$piece_path%s-%s.%s", $fileName, $i, $fileExt);
if (!is_file($tmp)) {
$intact[$i] = 0;
\SysTools::SysLog("分片缺失, 缺失的分片是第{$i}片,分片地址");
} else {
$intact[$i] = 1;
}
}
return $intact;
}