断点续存是什么?
断点续传(Resumable File Upload)是一种文件上传的技术,它允许在上传过程中出现中断或失败的情况下,能够从中断的位置继续上传,而不需要重新上传整个文件。这在处理大文件或不稳定的网络连接时非常有用。
断点续传的实现通常涉及以下几个关键概念和步骤:
- 分片:将大文件分割成较小的文件块(通常是固定大小的块),每个块都有一个唯一的标识符。
- 上传请求:客户端发起上传请求,并将文件分片按顺序上传到服务器。
- 上传状态记录:服务器端需要记录上传的状态,包括已接收的分片、分片的顺序和完整文件的大小等信息。
- 中断处理:如果上传过程中发生中断(例如网络中断、用户主动中止等),客户端可以记录已上传的分片信息,以便在恢复上传时使用。
- 恢复上传:当上传中断后再次开始上传时,客户端可以发送恢复上传请求,并将已上传的分片信息发送给服务器。
- 服务器处理:服务器接收到恢复上传请求后,根据已上传的分片信息,判断哪些分片已经上传,然后继续接收剩余的分片。
- 合并文件:当所有分片都上传完成后,服务器将所有分片按顺序组合成完整的文件。
下面是一个简化的断点续传流程图:
客户端 服务器 | | |------ 发起上传请求 ------------------>| | | |------ 上传分片1 -------------------->| | | |------ 上传分片2 -------------------->| | | | ... | | | |------ 上传分片N -------------------->| | | |------ 中断或失败 -------------------->| | | |------ 发起恢复上传请求 --------------->| | | |------ 发送已上传分片信息 ------------>| | | | ... | | | |------ 上传剩余分片 ------------------>| | | |------ 上传完成 ---------------------->| | | |------ 合并分片为完整文件 ------------>| | | |<----- 上传成功响应 --------------------| | |
上述流程图描述了客户端和服务器之间的交互过程。客户端发起上传请求,并逐个上传分片,如果中断或失败,客户端可以恢复上传并将已上传的分片信息发送给服务器。服务器根据已上传的分片信息,继续接收剩余的分片。当所有分片上传完成后,服务器将它们合并成完整的文件,并向客户端发送上传成功的响应。
断点续传技术可以提高文件上传的可靠性和效率,特别是在处理大文件或不稳定的网络环境时。它可以减少重新上传的数据量,节省带宽和时间,并提供更好的用户体验
断点续传实现
1.前端对文件进行分块
2.前端使用多线程上传分片,上传前给服务器发送消息验证当前分片是否已经上传。
3.所有分片上传完毕后,发送合并分片请求,校验文件的完整性。 (上传的分片应该具备顺序标记)
4.前端给服务器传一个MD5值,服务器合并文件后,利用MD5值计算是否与源文件一致。如果不一致,说明文件需要重新上传。
分片文件清理问题:
在数据库中有一张文件表记录minIo中存储的文件信息
文件开始上传时会写入文件表,状态为上传中,上传完成会更新状态为上传完成
当一个文件传了一半不再上传了,说明该文件没有上传完成,通过定时任务去查询文件表中的记录,如果文件距离上次上传结束超过24小时,则可以考虑清除MinIo中相关的分片数据
原理能是一大堆,代码如何去实现呢?
在这我使用的是前段vue3 + 后端的是基于tp5开发的fastadmin框架
前端部分:
<template> <div> <input type="file" @change="selectFile" /> <button @click="upload">上传</button> <progress :value="progress" :max="100"></progress> </div> </template>
script部分:
<template> <div> <input type="file" @change="selectFile" /> <!-- <button @click="upload" :disabled="disabled">上传</button> --> <progress :value="progress" :max="100"></progress> <button @click="toggleUpload">{{ isUploading ? '停止上传' : '开始上传' }}</button> </div> </template> <script setup> import axios from 'axios'; import qs from 'qs'; import { ref, watch ,onMounted } from 'vue'; import { MD5, enc } from 'crypto-js'; const disabled = ref(false); const serializedData = qs.stringify(); const tableData = ref([]); axios.post('/api/uploadss/index', serializedData) .then(response => { console.log(response.data); tableData.value = response.data; }) .catch(error => { console.error(error); }); const encodeMD5 = (md5Hash) => { const part1 = md5Hash.substr(0, 6); const part2 = md5Hash.substr(6, 6); const part3 = md5Hash.substr(12, 6); const part4 = md5Hash.substr(18, 6); const part5 = md5Hash.substr(24, 8); return `${part1}-${part2}-${part3}-${part4}-${part5}`; }; const file = ref(null); const progress = ref(0); const isUploading = ref(false); let filename; const selectFile = (event) => { file.value = event.target.files[0]; filename = `.${file.value.name.split('.')[1]}`; }; let chunkSize; let totalChunks; let currentChunk; let chunkIdValue; const uploadChunk = (start, end) => { const formData = new FormData(); const reader = new FileReader(); reader.onload = () => { const imageData = reader.result; if (currentChunk == 0) { chunkIdValue = MD5(imageData); } formData.append('file', new File([file.value.slice(start, end)], filename)); formData.append('action', currentChunk === totalChunks ? 'merge' : 'clean'); formData.append('chunkindex', currentChunk); formData.append('chunkid', encodeMD5(chunkIdValue.toString(enc.Hex))); formData.append('chunkcount', totalChunks); formData.append('filename', filename); axios.post('/api/uploadss/upload', formData, { headers: { 'Content-Type': 'multipart/form-data', }, onUploadProgress: (progressEvent) => { const progressValue = Math.round((progressEvent.loaded / progressEvent.total) * 100); progress.value = progressValue; }, }) .then((res) => { currentChunk++; if (currentChunk <= totalChunks && isUploading.value) { const start = currentChunk * chunkSize; const end = Math.min(start + chunkSize, file.value.size); uploadChunk(start, end); } else { console.log(res); console.log('上传完成'); } }) .catch((error) => { console.error('上传失败:', error); }); }; reader.readAsDataURL(file.value.slice(start, end)); }; const upload = () => { if (!file.value) { return; } console.log(1111); chunkSize = 2 * 1024 * 1024; // 设置分片大小为8MB totalChunks = Math.ceil(file.value.size / chunkSize); currentChunk = 0; chunkIdValue; const start = 0; const end = Math.min(chunkSize, file.value.size); uploadChunk(start, end); return uploadChunk; }; const toggleUpload = () => { isUploading.value = !isUploading.value; if (isUploading.value) { // 继续上传时,从存储中读取记录并恢复上传状态 const uploadRecord = localStorage.getItem('uploadRecord'); if (uploadRecord) { const { currentChunk, progressValue } = JSON.parse(uploadRecord); progress.value = progressValue; uploadChunk(currentChunk * chunkSize, file.value.size); } } else { // 停止上传时,保存当前的分片索引和进度值到本地存储中 const uploadRecord = JSON.stringify({ currentChunk: currentChunk - 1, progressValue: progress.value, }); localStorage.setItem('uploadRecord', uploadRecord); } }; watch(isUploading, (value) => { if (!value) { // 中断上传并保存记录 console.log('停止上传'); const uploadRecord = JSON.stringify({ currentChunk: currentChunk - 1, progressValue: progress.value, }); localStorage.setItem('uploadRecord', uploadRecord); } else { // 恢复上传 console.log('继续上传'); upload(); } }); onMounted(() => { const handleOnline = () => { console.log('网络已恢复,继续上传'); // if (file.value && progress.value !== 100) { isUploading.value = true; upload(); // } }; const handleOffline = () => { isUploading.value = false; console.log('网络中断,停止上传'); }; window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }); </script>
这些是vue3的script部分其中要注意的是参数的定义,在这里我使用的实例formeData进行传值向后端的接口发起请求,我们这里有调用了一个reader.onload的一个函数用来进行这个异步加载来访问接口.其中他的action的值他其他的值不同与其他的值不同(本来是可用不同的值,fastadmin框架需要这种类型的值)因为上面有一个前段的Md5的36为字符的加密想要使用需要在你的终端上输入
npm install crypto-js //下载md5算法的依赖
这里我们前段的值就传递了大不多了,其中有formData.append('filename', filename);这个值一开始是需要后端返回过来的,但实际上并不是,它是由前端传递他的后缀名;这些值传递完成后就是后端的操作流程到这里前端部分就完成了
后端部分(前面说过了使用的是fastadmin框架完成的后端,也就是需要去改框架代码);
这里我就编写一些需要改的连接以及调用的接口;
public function upload() { Config::set('default_return_type', 'json'); //必须设定cdnurl为空,否则cdnurl函数计算错误 Config::set('upload.cdnurl', ''); $chunkid = $this->request->post("chunkid"); if ($chunkid) { if (!Config::get('upload.chunking')) { $this->error(__('Chunk file disabled')); } $action = $this->request->post("action"); $chunkindex = $this->request->post("chunkindex/d"); $chunkcount = $this->request->post("chunkcount/d"); $filename = $this->request->post("filename"); $method = $this->request->method(true); if ($action == 'merge') { $attachment = null; //合并分片文件 try { $upload = new Upload(); $attachment = $upload->merge($chunkid, $chunkcount, $filename); } catch (UploadException $e) { // return 111; $this->error($e->getMessage()); } $this->success(__('Uploaded successful'), ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]); } elseif ($method == 'clean') { //删除冗余的分片文件 try { $upload = new Upload(); $upload->clean($chunkid); } catch (UploadException $e) { $this->error($e->getMessage()); } $this->success(); } else { //上传分片文件 //默认普通上传文件 $file = $this->request->file('file'); try { $upload = new Upload($file); return $upload->chunk($chunkid, $chunkindex, $chunkcount); } catch (UploadException $e) { $this->error($e->getMessage()); } } } else { $attachment = null; //默认普通上传文件 $file = $this->request->file('file'); try { $upload = new Upload($file); $attachment = $upload->upload(); } catch (UploadException $e) { $this->error($e->getMessage()); } $this->success(__('Uploaded successful'), ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]); } }
这里后端的接口(不要复制了,这里的代码在application/admin/controller/Ajax.php文件),因为使用的它自带的admin的上传(修改的幅度较大)根据情况也可以使用api的上传;首先我们要去打开多图片上传(application/extra/upload.php) 修改 'chunking' => false,改为true;根据上面所需要的值进行传值;这些我们在前段已经定义过了主要去说一下filename的值,这里有个判断是前段定义的,当action的值为不为merge访问chunk方法(地址:application/common/library)这里面都是upload等方法,包括处理分片合并,以及向public/uploads移入都可以.
以上就是简单点断点续传的示例