文件切片及断点续传

一、初衷、想法 今年上半年的时候写了一个文件切片的库(凭空想象写的,没有结合实际项目开发),近期在使用的时候发现有些功能并没有考虑周全,然鹅花了这周六日来重写? 之前的思路是,传入文件及对应的fileKey,然后根据byte来进行切片,切一片发送一片,直到全部发送完毕,执行callback,当时只考虑到了单个文件切片上传。 现在的思路,先全部切片完后再上传,对比后端返回的当前文件还有哪些切片未被上传(实现断点续传),多个文件上传,如果单个切片上传失败单个切片自动进行三次上传请求,所有文件全部上传成功后,再执行callback。over。 其实文件切片及断点续传并不难,最重要的是有思路,然后将自己的思路用代码实现。over。(编程中的任何事情都一样,最重要的是要有思路。) 这篇博客只会抛出代码及部分重要注释,拒绝做复制侠,里面的逻辑并不复杂。 二、后端需要提供的两个接口 2.1 查询当前文件是否已经存在 对前端用户而已来说:提升用户体验、加快文件传输速度 对后端服务来说:减少不必要的带宽占用和磁盘空间的浪费 如果已经存在就不再继续上传该文件,如何判断当前文件是否已存在。唯一id: md5 这里的md5转成了62进制,因为16进制比较长。 2.2 上传文件 将切片文件上传给服务器,服务器进行合成。 三、生成md5+文件切片库 import Vue from 'vue'; import SparkMD5 from 'spark-md5' // 思路 /** * 首先将对传入的文件做MD5加密,根据加密后的MD5查询这个文件是否已被上传过, * 查询这个文件是否“部分”切片被上传,还有哪些切片未被上传,对文件进行切片(根据大小或数量切片) * 返回切片数组 */ class SectionFileNew { // 文件md5加密 getFileMD5 = (file: File, callback?: Function) => { const spark = new (SparkMD5 as any)(), fileReader = new FileReader(); const ops = () => new Promise(resolve => { fileReader.readAsBinaryString(file) fileReader.onload = function (e: any) { spark.appendBinary(e.target.result) const md5key = spark.end() resolve(md5key) callback && callback(md5key) } }) async function invoke() { return await ops() } return invoke() } // 16进制转62进制 string16to62 = (val: any) => { val = parseInt(val, 16) let chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split(''), radix = chars.length, qutient = +val, arr = []; do { const mod = qutient % radix; qutient = (qutient - mod) / radix; arr.unshift(chars[mod]); } while (qutient); return arr.join(''); } // 文件切片 /** * chunkByteSize 字节 默认0.5兆 */ sectionFile = async (file: File, callback: Function, chunkByteSize = 524288, callbackFileType = "file") => { chunkByteSize = chunkByteSize ? chunkByteSize : 524288; // 计算该文件的可分为多少块 const chunks: any = Math.ceil(file.size / chunkByteSize); let sectionFileArray = [] if (chunks === 1) { sectionFileArray.push(file) return callback && callback(sectionFileArray, chunks, this.string16to62(await this.getFileMD5(file))) } // 当前切片 for (let i = 0; i < chunks; i++) { const start = i * chunkByteSize, end = Math.min(start + chunkByteSize, file.size) const blob = file.slice(start, end); // blob 转 file let res = callbackFileType === "file" ? new window.File([blob], file.name, { type: file.type }) : blob; sectionFileArray.push(res) } callback && callback(sectionFileArray, chunks, this.string16to62(await this.getFileMD5(file))) } } export default function () { Vue.prototype.$sectionFileNew = SectionFileNew } export const sectionFileFnNew = SectionFileNew; 四、结合项目封装库 import Vue from "vue" import { sectionFileFnNew } from "./indexNew" import { sectionToUpload, checkHash } from "@/api/http/sectionToUpload"; export default function () { class ProjectFileUploadNew { private section = new (sectionFileFnNew as any)() constructor() { } /** * 项目级单个文件上传 * file:文件 * fileKey:对应上传的key * callback:回调 */ projectFileSection = async (file: File, fileKey: string, callback?: Function): Promise => { return new Promise(async resolve => { await this.section.getFileMD5(file, (md5: string) => { // 检查该文件是否存在此hash checkHash({ hash: this.section.string16to62(md5), fnRes: (res: any) => { // 接口返回正常 if (res.code == 0 && res.data) { const { spiltChunkSize, uploadedSuccess, supportChunkUpload, nextChunkIndexs } = res.data; // 文件未存在 并且支持分片上传 this.fileSection(file, async (fileArr: Array, allChunks: number, md5Keyto62: string) => { // 不支持分片上传 且 该文件未被上传 if (!supportChunkUpload && !uploadedSuccess) { } // 需要文件上传的切片数组 let uploadArr = this.needUploadSectionFileParameters(file, fileArr, allChunks, md5Keyto62) // 断点续传 if (nextChunkIndexs) { uploadArr = this.breakpointResume(uploadArr, nextChunkIndexs) } // 文件已存在 无需再次上传 if (uploadedSuccess) { const res = { md5Keyto62, // md5 fileKey, nextChunkIndexs: [] // 未上传的切片 } resolve(res) return callback && callback(res) } // 开始上传 await this.toupload(uploadArr, fileKey, (arr: Array) => { const res = { md5Keyto62, fileKey, nextChunkIndexs: arr } resolve(res) callback && callback(res) }) }, spiltChunkSize) } } }); }) }) } // 项目级多个文件上传 projectFileSectionMultiple = async (fileArr: Array, callback?: Function) => { if (fileArr.length === 0) return; const res: any = [] for (let i = 0; i < fileArr.length; i++) { res.push(await this.projectFileSection(fileArr[i].file, fileArr[i].filekey)) } return new Promise(resolve => resolve(res)); } /** * 文件切片 * file:文件 * callback:回调 参数(切片后的数组、总共切片数量、62进制的md5) * chunkByteSize:切片大小 */ fileSection = (file: File, callback: Function, chunkByteSize?: number | null) => { this.section.sectionFile(file, callback, chunkByteSize) } /** * 需要上传的切片文件参数整理 * sectionFile:切片文件 * allChunks:总切片数 * md5Keyto62:文件加密后的md5(已转62进制) */ needUploadSectionFileParameters = (file: File, sectionFile: Array, allChunks: number, md5Keyto62: string): Array => { const resParametersArr: Array = [] // 整理请求参数 sectionFile.forEach((v: any, i: number) => { const parameter: any = {} parameter.fileKey = md5Keyto62; parameter.file = v; parameter.fileInfo = { // 文件名称 name: v.name, // 文件后缀 suffix: v.type.substring(v.type.lastIndexOf("/") + 1).toLowerCase(), // 文件大小 size: file.size, use: md5Keyto62, // 分片索引 shardIndex: i + 1, // 分片大小 shardSize: v.size, // 总分片数 shardTotal: allChunks, // 文件md5处理后的key 已转62进制 key: md5Keyto62 } resParametersArr.push(parameter) }) return resParametersArr } /** * 断点续传 整理已经上传过的文件 * file:文件数组 * nextChunkIndexs:剩余需要上传的文件块索引 */ breakpointResume = (fileArr: Array, nextChunkIndexs: Array) => { const resArr: Array = [] fileArr.forEach((v: any) => { if (nextChunkIndexs.find(index => index == (v.fileInfo.shardIndex))) { resArr.push(v) } }) return resArr; } // 切片开始上传 toupload = async (uploadArr: Array, filekey: string, callback: Function,) => { // 反转数组 uploadArr.reverse(); for (let i = uploadArr.length - 1; i >= 0; i--) { // 当前上传未成功 执行三遍 如果三次未成功跳出失败循环 执行下一次 for (let j = 0; j < 3; j++) { const res: any = await this.sendToUpload(uploadArr[i], filekey) if (res.code == 0) { uploadArr.splice(i, 1) break; } } } callback && callback(uploadArr) } // 发送上传请求 sendToUpload = (data: object, filekey: string) => { return new Promise(resolve => { sectionToUpload({ data, filekey, fnRes: (res: any) => { resolve(res); } }); }); } } Vue.prototype.$projectFileUploadNew = ProjectFileUploadNew; } 五、使用 单个文件使用 const section = new (this as any).$projectFileUploadNew(); section .projectFileSection((this.$refs as any).file.files[0], "imageForTest") .then((val: string) => { // 获取md5 key console.log("md5", val); }); 多个文件使用 const fileArr: Array = []; (this.$refs as any).fileMultiple.files.forEach((v: any, i: number) => { fileArr.push({ filekey: "test" + (i + 1), file: v, }); }); const section = new (this as any).$projectFileUploadNew(); section.projectFileSectionMultiple(fileArr).then((val: Array) => { console.log("多张文件一起上传", val); });

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):