文件切片及断点续传

作者: 蓝海00 | 来源:发表于2020-08-30 19:55 被阅读0次

    一、初衷、想法

    今年上半年的时候写了一个文件切片的库(凭空想象写的,没有结合实际项目开发),近期在使用的时候发现有些功能并没有考虑周全,然鹅花了这周六日来重写🥺
    之前的思路是,传入文件及对应的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<object> => {
                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<File>, 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<object>) => {
                                            const res = {
                                                md5Keyto62,
                                                fileKey,
                                                nextChunkIndexs: arr
                                            }
                                            resolve(res)
                                            callback && callback(res)
                                        })
    
                                    }, spiltChunkSize)
                                }
                            }
                        });
                    })
                })
            }
    
            // 项目级多个文件上传
            projectFileSectionMultiple = async (fileArr: Array<any>, 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<File>, allChunks: number, md5Keyto62: string): Array<object> => {
                const resParametersArr: Array<object> = []
                // 整理请求参数
                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<object>, nextChunkIndexs: Array<number>) => {
                const resArr: Array<object> = []
                fileArr.forEach((v: any) => {
                    if (nextChunkIndexs.find(index => index == (v.fileInfo.shardIndex))) {
                        resArr.push(v)
                    }
                })
                return resArr;
            }
    
            // 切片开始上传
            toupload = async (uploadArr: Array<object>, 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<object> = [];
        (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<object>) => {
          console.log("多张文件一起上传", val);
        });
    

    相关文章

      网友评论

        本文标题:文件切片及断点续传

        本文链接:https://www.haomeiwen.com/subject/eyuesktx.html