当前位置: 首页 > news >正文

ASP.NET Core文件分片上传

1.后端实现

服务定义

using System.Security.Cryptography;
using dotnet_start.Model.CusException;
using dotnet_start.Model.Request;
using dotnet_start.Model.Response;
using Path = System.IO.Path;namespace dotnet_start.Services;/// <summary>
/// 分片上传服务
/// </summary>
public class ChunkUploadService : BaseService<ChunkUploadService>
{private readonly string _chunkDir;private readonly string _finalDir;public ChunkUploadService(ILogger<ChunkUploadService> logger, IConfiguration configuration) : base(logger, configuration){_chunkDir = configuration["Upload:ChunkDir"] ?? "uploads/chunk/";_finalDir = configuration["Upload:FinalDir"] ?? "uploads/final/";}public ChunkUploadInitResponse InitUpload(ChunkUploadInitRequest request){var uuid = Guid.NewGuid().ToString("D");return new ChunkUploadInitResponse(uuid, request.FileMD5, request.FileSize);}public async Task UploadChunkAsync(ChunkUploadStartRequest request){var chunkDir = Path.Combine(Directory.GetCurrentDirectory(), _chunkDir, $"{request.FileMD5}_{request.UploadId}");if (!Directory.Exists(chunkDir)){Directory.CreateDirectory(chunkDir);}var chunkFilePath = Path.Combine(chunkDir, $"chunk_{request.Index}.tmp");if (File.Exists(chunkFilePath)){_logger.LogDebug("分片 {Index} 已存在,跳过写入", request.Index);return;}try{await using var stream = new FileStream(chunkFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 1024 * 1024);await request.File.CopyToAsync(stream);_logger.LogInformation("分片文件==={chunkFilePath}上传成功", chunkFilePath);}catch (Exception ex){_logger.LogWarning(ex, "分片上传失败");throw new BusinessException(500, "分片上传失败,稍后重试");}}public async Task MergeChunksAsync(ChunkUploadMergeRequest request){var chunkPath = Path.Combine(Directory.GetCurrentDirectory(), _chunkDir, $"{request.FileMD5}_{request.UploadId}");if (!Directory.Exists(chunkPath)){throw new BusinessException(500, "分片目录不存在");}var finalDir = Path.Combine(Directory.GetCurrentDirectory(), _finalDir);if (!Directory.Exists(finalDir)){Directory.CreateDirectory(finalDir);}var fileName = request.FileName;var finalFilePath = Path.Combine(finalDir, fileName);if (File.Exists(finalFilePath)){_logger.LogInformation("文件已存在==={finalFilePath}", finalFilePath);// 避免覆盖var timeSuffix = DateTime.Now.ToString("yyyyMMddHHmmssfff");var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName);var extension = Path.GetExtension(fileName);// 随机数防止并发冲突var rand = new Random().Next(1000, 9999);var uuid = Guid.NewGuid().ToString("D");finalFilePath = Path.Combine(finalDir, $"{fileNameWithoutExt}_{timeSuffix}_{uuid}_{rand}{extension}");}try{var chunkFiles = Directory.GetFiles(chunkPath).OrderBy(f => int.Parse(Path.GetFileName(f).Split('_')[1].Replace(".tmp", ""))).ToList();using var md5 = MD5.Create();await using var finalStream = new FileStream(finalFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 1024 * 1024);await using var cryptoStream = new CryptoStream(Stream.Null, md5, CryptoStreamMode.Write);var buffer = new byte[1024 * 1024];foreach (var chunkFile in chunkFiles){await using var chunkStream = new FileStream(chunkFile, FileMode.Open, FileAccess.Read, FileShare.Read, 1024 * 1024);int bytesRead;while ((bytesRead = await chunkStream.ReadAsync(buffer)) > 0){await finalStream.WriteAsync(buffer.AsMemory(0, bytesRead));await cryptoStream.WriteAsync(buffer.AsMemory(0, bytesRead));}}await cryptoStream.FlushFinalBlockAsync();var mergedMD5 = BitConverter.ToString(md5.Hash!).Replace("-", "").ToLowerInvariant();if (!string.Equals(mergedMD5, request.FileMD5, StringComparison.OrdinalIgnoreCase)){File.Delete(finalFilePath);throw new BusinessException(500, "分片合并文件MD5校验失败");}_logger.LogInformation("分片合并成成功:{ChunkPath}", chunkPath);// 清理分片目录_ = Task.Run(async () =>{var timeSpan = TimeSpan.FromMinutes(1);try{await Task.Delay(timeSpan);Directory.Delete(chunkPath, true);_logger.LogInformation("延迟==={@timeSpan}===删除分片目录成功:{ChunkPath}", timeSpan, chunkPath);}catch (Exception ex){_logger.LogWarning(ex, "延迟==={@timeSpan}===删除分片目录异常:{ChunkPath}", timeSpan, chunkPath);}});}catch (Exception ex){_logger.LogWarning(ex, "分片合并失败");throw new BusinessException(500, "分片合并失败,稍后重试");}}
}

参数定义

分片初始化参数

using System.ComponentModel.DataAnnotations;namespace dotnet_start.Model.Request;/// <summary>
/// 分片上传
/// </summary>
public class ChunkUploadInitRequest
{/// <summary>/// 文件MD5/// </summary>[Required(ErrorMessage = "文件 MD5 不能为空")][RegularExpression("^[a-fA-F0-9]{32}$", ErrorMessage = "文件MD5格式不正确")][StringLength(32, MinimumLength = 32, ErrorMessage = "文件 MD5 必须是32位")]public required string FileMD5 { get; set; }/// <summary>/// 文件大小/// </summary>[Required(ErrorMessage = "文件大小不能为空")][Range(1, long.MaxValue, ErrorMessage = "文件大小必须大于0")]public int FileSize { get; set; }
}

分片上传分片参数

using System.ComponentModel.DataAnnotations;namespace dotnet_start.Model.Request;/// <summary>
/// 分片上传开始参数
/// </summary>
public class ChunkUploadStartRequest
{/// <summary>/// 上传文件/// </summary>[Required(ErrorMessage = "上传文件不能为空")]public required IFormFile File { get; set; }/// <summary>/// 上传唯一ID/// </summary>[Required(ErrorMessage = "上传唯一ID不能为空")]public required string UploadId { get; set; }/// <summary>/// 文件MD5/// </summary>[Required(ErrorMessage = "文件MD5不能为空")][StringLength(32, MinimumLength = 32, ErrorMessage = "文件MD5必须是32位")][RegularExpression("^[a-fA-F0-9]{32}$", ErrorMessage = "文件MD5格式不正确")]public required string FileMD5 { get; set; }/// <summary>/// 分片索引/// </summary>[Required(ErrorMessage = "分片索引不能为空")][Range(0, 2000, ErrorMessage = "分片索引最多2000个")]public required int Index { get; set; }
}

分片上传合并参数

using System.ComponentModel.DataAnnotations;namespace dotnet_start.Model.Request;/// <summary>
/// 分片上传合并参数
/// </summary>
public class ChunkUploadMergeRequest
{/// <summary>/// 文件名称/// </summary>[Required(ErrorMessage = "文件名称不能为空")]public required string FileName { get; set; }/// <summary>/// 上传唯一ID/// </summary>[Required(ErrorMessage = "上传唯一ID不能为空")]public required string UploadId { get; set; }/// <summary>/// 文件MD5/// </summary>[Required(ErrorMessage = "文件 MD5 不能为空")][RegularExpression("^[a-fA-F0-9]{32}$", ErrorMessage = "文件MD5格式不正确")][StringLength(32, MinimumLength = 32, ErrorMessage = "文件 MD5 必须是32位")]public required string FileMD5 { get; set; }}

请求控制器

using dotnet_start.Model.Request;
using dotnet_start.Model.Response;
using dotnet_start.Services;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;namespace dotnet_start.Controllers;/// <summary>
/// 文件分片上传控制器
/// </summary>
/// <param name="service">文件分片上传服务</param>
[SwaggerTag("分片上传请求控制器")]
[ApiController]
[Route("chunk")]
public class ChunkUploadController(ChunkUploadService service) : ControllerBase
{/// <summary>/// 分片上传初始化/// </summary>/// <returns>CommonResult</returns>[HttpPost("upload/init")][ProducesResponseType(typeof(CommonResult<ChunkUploadInitResponse>), StatusCodes.Status200OK)]public IActionResult PostUploadChunkInit([FromForm] ChunkUploadInitRequest request){service._logger.LogDebug("分片上传的初始化参数==={@request}", request);return Ok(CommonResult<ChunkUploadInitResponse>.Success("上传初始化成功", service.InitUpload(request)));}/// <summary>/// 分片上传开始/// </summary>/// <returns>CommonResult</returns>[HttpPost("upload/start")][ProducesResponseType(typeof(CommonResult<string>), StatusCodes.Status200OK)]public async Task<IActionResult> PostUploadChunkStart([FromForm] ChunkUploadStartRequest request){service._logger.LogDebug("分片上传参数==={@request}", request);await service.UploadChunkAsync(request);return Ok(CommonResult<string>.Success(200, "分片上传成功"));}/// <summary>/// 分片上传合并/// </summary>/// <returns>CommonResult</returns>[HttpPost("upload/merge")][ProducesResponseType(typeof(CommonResult<string>), StatusCodes.Status200OK)]public async Task<IActionResult> PostUploadChunkMerge([FromBody] ChunkUploadMergeRequest request){service._logger.LogDebug("分片合并参数==={@request}", request);await service.MergeChunksAsync(request);return Ok(CommonResult<string>.Success(200, "分片合并并校验成功"));}}

2.前端调用

浏览器F12

后台控制台日志

查看上传文件目录

到此为止,asp.net core后端处理文件分片上传已完成。至于前端,使用原声带js+html,vue或者react等都可以,只要匹配后端参数即可。欢迎留言点赞与评论。


文章转载自:

http://tlIRU5iZ.txLxr.cn
http://J4UDE0wX.txLxr.cn
http://PmtN7i5I.txLxr.cn
http://IW4l1Za3.txLxr.cn
http://3XB4Ccal.txLxr.cn
http://76VhMmjz.txLxr.cn
http://xigKeipq.txLxr.cn
http://5K6Vt8uQ.txLxr.cn
http://Uc8w0pTZ.txLxr.cn
http://S5RZFfgy.txLxr.cn
http://RqVOqulP.txLxr.cn
http://DL3I7l1z.txLxr.cn
http://B2nSh40h.txLxr.cn
http://7bPokeup.txLxr.cn
http://8QDeM8XI.txLxr.cn
http://sAn0v4Ng.txLxr.cn
http://EskLY6v9.txLxr.cn
http://HyyvXGED.txLxr.cn
http://jf1TsNFv.txLxr.cn
http://V64xeA57.txLxr.cn
http://jTpK8Sy3.txLxr.cn
http://pD3iatgi.txLxr.cn
http://JCFQTqVC.txLxr.cn
http://CFMho6T0.txLxr.cn
http://1ziSTOt9.txLxr.cn
http://PaJ8CoCN.txLxr.cn
http://MkkpoV5y.txLxr.cn
http://DOzNd0bM.txLxr.cn
http://a4MDdYs4.txLxr.cn
http://DtI1aMBW.txLxr.cn
http://www.dtcms.com/a/368682.html

相关文章:

  • OCA、OCP、OCM傻傻分不清?Oracle认证就看这篇
  • 面试了一个外包公司,面试不到5分钟就出来,这问题问得有点变态。。。。。。
  • Matlab使用小技巧合集(系列四):Table类型高效用法与数据处理实战
  • 25高教社杯数模国赛【C题超高质量思路+可运行代码】第十弹
  • WinForms 项目里生成时选择“首选目标平台 32 位导致有些电脑在获取office word对象时获取不到
  • ANSYS 热力耦合计算
  • UE4 Mac构建编译报错 no member named “disjunction” in namespace “std”
  • 深度相机详解
  • vue 经常写的echarts图表模块结构抽取
  • 蚂蚁 S21e XP Hyd 3U 860T矿机性能分析与技术特点
  • Python迭代协议完全指南:从基础到高并发系统实现
  • CT影像寻找皮肤轮廓预处理
  • 7种流行Prompt设计模式详解:适用场景与最佳实践
  • uni-app 项目 iOS 上架踩坑经验总结 从证书到审核的避坑指南
  • 3.3_第一行之hard_local_irq_disable
  • 汽车 信息娱乐系统 概览
  • 将已有 Vue 项目通过 Electron 打包为桌面客户端的完整步骤
  • Nginx 配置片段主要用于实现​​正向代理​​,可以用来转发 HTTP 和 HTTPS 请求
  • 有鹿机器人的365天奇幻日记:我在景区当扫地僧
  • C++算法专题学习——分治
  • 智能工单路由系统(Java)
  • 生成模型实战 | 深度分层变分自编码器(Nouveau VAE,NVAE)
  • Windows多开文件夹太乱?Q-Dir四窗口同屏,拖拽文件快一倍
  • 测试驱动开发 (TDD) 与 Claude Code 的协作实践详解
  • Bug 排查日记:打造高效问题定位与解决的技术秘籍
  • MySQL InnoDB索引机制
  • Nextcloud 实战:打造属于你的私有云与在线协作平台
  • linux上nexus安装教程
  • vosk语音识别实战
  • 美团发布 | LongCat-Flash最全解读,硬刚GPT-4.1、Kimi!