之前做了个使用vue-simple-uploader实现大文件分片上传功能,前端的代码参考夏大师的基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件 基本没有问题。
但是,这位老哥没有给后端接口的代码,于是我只能不断看组件的开发文档、以及网上其他分片上传的后端接口。终于自己摸索出了一个后端接口。
以下代码使用.net 开发,且只简述分片上传的部分,不包括单文件上传
我第一次为这个组件写接口时,把文件上传和分片校验放在了同一个接口,导致一直开发失败。后来仔细阅读博客时注意到夏大师有提到第一次会发起请求校验已上传的分片,然后才开始上传。
分片校验
于是我就单独写个接口来返回已校验的分片。

	    /// <summary>
        /// 分片上传文件前,检查哪些文件已上传
        /// </summary>
        /// <param name="chunkNumber">当前块的次序,第一个块是 1,注意不是从 0 开始的。</param>
        /// <param name="chunkSize">分块大小,根据 totalSize 和这个值你就可以计算出总共的块数。注意最后一块的大小可能会比这个要大</param>
        /// <param name="currentChunkSize">当前块的大小,实际大小</param>
        /// <param name="totalSize">文件总大小。</param>
        /// <param name="identifier">文件的唯一标示。MD5</param>
        /// <param name="filename">文件名。</param>
        /// <param name="relativePath">文件夹上传的时候文件的相对路径属性。</param>
        /// <param name="totalChunks">文件被分成块的总数。</param>
        /// <returns></returns>
        [HttpGet]
        public AjaxResponse Upload([FromUri] int chunkNumber, [FromUri] int chunkSize, [FromUri] int currentChunkSize
            , [FromUri] int totalSize, [FromUri] string identifier, [FromUri] string filename, [FromUri] string relativePath, [FromUri] int totalChunks)
        {
            //这个是我自己的一个sql查询,根据前端请求发来的请求中的MD5码,查询是否已上传过完整的文件。
            //在所有分片都已经上传后生成的数据库记录
            bool fileExists = _fileAppService.GetFileIsExistsByMD5(identifier) != null;
            if (fileExists)//已上传过相同MD5码的文件
                return new AjaxResponse
                {
                    Result = new
                    {
                    //skipUpload  是我自己定义用来提示前端可以跳过已上传的部分
                        skipUpload = true
                    },
                    Success = true
                };
            //如果文件暂未完成上传,查询已上传的分片文件
            string filePath = HttpContext.Current.Server.MapPath($"/UploadFiles/temp/{identifier}");
            var files = new List<string>();

            //string httpMethod = HttpContext.Current.Request.HttpMethod;
            DirectoryInfo folder = new DirectoryInfo(filePath);
            if (folder.Exists)
                folder.GetFiles().ToList().ForEach(s =>
                {
                //vue-simple-uploader根据索引数组判断是否已上传
                //返回格式参考:[2, 3, 4, 5, 6]
                //s.name 我设置的分片文件名为:1.temp、2.temp、3.temp....
                    files.Add(s.Name.Substring(0, s.Name.LastIndexOf(".")));
                });
            var result = new
            {
                uploaded = files
            };

            return new AjaxResponse
            {
                Result = result,
                Success = true
            };

        }

检验完成后,如果有未上传的分片文件,则组件会开始发起发送分片的请求

		/// <summary>
        /// 校验完成后,上传分片文件
        /// 重载,无参数的同名Upload方法
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        public AjaxResponse Upload()
        {
        //md5码
            string identifier = HttpContext.Current.Request.Params["identifier"] ?? "";
		// 分片索引
            string chunkNumber = HttpContext.Current.Request.Params["chunkNumber"] ?? "";
			//分片总数
            string totalChunks = HttpContext.Current.Request.Params["totalChunks"] ?? "";
            string filePath = HttpContext.Current.Server.MapPath($"/UploadFiles/temp/{identifier}");

            string fileName = $"{filePath}/{chunkNumber}.temp";//文件完全路径名

            Directory.CreateDirectory(filePath);
            var fileData = HttpContext.Current.Request.Files["file"];//前端{fileParameterName: 'file'}设置的参数
            //保存上载的内容
            fileData.SaveAs(fileName);
            return new AjaxResponse
            {
                Result = chunkNumber == totalChunks ? "needMerge" : "分片上传完成",//全部上传完成后,返回needMerge(自定义标识)表示可以开始进行合并了
                Success = true
            };
        }

当所有的分片都上传完后,上面的方法会返回needMerge 提示前端发起合并请求

		/// <summary>
        /// 文件合并
        /// </summary>
        /// <param name="FileBelongChild">归属子ID</param>
        /// <param name="FileBelongID">归属主ID</param>
        /// <param name="identifier">文件的唯一标示</param>
        /// <param name="fileName">文件名(包含后缀名)</param>
        /// <param name="fileSize">文件大小</param>
        /// <param name="needMerge">是否需要合并</param>
        [HttpGet]
        public void FileMerge(string FileBelongChild, string FileBelongID, string identifier, string fileName, int fileSize, bool needMerge)
        {

            string fileId = Guid.NewGuid().ToString();
            // 创建数据库记录,记载文件的上传信息
            var input = new Files
            {
                Id = fileId,
                FileName = fileName,
                Size = fileSize,
                ...
                FileMD5 = identifier,
            };
            if (needMerge)//校验MD5码,如果附件已上传过,则不需要重复合并
                //最终保存的文件为MD5码+后缀名组成的文件
                FileMerge(identifier + input.FileNameSuffix, identifier);
            _fileAppService.Insert(input);//创建文件信息
        }

		/// <summary>
        /// 分片合并
        /// </summary>
        /// <param name="fileName">文件名</param>
        /// <param name="tempFolder">临时文件夹(以文件MD5码命名)</param>
        public static void FileMerge(string fileName, string tempFolder)
        {

            string filePath = System.Web.HttpContext.Current.Server.MapPath($"/UploadFiles/");
            string tempPath = $"{filePath}temp/{tempFolder}";
            List<string> files = Directory.GetFiles(tempPath).OrderBy(s => s).ToList();
            if (!(files.Count > 0))
            {
                throw new Exception("文件列表为空");
            }
            //确保所有的文件都存在
            foreach (string item in files)
            {
                if (!File.Exists(item))
                {
                    throw new Exception(string.Format("文件{0}不存在", item));
                }
            }
            byte[] buffer = new byte[1024 * 100];
            using (FileStream outStream = new FileStream(filePath + fileName, FileMode.Create))
            {
                int readedLen = 0;
                FileStream srcStream = null;
                for (int i = 0; i < files.Count; i++)
                {
                    srcStream = new FileStream(files[i], FileMode.Open);
                    while ((readedLen = srcStream.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        outStream.Write(buffer, 0, readedLen);
                    }
                    srcStream.Close();
                }
            }
			//合并完后,删除掉临时的工具人文件夹
            DirectoryInfo dir = new DirectoryInfo(tempPath);
            dir.Delete(true);//删除子目录和文件
        }

前端的代码参考夏大师的代码,我这里说一下一些要点,各位后来者可以少走点弯路。

	  //options 里的target
      // 我一开始也是很费解,就一个接口名,怎么同时完成校验和附件上传,直到后来知道分片校验和文件上传是分开进行的,便用重载方法名来解决了。
      target: '域名/Upload', // 目标上传 URL
		......
		//option 里的 checkChunkUploadedByResponse 函数
		// 服务器分片校验函数,秒传及断点续传基础
          checkChunkUploadedByResponse: function (chunk, message) {
            let objMessage = JSON.parse(message);
			//参考后端代码里提到的skipUpload,校验时,如果遇到后端返回该参数则表示已经上传过相同MD5码的文件了
            if (objMessage.result.skipUpload) {
              return true;
            }
            //objMessage.uploaded = [2, 3, 4, 5, 6, 8, 10, 11, 12, 13, 17, 20, 21]
            return (objMessage.result.uploaded || []).indexOf((chunk.offset + 1).toString()) >= 0
          },
		......
		//文件上传成功后
		onFileSuccess(rootFile, file, response, chunk) {
         ...
        // 如果服务端返回需要合并
         if (res.result == 'needMerge') 
         //发起 FileMerge 请求
        this.$emit('uploadSuccess',
          file,
          !res.result.skipUpload
        )
        this.prompt = `上传完毕,正在合并分片文件...`;
        / else { // 不需要合并    
          console.log('上传成功');
          ...
         }
      },

2021-10-22 补充 关于前端option 携带参数怎么接收。我当时使用的get请求,所以后端需要逐个声明参数,关于如何使用post 发送复杂参数的没有研究过。
前端option完整的参数示例:

options: {
          target: baseUrl + '/api/Files/Upload', // 目标上传 URL
          chunkSize: '2048000', //分块大小
          fileParameterName: 'file', //上传文件时文件的参数名,默认file
          maxChunkRetries: 3, //最大自动失败重试上传次数
          testChunks: true, //是否开启服务器分片校验
          // 服务器分片校验回调函数,秒传及断点续传基础
          checkChunkUploadedByResponse: function (chunk, message) {
            let objMessage = JSON.parse(message);
            if (objMessage.result.skipUpload) {
              return true;
            }
            return (objMessage.result.uploaded || []).indexOf((chunk.offset + 1).toString()) >= 0
          },
          headers: {
            // 在header中添加的验证,请根据实际业务来
            Authorization: getToken(),
            'Access-Control-Allow-Origin': '*',
          },
        },

后端接口在页面的最上方,就是这个:

public AjaxResponse Upload([FromUri] int chunkNumber, [FromUri] int chunkSize, [FromUri] int currentChunkSize
            , [FromUri] int totalSize, [FromUri] string identifier, [FromUri] string filename, [FromUri] string relativePath, [FromUri] int totalChunks)

具体内容往上翻

Logo

前往低代码交流专区

更多推荐