文件切片及断点续传

作者: 蓝海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);
    });

相关文章

  • iOS将文件切片储存并上传仿断点续传机制

    iOS将文件切片储存并上传仿断点续传机制 iOS将文件切片储存并上传仿断点续传机制

  • 文件切片及断点续传

    一、初衷、想法 今年上半年的时候写了一个文件切片的库(凭空想象写的,没有结合实际项目开发),近期在使用的时候发现有...

  • AFN下载文件解压文件及断点续传

    下载文件及断点续传:http://www.cnblogs.com/qingche/p/5362592.html 下...

  • NSURLSession实现断点下载

    断点续传概述 断点续传就是从文件上次中断的地方开始重新下载或上传数据,而不是从文件开头。(本文的断点续传仅涉及下载...

  • IOS 断点续传原理浅析(第一篇)

    断点续传概述: 断点续传就是从文件上次中断的地方开始重新下载或上传数据,当下载大文件的时候,如果没有实现断点续传功...

  • iOS-16 断点续传 下载

    断点续传概述: 断点续传就是从文件上次中断的地方开始重新下载或上传数据,当下载大文件的时候,如果没有实现断点续传功...

  • android 中断点续传

    android 中断点续传 单线程断点续传 所谓的断点续传就是在下载一个文件时,文件没有完全下载,中途暂停,那么再...

  • Android下载文件(一)下载进度&断点续传

    索引 Android下载文件(一)下载进度&断点续传 Android下载文件(二)多线程并发&断点续传(待续) A...

  • js大文件断点续传

    以1G的电影为例,断点续传功能的思路是:1、前端将电影切成1024份小片,每份大小是1m2、前端将切片文件进行递归...

  • rsync - 断点续传

    总结   其实,rsync本身就支持断点续传,加上--partial的作用是能实现单个文件内的断点续传(当文件比较...

网友评论

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

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