搭建Wpf框架(17) ——大文件上传与下载

发布时间 2023-09-23 23:50:06作者: 竹天笑

先上效果图:

大文件上传

1.客户端需要按照块拆成一块一块,先计算大小,然后计算块的个数,然后按块逐个上传,代码如下:

public async Task<UploadResult> UploadFileChunck(string path, Action<double> progressAction)
        {
            try
            {
                var fStream = File.OpenRead(path);
                int chunckSize = 2097152;//2MB
                int totalChunks = (int)(fStream.Length / chunckSize);
                if (fStream.Length % chunckSize != 0)
                {
                    totalChunks++;
                }

                double progress = 0d;
                progressAction?.Invoke(progress);

                var tempDirectory = Guid.NewGuid().ToString("N");
                UploadResult result = null;
                for (int i = 0; i < totalChunks; i++)
                {
                    long positon = (i * (long)chunckSize);
                    int toRead = (int)Math.Min(fStream.Length - positon, chunckSize);
                    byte[] buffer = new byte[toRead];
                    await fStream.ReadAsync(buffer, 0, buffer.Length);

                    using (MultipartFormDataContent data = new MultipartFormDataContent())
                    {
                        data.Add(new StringContent(tempDirectory ?? ""), "tempDirectory");
                        data.Add(new StringContent(i.ToString()), "index");
                        data.Add(new StringContent(totalChunks.ToString()), "total");
                        data.Add(new ByteArrayContent(buffer), "file", Path.GetFileName(path));

                        var content = await PostAsync(string.Format("{0}/Base_Manage/Upload/UploadFileChunck", Url), data, TimeOut, Header.GetHeader());
                        result = JsonConvert.DeserializeObject<AjaxResult<UploadResult>>(content)?.Data;

                        progress += 1d / totalChunks;
                        progressAction?.Invoke(progress);
                    }
                }
                fStream.Close();
                return result;
            }
            catch (Exception ex)
            {
                return new UploadResult() { status = ex.Message };
            }
        }

注:加了一个Action用于通知进度 2.服务端需要一块一块接送,直接最后一块接收后,把文件合并,删除块文件,结束,代码如下:

       #region  大文件上传
        /// <summary>
        /// 上传附件
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        public async Task<ActionResult> UploadFileChunck(IFormFile file, string tempDirectory, int index, int total)
        {
            if (file == null)
            {
                return BadRequest("请选择上传文件");
            }

            string fileUploadPath = GetUploadPath();
         
            string tmp = Path.Combine(fileUploadPath, tempDirectory) + "/";//临时保存分块的目录
            if (index == 0)
            {
                if (Directory.Exists(tmp))
                {
                    Directory.Delete(tmp);
                }
            }
            try
            {
                using (var stream = file.OpenReadStream())
                {
                    var strmd5 = GetMD5Value(stream);
                    //if (md5 == strmd5)//校验MD5值
                    //{
                    //}

                    if (await Save(stream, tmp, index.ToString()))
                    {
                      
                        bool mergeOk = false;
                        string path = "";
                        string physicPath = "";
                        if (total - 1 == index)
                        {
                            path = $"/Upload/{Guid.NewGuid().ToString("N")}/{file.FileName}";
                            physicPath = GetAbsolutePath($"~{path}");
                            string dir = Path.GetDirectoryName(physicPath);
                            if (!Directory.Exists(dir))
                                Directory.CreateDirectory(dir);

                            mergeOk = await FileMerge(tmp, physicPath);
                            if (mergeOk)
                            {
                                _logger.LogInformation($"文件上传成功:{physicPath}");
                            }
                        }

                        string url = $"{AppSettingsConfig.webUrl}{path}";
                        var res = new
                        {
                            index = index,
                            name = file.FileName,
                            status = mergeOk == true ? "done" :"part",
                            thumbUrl = url,
                            url = url
                        };

                        return AjaxResultActionFilter.Success(res);
                    }
                    else
                    {
                        return AjaxResultActionFilter.Error("上传失败");
                    }
                }
            }
            catch (Exception ex)
            {
                Directory.Delete(tmp);//删除文件夹
                _logger.LogError($"文件上传异常:{ex.Message}");
                return AjaxResultActionFilter.Error("上传失败");
            }

        }
        /// <summary>
        /// 合并文件
        /// </summary>
        /// <param name="tmpDirectory">临时上传目录</param>        
        /// <param name="path">上传目录</param>
        /// <param name="saveFileName">保存之后新文件名</param>
        /// <returns></returns>
        private async Task<bool> FileMerge(string tmpDirectory, string saveName)
        {
            try
            {
                var files = Directory.GetFiles(tmpDirectory);

                using (var fs = new FileStream(saveName, FileMode.Create))
                {
                    foreach (var part in files.OrderBy(x => x.Length).ThenBy(x => x))
                    {
                        var bytes = System.IO.File.ReadAllBytes(part);
                        await fs.WriteAsync(bytes, 0, bytes.Length);
                        bytes = null;
                        System.IO.File.Delete(part);//删除分块
                    }
                    fs.Close();

                    Directory.Delete(tmpDirectory);//删除临时目录
                    return true;
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"文件合并异常:{ex.Message}");
                return false;
            }

        }

        #endregion       

        /// <summary>
        /// 文件保存到本地
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="path"></param>
        /// <param name="saveName"></param>
        /// <returns></returns>
        private async Task<bool> Save(Stream stream, string path, string saveName)
        {
            try
            {
                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }

                await Task.Run(() =>
                {
                    FileStream fs = new FileStream(path + saveName, FileMode.Create);
                    stream.Position = 0;
                    stream.CopyTo(fs);
                    fs.Close();
                });
                return true;

            }
            catch (Exception ex)
            {
                _logger.LogError($"文件保存异常:{ex.Message}");
                return false;
            }
        }


        /// <summary>
        /// 计算文件的MD5值
        /// </summary>
        /// <param name="obj">类型只能为string or stream,否则将会抛出错误</param>
        /// <returns>文件的MD5值</returns>
        private string GetMD5Value(object obj)
        {
            MD5 md5Hash = MD5.Create();
            byte[] data = null;
            switch (obj)
            {
                case string str:
                    data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(str));
                    break;
                case Stream stream:
                    data = md5Hash.ComputeHash(stream);
                    break;
                case null:
                    throw new ArgumentException("参数不能为空");
                default:
                    throw new ArgumentException("参数类型错误");
            }

            return BitConverter.ToString(data).Replace("-""");
        }

        /// <summary>
        /// 获取路径
        /// </summary>
        /// <param name="virtualPath"></param>
        /// <returns></returns>
        protected string GetAbsolutePath(string virtualPath)
        {
            string path = virtualPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
            if (path[0] == '~')
                path = path.Remove(0, 2);
            string rootPath = HttpContext.RequestServices.GetService<IWebHostEnvironment>().WebRootPath;

            return Path.Combine(rootPath, path);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        protected string GetUploadPath()
        {
            string rootPath = HttpContext.RequestServices.GetService<IWebHostEnvironment>().WebRootPath;

            return Path.Combine(rootPath, "Upload");
        }

示例实现的比较简单,您还可以进行优化,比如所有块拆分后同时长传,同时上传完毕后,客户在发起合并请求,另外如果丢了一块,其实也是可以进行检查,补上传的。

大文件下载

使用开源框架Downloader,大家自己去GitHub看吧,链接https://github.com/bezzad/Downloader,下图为官网例子图

支持分块同时下载,非常不错哟。

最后老规矩,上源码地址

前台 https://gitee.com/akwkevin/aistudio.-wpf.-aclient

后台 https://gitee.com/akwkevin/AIStudio.Blazor.App