翼度科技»论坛 编程开发 .net 查看内容

基于.NET Core + Jquery实现文件断点分片上传

10

主题

10

帖子

30

积分

新手上路

Rank: 1

积分
30
基于.NET Core + Jquery实现文件断点分片上传

前言

该项目是基于.NET Core 和 Jquery实现的文件分片上传,没有经过测试,因为博主没有那么大的文件去测试,目前上传2G左右的文件是没有问题的。
使用到的技术


  • Redis缓存技术
  • Jquery ajax请求技术
为什么要用到Redis,文章后面再说,先留个悬念。
页面截图


NuGet包


  • Microsoft.Extensions.Caching.StackExchangeRedis
  • Zack.ASPNETCore 杨中科封装的操作Redis包
分片上传是如何进行的?

在实现代码的时候,我们需要了解文件为什么要分片上传,我直接上传不行吗。大家在使用b站、快手等网站的视频上传的时候,可以发现文件中断的话,之前已经上传了的文件再次上传会很快。这就是分片上传的好处,如果发发生中断,我只要上传中断之后没有上传完成的文件即可,当一个大文件上传的时候,用户可能会断网,或者因为总总原因导致上传失败,但是几个G的文件,难不成又重新上传吗,那当然不行。
具体来说,分片上传文件的原理如下:

  • 客户端将大文件切割成若干个小文件块,并为每个文件块生成一个唯一的标识符,以便后续的合并操作。
  • 客户端将每个小文件块上传到服务器,并将其标识符和其他必要的信息发送给服务器。
  • 服务器接收到每个小文件块后,将其保存在临时文件夹中,并返回一个标识符给客户端,以便客户端后续的合并操作。
  • 客户端将所有小文件块的标识符发送给服务器,并请求服务器将这些小文件块合并成一个完整的文件。
  • 服务器接收到客户端的请求后,将所有小文件块按照其标识符顺序进行合并,并将合并后的文件保存在指定的位置。
  • 客户端接收到服务器的响应后,确认文件上传成功。
总的来说,分片上传文件的原理就是将一个大文件分成若干个小文件块,分别上传到服务器,最后再将这些小文件块合并成一个完整的文件。
在了解原理之后开始实现代码。
后端实现

注册reidis服务

首先在Program.cs配置文件中注册reidis服务
  1. builder.Services.AddScoped<IDistributedCacheHelper, DistributedCacheHelper>();
  2. //注册redis服务
  3. builder.Services.AddStackExchangeRedisCache(options =>
  4. {
  5.     string connStr = builder.Configuration.GetSection("Redis").Value;
  6.     string password = builder.Configuration.GetSection("RedisPassword").Value;
  7.     //redis服务器地址
  8.     options.Configuration = $"{connStr},password={password}";
  9. });
复制代码
在appsettings.json中配置redis相关信息
  1.   "Redis": "redis地址",
  2.   "RedisPassword": "密码"
复制代码
保存文件的实现

在控制器中注入
  1. private readonly IWebHostEnvironment _environment;
  2. private readonly IDistributedCacheHelper _distributedCache;
  3. public UpLoadController(IDistributedCacheHelper distributedCache, IWebHostEnvironment environment)
  4.         {
  5.             _distributedCache = distributedCache;
  6.             _environment = environment;
  7.         }
复制代码
从redis中取文件名
  1. string GetTmpChunkDir(string fileName)
  2. {
  3.             var s = _distributedCache.GetOrCreate<string>(fileName, ( e) =>
  4.             {
  5.                 //滑动过期时间
  6.                 //e.SlidingExpiration = TimeSpan.FromSeconds(1800);
  7.                 //return Encoding.Default.GetBytes(Guid.NewGuid().ToString("N"));
  8.                 return fileName.Split('.')[0];
  9.             }, 1800);
  10.             if (s != null) return fileName.Split('.')[0]; ;
  11.             return "";
  12. }
复制代码
实现保存文件方法
  1.                 /// <summary>
  2.         /// 保存文件
  3.         /// </summary>
  4.         /// <param name="file">文件</param>
  5.         /// <param name="fileName">文件名</param>
  6.         /// <param name="chunkIndex">文件块</param>
  7.         /// <param name="chunkCount">分块数</param>
  8.         /// <returns></returns>
  9. public async Task<JsonResult> SaveFile(IFormFile file, string fileName, int chunkIndex, int chunkCount)
  10.         {
  11.             try
  12.             {
  13.                 //说明为空
  14.                 if (file.Length == 0)
  15.                 {
  16.                     return Json(new
  17.                     {
  18.                         success = false,
  19.                         mas = "文件为空!!!"
  20.                     });
  21.                 }
  22.                 if (chunkIndex == 0)
  23.                 {
  24.                     ////第一次上传时,生成一个随机id,做为保存块的临时文件夹
  25.                     //将文件名保存到redis中,时间是s
  26.                     _distributedCache.GetOrCreate(fileName, (e) =>
  27.                     {
  28.                         //滑动过期时间
  29.                         //e.SlidingExpiration = TimeSpan.FromSeconds(1800);
  30.                         //return Encoding.Default.GetBytes(Guid.NewGuid().ToString("N"));
  31.                         return fileName.Split('.')[0]; ;
  32.                     }, 1800);
  33.                 }
  34.                 if(!Directory.Exists(GetFilePath())) Directory.CreateDirectory(GetFilePath());
  35.                 var fullChunkDir = GetFilePath() + dirSeparator + GetTmpChunkDir(fileName);
  36.                 if(!Directory.Exists(fullChunkDir)) Directory.CreateDirectory(fullChunkDir);
  37.                 var blog = file.FileName;
  38.                 var newFileName = blog + chunkIndex + Path.GetExtension(fileName);
  39.                 var filePath = fullChunkDir + Path.DirectorySeparatorChar + newFileName;
  40.                                
  41.                 //如果文件块不存在则保存,否则可以直接跳过
  42.                 if (!System.IO.File.Exists(filePath))
  43.                 {
  44.                     //保存文件块
  45.                     using (var stream = new FileStream(filePath, FileMode.Create))
  46.                     {
  47.                         await file.CopyToAsync(stream);
  48.                     }
  49.                 }
  50.                 //所有块上传完成
  51.                 if (chunkIndex == chunkCount - 1)
  52.                 {
  53.                     //也可以在这合并,在这合并就不用ajax调用CombineChunkFile合并
  54.                     //CombineChunkFile(fileName);
  55.                 }
  56.                 var obj = new
  57.                 {
  58.                     success = true,
  59.                     date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
  60.                     newFileName,
  61.                     originalFileName = fileName,
  62.                     size = file.Length,
  63.                     nextIndex = chunkIndex + 1,
  64.                 };
  65.                 return Json(obj);
  66.             }
  67.             catch (Exception ex)
  68.             {
  69.                 return Json(new
  70.                 {
  71.                     success = false,
  72.                     msg = ex.Message,
  73.                 });
  74.             }
  75.         }
复制代码
讲解关键代码 Redis部分

当然也可以放到session里面,这里就不做演示了。
这是将文件名存入到redis中,作为唯一的key值,当然这里最好采用
Encoding.Default.GetBytes(Guid.NewGuid().ToString("N"));去随机生成一个id保存,为什么我这里直接用文件名,一开始写这个是为了在学校上机课时和室友之间互相传文件,所以没有考虑那么多,根据自己的需求来。
在第一次上传文件的时候,redis会保存该文件名,如果reids中存在该文件名,那么后面分的文件块就可以直接放到该文件名下。
  1. _distributedCache.GetOrCreate(fileName, (e) =>
  2. {
  3.      //滑动过期时间
  4.      //e.SlidingExpiration = TimeSpan.FromSeconds(1800);
  5.      //return Encoding.Default.GetBytes(Guid.NewGuid().ToString("N"));
  6.      return fileName.Split('.')[0]; ;
  7. }, 1800);
复制代码
合并文件方法
  1. //目录分隔符,兼容不同系统
  2. static readonly char dirSeparator = Path.DirectorySeparatorChar;
复制代码
  1. //获取文件的存储路径
  2. //用于保存的文件夹
  3. private string GetFilePath()
  4. {
  5.     return Path.Combine(_environment.WebRootPath, "UploadFolder");
  6. }
复制代码
  1. public async Task<JsonResult> CombineChunkFile(string fileName)
  2. {
  3.             try
  4.             {
  5.                 return await Task.Run(() =>
  6.                 {
  7.                     //获取文件唯一id值,这里是文件名
  8.                     var tmpDir = GetTmpChunkDir(fileName);
  9.                     //找到文件块存放的目录
  10.                     var fullChunkDir = GetFilePath() + dirSeparator + tmpDir;
  11.                                         //开始时间
  12.                     var beginTime = DateTime.Now;
  13.                     //新的文件名
  14.                     var newFileName = tmpDir + Path.GetExtension(fileName);
  15.                     var destFile = GetFilePath() + dirSeparator + newFileName;
  16.                     //获取临时文件夹内的所有文件块,排好序
  17.                     var files = Directory.GetFiles(fullChunkDir).OrderBy(x => x.Length).ThenBy(x => x).ToList();
  18.                     //将文件块合成一个文件
  19.                     using (var destStream = System.IO.File.OpenWrite(destFile))
  20.                     {
  21.                         files.ForEach(chunk =>
  22.                         {
  23.                             using (var chunkStream = System.IO.File.OpenRead(chunk))
  24.                             {
  25.                                 chunkStream.CopyTo(destStream);
  26.                             }
  27.                             System.IO.File.Delete(chunk);
  28.                         });
  29.                         Directory.Delete(fullChunkDir);
  30.                     }
  31.                                         //结束时间
  32.                     var totalTime = DateTime.Now.Subtract(beginTime).TotalSeconds;
  33.                     return Json(new
  34.                     {
  35.                         success = true,
  36.                         destFile = destFile.Replace('\\', '/'),
  37.                         msg = $"合并完成 ! {totalTime} s",
  38.                     });
  39.                 });
  40.             }catch (Exception ex)
  41.             {
  42.                 return Json(new
  43.                 {
  44.                     success = false,
  45.                     msg = ex.Message,
  46.                 });
  47.             }
  48.             finally
  49.             {
  50.                 _distributedCache.Remove(fileName);
  51.             }
  52. }
复制代码
前端实现

原理

原理就是获取文件,然后切片,通过分片然后递归去请求后端保存文件的接口。


首先引入Jquery


然后随便写一个上传页面
  1.     将文件拖拽到这里上传
  2.     或者
  3.     <input type="file" id="file1">
  4.     <button for="file-input" id="btnfile" value="Upload" >选择文件</button>
  5.    
  6.         
  7.    
  8.    
  9.     0%
  10. <button id="btnQuxiao"  disabled>暂停上传</button>
复制代码
css实现

稍微让页面能够看得下去
  1. [/code][size=5]Jqueuy代码实现[/size]
  2. [code]
复制代码
合并文件请求

var isLastChunk = chunkIndex === chunkCount - 1;
当isLastChunk 为true时,执行合并文件,这里就不会再去请求保存文件了。
总结

分片上传文件原理很简单,根据原理去实现代码,慢慢的摸索很快就会熟练掌握,当然本文章有很多写的不好的地方可以指出来,毕竟博主还只是学生,需要不断的学习。
有问题评论,看到了会回复。
参考资料




来源:https://www.cnblogs.com/ZYPLJ/archive/2023/03/27/17263430.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具