文件切片及断点续传

一、初衷、想法

今年上半年的时候写了一个文件切片的库(凭空想象写的,没有结合实际项目开发),近期在使用的时候发现有些功能并没有考虑周全,然鹅花了这周六日来重写🥺
之前的思路是,传入文件及对应的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);
    });
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342