javascript ·

阿里OSS大文件支持刷新的断点续传

在使用OSS时经常会有大文件上传的需求,然而大文件上传经常会遇到因网络原因造成的上传失败,结果就是辛辛苦苦上传了半天的东西失败了,还得重新上传。阿里云OSS提供的分片上传(Multipart Upload)和断点续传功能,可以将要上传的文件分成多个数据块(OSS里又称之为Part)来分别上传,上传完成之后再调用OSS的接口将这些Part组合成一个Object来达到断点续传的效果。

这一应用例子阿里云已经将其整理成了demo上传的git:https://github.com/ali-sdk/ali-oss。

但是在这个demo中只是实现了正常的上传,停止,在上传的一个过程,如果刷新页面之后则会重新开始上传。那么下面我来说一下如何解决刷新后继续断点续传。

基本思路

先来说一下基本的实现思路。断点续传的原理其实是将文件分成多个数据块,然后分别来上传,完成之后在将其组合成一个文件,当上传了一部分之后,你可以发现在oss的文件管理里面碎片增加了,增加的这些碎片便是已经上传成功的部分。

我们首先要做的是向后台获取accessKeyId、accessKeySecret、stsToken、region、bucket和endpoint这些信息。前面几个不再说,demo里面都已经给出了,endpont这个参数在demo中没有给出,如果你的网站是http网站,这个参数可以不用写,但是如果你的网站是https的则该参数是需要必填的,否则会上传失败,因为其默认上传是以http方式上传,当你的网站是https的时候,他会在前面也添加https,但是aliyuncs.com后面你会发现多了一个80端口。解决访问就是添加endpoint参数,强制指定https即可。

获取到了这些信息之后要做的就是构建OSS对象了

const client = new OSS({//这里或者是使用 new OSS.Wrapper({})的方式,这两种方式的区别是什么暂时没有弄清楚
        region,
        accessKeyId: creds.AccessKeyId,
        accessKeySecret: creds.AccessKeySecret,
        stsToken: creds.SecurityToken,
        bucket,
        endpoint:"https://"+region+".aliyuncs.com"
      })

在构造好的这个对象中有一个listUploads接口,通过“prefix”可以获取到目前上传列表中的状态,prefix其实就是我们上传的那个key,比如我们要上传到"oecom/cn/123.jpg",则这个prefix就是"oecom/cn/123.jpg"。那么我们是如何知道即将上传的文件是哪个文件呢,在这里就涉及到了刷新断点续传的一个关键了,那就是将文件上传进度保存到localstorage中。

我们使用文件名加文件大小做md5加密之后为key来存储上传进度,只要在locslstorage中发现了这个key,就说明之前没有上传完成,取出其进度,在此进度之上继续上传即可

具体实现

我们先来说一下所需要的全局变量

let OSS = OSS.Wrapper;//oss对象
let uploadFileClient,currentCheckpoint;//分别是实例化后的client和当前上传进度
let retryCount = 0;//当前重试次数,oss因网络原因上传失败后,重新发起上传请求,只会重试一定次数
const retryCountMax = 3;//最多重试3次
var upload_oss = {//用来存储后台生成的上传文件路径和文件的完整url
    fileKey : "",
    fileUrl : ""
}

然后则是建立OSS连接,也就是我们的主方法:

var applyTokenDo = function (func, inputFile) {
    var filename = inputFile[0].files[0].name;
    var fileSize = inputFile[0].files[0].size;
    var fileInfo = JSON.parse(localStorage.getItem(ossMd5(filename+fileSize)))
    var fileKey = "";
    if(fileInfo){
        var pos = fileInfo.name.lastIndexOf(".");
        var suffix = (pos >= 0) ? fileInfo.name.substring(0,pos) : fileInfo.name;
        fileKey = suffix
        if(fileKey){
            fileKey = "?fileKey="+fileKey;
        }else{
            fileKey = "";
        }
    }

    var url = '/api/checkUploadFileInfo' + fileKey;//文件的上传路径有后台定义,文件名随机生成
    $.get(url, function (cr) {
        upload_oss.fileKey = cr.fileKey;
        upload_oss.fileUrl = cr.fileUrl;
        //cr是后台已经拼接好的参数列表,这里直接使用,额外增加了endpont参数
        cr.endpoint = "https://"+cr.region+".aliyuncs.com"
        var client = new OSS(cr);

        func(client, inputFile);
    });
};

在这个方法中需要传递两个参数,一个是上传文件的函数,一个是上传文件域的input对象,我们在此处聪localStorage中获取上传文件的上传情况,如果之前是上传了一部分,则上传文件的路径不需要重新生成,为了避免文件名称重复而导致的文件覆盖问题,后台的操作策略是采用30位随机字符串作为文件名称。所以如果之前已经存在了,则将存储在localStorage中的文件路径取出来,传给后台,后台发现有这个参数,则直接将此参数返回,不自动生成。对于存储的这个对象具体存储了什么我们后面会详细说。

前面的准备工作基本上做的差不多了,下面就是主要的上传和上传检测了:

var uploadFile = function (client, inputFile) {
    var file = inputFile[0].files[0];
    if (!uploadFileClient || Object.keys(uploadFileClient).length === 0) {
        uploadFileClient = client;
    }
    //取出文件后缀
    var fileName = file.name;
    var fileSize = file.size;
    var pos = fileName.lastIndexOf(".");
    var suffix = (pos >= 0) ? fileName.substring(pos) : "";
    let options = {
        progress,//这里的progress是上传进度的方法,用于更新进度条和获取当前的上传进度,下面会有介绍
        partSize: 500 * 1024,//分片的大小
        timeout: 60000
    };
    upload_oss.fileKey += suffix;
    upload_oss.fileUrl += suffix;
    uploadFileClient.listUploads({"prefix":upload_oss.fileKey}).then(res=> {
        if(res.nextUploadIdMarker){
            let theCheckPoint = JSON.parse(localStorage.getItem(ossMd5(fileName+fileSize)));

            currentCheckpoint = theCheckPoint;
            currentCheckpoint.file = file;
        }
        if (currentCheckpoint) {
            options.checkpoint = currentCheckpoint;
        }
        return uploadFileClient.multipartUpload( upload_oss.fileKey, file,options).then(function (uploadRes) {
            if(currentCheckpoint){
                localStorage.removeItem(ossMd5(currentCheckpoint.file.name+currentCheckpoint.file.size))
            }
            currentCheckpoint = null;
            uploadFileClient = null;
            client_callback.pushUrl(upload_oss, inputFile, uploadRes);
        }).catch((err) => {
            if (uploadFileClient && uploadFileClient.isCancel()) {
                console.log('stop-upload!');
            } else {
                console.error(err);
                console.log(`err.name : ${err.name}`);
                console.log(`err.message : ${err.message}`);
                if (err.name.toLowerCase().indexOf('connectiontimeout') !== -1) {
                    // timeout retry
                    if (retryCount < retryCountMax) {
                        retryCount++;
                        console.error(`retryCount : ${retryCount}`);
                        uploadFile('');
                    }
                }
            }
        });
    });
};

这个uploadFile方法就是applyTokenDo方法传入的第一个参数,uploadFile方法前面的一些不再赘述,单说listUploads方法,通过传递路径参数,来获取未完成的文件信息,我们来看一下下图,下图所示的是其返回结果
阿里OSS大文件支持刷新的断点续传
当这个文件确实没有完成的时候nextUploadIdMarker就是其uploadID,如果没有上传过或已经上传成功,返回结果是空的,我们这里判断nextUploadIdMarker是否存在来表示是否已经上传了一部分,如果是,则从localStorage中取出上传进度,并将当前的文件对象赋值给它,因为checkpoint序列化存入后file对象会丢失,所以这里要重新赋值一下。当上传成功后将localstorage的key移除即可。client_callback方法是业务代码中添加的一个回调函数,和断点续传没有什么关系,可以忽略

下面来看一下进度条:

//更新进度条
var progress = function (p,checkpoint) {
    if(checkpoint){
        var checkOnlyKey = ossMd5(checkpoint.file.name+checkpoint.fileSize);
        localStorage.setItem(checkOnlyKey,JSON.stringify(checkpoint))
    }
    console.log(checkpoint)
    currentCheckpoint = checkpoint;
    return function (done) {
         //更新上传进度条
        var bar = $("#uploadProcess");
        var showText = Math.floor(p * 100) + '%';
        //bar.style.width = showText;
        bar.html(showText);
        done();
    }
};

更新进度条方法就不在赘述了。这里来看一下checkpoint的结构吧:
阿里OSS大文件支持刷新的断点续传
里面的name就是上传文件的路径,其他的参数都是可以看出代表什么的就不再说了。
整个oss大文件支持刷新断点续传基本上就是这些了。

参与评论