不积跬步,无以至千里;不积小流,无以成江海。

Dean's blog

  • Join Us on Facebook!
  • Follow Us on Twitter!
  • LinkedIn
  • Subcribe to Our RSS Feed

使用fine-uploader进行分块上传

在上一篇《使用fine-uploader上传文件》中,简单介绍了使用fine-uploader进行文件上传。这个插件也支持分块上传,对于大文件,可以同时分成多个块,同时上传,加快文件上传速度。

客户端配置

var elid = "uploader";
var uploadFile = new qq.FineUploaderBasic({
    maxConnections: 5,  //最多一次上传数量
    debug: false,       // 开启调试模式
    multiple: false,    // 多文件上传
    button: document.getElementById(elid),   //上传按钮
    autoUpload: true, //不自动上传则调用uploadStoredFiless方法 手动上传
    // 验证上传文件
    validation: {
        sizeLimit: config.Maxlength * 1000 * 1000,//单位:byte,在Fine-uploader中,1M=1000000
    },
    // 远程请求地址(相对或者绝对地址)
    request: {
        endpoint: '/api/common/attachment/upload',                  //接口地址
        inputName: "file"                                           //file对应的key
    },
    retry: {
        enableAuto: true,// defaults to false 自动重试
        maxAutoAttempts: 120,//重试120试
        autoAttemptDelay: 30//间隔30秒,相当于一小时
    },
    chunking: {             //断点续传
        enabled: true,
        concurrent: {       //同时上传
            enabled: true
        },
        mandatory: false,   //强制性的【如果为true,则不论大小,都用分块上传】
        paramNames: {
            partIndex: "qqpartindex",
            partByteOffset: "qqpartbyteoffset",
            chunkSize: "qqchunksize",
            totalFileSize: "qqtotalfilesize",
            totalParts: "qqtotalparts"
        },
        partSize: 1024*1024,      //包大小:单位byte,1M为一个包
        success: {                //结束后调用的地址
            endpoint: '/api/common/attachment/upload'
        }
    },
    callbacks: {
        onSubmit: function (fileid, fileName) {                 //选择了文件
            var index1 = fileName.lastIndexOf(".");
            var index2 = fileName.length;
            var suffix = fileName.substring(index1 + 1, index2).toLowerCase();//后缀名

            if(config.Allowed != null){//配置了允许的类型,就做白名单验证
                if(!$.inArray(suffix, config.Allowed.split(","))){
                    top.toastr.warning("上传的附件类型受限");
                    return false;
                }
            } else {//否则做黑名单验证
                if ($.inArray(suffix, limitExts) > -1) {
                    top.toastr.warning("上传的附件类型受限");
                    return false;
                }
            }
        },
        onError: function (fileid, name, reason, maybeXhrOrXdr) {
            alert("上传出错:"+reason);
            uploadFile.cancel(fileid);//如果上传出错,就取消上传
        },

        //上传完成后
        onComplete: function (fileid, fileName, responseJSON) {
            if (responseJSON.success) {
                alert("文件上传成功")
            } else {
                alert("文件上传失败,请重试");
            }
        },

        //分块上传
        onUploadChunk: function (id, name, chunkData) {
            
        },

        //分块完成
        onUploadChunkSuccess: function (id, chunkData, responseJSON, xhr) {
            
        }
    }
});

启用分块上传

chunking: {             //分块上传
    enabled: true,     //启用
    concurrent: {       //同时上传
        enabled: true
    },
    mandatory: false,   //强制性的【如果为true,则不论大小,都用分块上传】
    paramNames: {      //分块上传时的参数
        partIndex: "qqpartindex",
        partByteOffset: "qqpartbyteoffset",
        chunkSize: "qqchunksize",
        totalFileSize: "qqtotalfilesize",
        totalParts: "qqtotalparts"
    },
    partSize: 1024*1024,      //包大小:单位byte,1M为一个包
    success: {                       //分块上传结束后调用的地址
        endpoint: '/api/common/attachment/upload'
    }
}

其中,有几个需要注意的:
chunking.concurrent.enabled   启用同时上传
chunking.mandatory               是否不管大小,都强制分块上传
chunking.partSize                   分块的大小,如果mandatory为false时,小于这个的,则不会分块上传
chunking.success.endpoint      所有分块上传后调用的地址

默认为3个分块上传,如果希望增加,可以调整maxConnections的值。

启用重试

如果考虑网络不稳定的情况,可以使用retry进行重试:

retry: {
    enableAuto: true,// defaults to false 自动重试
    maxAutoAttempts: 120,//重试120次
    autoAttemptDelay: 30//间隔30秒,相当于一小时
}

分块事件

callbacks: {
    //分块上传
    onUploadChunk: function (id, name, chunkData) {
            
    },

    //分块完成
    onUploadChunkSuccess: function (id, chunkData, responseJSON, xhr) {
            
    }
}

服务器端

定义DTO接收上传的数据

public class UploadDTO
{
    #region 上传的文件
    /// <summary>
    /// 文件
    /// </summary>
    public PostedFile file { get; set; }

    #endregion

    #region 启动分块时参数

    #region 启动分块时肯定有的

    /// <summary>
    /// 文件唯一标识
    /// </summary>
    public string QQUuid { get; set; }

    /// <summary>
    /// 文件名称
    /// </summary>
    public string QQFileName { get; set; }

    /// <summary>
    /// 文件大小
    /// </summary>
    public int QQTotalFileSize { get; set; }

    /// <summary>
    /// 分块总数
    /// </summary>
    public int QQTotalParts { get; set; }
    #endregion

    #region 分块上传才有的
    /// <summary>
    /// 分块序号
    /// </summary>
    public int? QQPartIndex { get; set; }

    /// <summary>
    /// 分块偏移量
    /// </summary>
    public int? QQPartByteOffset { get; set; }

    /// <summary>
    /// 分块大小
    /// </summary>
    public int? QQChunkSize { get; set; }

    #endregion

    #endregion

    /// <summary>
    /// 当前上传状态
    /// </summary>
    public UploadType UploadType
    {
        get
        {
            if (QQTotalParts == 0) return UploadType.Normal;
            else return QQPartIndex.HasValue ? UploadType.ChunkingPart : UploadType.ChunkingComplete;
        }
    }
}

Fine-Uploader上传有三种场景:

QQTotalParts为0时,是正常上传
QQTotalParts不为0且有QQPartIndex时,是上传的分块;
QQTotalParts不为0但没有QQPartIndex时,是分块上传完成

UploadType

public enum UploadType
{
    /// <summary>
    /// 正常上传
    /// </summary>
    Normal,
    /// <summary>
    /// 分块上传过程
    /// </summary>
    ChunkingPart,
    /// <summary>
    /// 分块上传完成
    /// </summary>
    ChunkingComplete
}

正常上传

正常上传时,直接处理上传即可:

url = string.Format("/{0}/{1:yyyyMM}/{2}{3}", saveUrl.Trim('/'), DateTime.Now, Guid.NewGuid().ToString().Replace("-", ""), ext);
full = context.Server.MapPath(url);
if (!Directory.Exists(Path.GetDirectoryName(full)))
{
    Directory.CreateDirectory(Path.GetDirectoryName(full));
}

dto.file.SaveAs(full);

上传分块

上传分块时,把上传的分块保存到临时目录。

ext = Path.GetExtension(dto.QQFileName);
url = string.Format("/{0}/{1}/{1}_{2}{3}", tempDirectory, Security.MD5Encrypt(dto.QQUuid), dto.QQPartIndex, ext);
full = context.Server.MapPath(url);
if (!Directory.Exists(Path.GetDirectoryName(full)))
{
    Directory.CreateDirectory(Path.GetDirectoryName(full));
}
dto.file.SaveAs(full);

分块上传和分块完成时,fine-uploader会有一个QQUuid参数,这个同一个文件参数是相同的,使用它作为一个子目录,即可区分不同的文件上传。这里对QQUuid进行MD5计算,避免不必要的风险。分块上传时,原始的文件名,可通过QQFilename获取。

分块完成

分块完成后,将上传的分块合并为一个文件,删除各个分块。

ext = Path.GetExtension(dto.QQFileName);
url = string.Format("/{0}/{1:yyyyMM}/{2}{3}", saveUrl, DateTime.Now, Guid.NewGuid().ToString().Replace("-", ""), ext);
full = context.Server.MapPath(url);
if (!Directory.Exists(Path.GetDirectoryName(full)))
{
    Directory.CreateDirectory(Path.GetDirectoryName(full));
}

#region 合并文件
var key = Security.MD5Encrypt(dto.QQUuid);
var parturl = string.Format("/{0}/{1}", tempDirectory.Trim('/'), key);
var partfull = context.Server.MapPath(parturl);

using (Stream writer = new FileStream(full, FileMode.Create))
{
    for (var i = 0; i < dto.QQTotalParts; i++)
    {
        var fileName = string.Format("{0}_{1}{2}", key, i, ext);
        var part = Path.Combine(partfull, fileName);
        if (File.Exists(part))
        {
            using (Stream stream = new FileStream(part, FileMode.Open))
            {
                stream.CopyTo(writer);
            }
        }
        else
        {
            return new UploadResponse() { success = false, message = "缺少分块文件:" + fileName };
        }
    }
}
#endregion

#region 删除临时文件
try
{
    Directory.Delete(partfull, true);
}
catch { }
#endregion

完整的服务器端代码,从这里下载:

FineUploader-CShare.rar (2.57 kb)

不允许评论
粤ICP备17049187号-1