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

前言

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

使用到的技术

  • Redis缓存技术
  • Jquery ajax请求技术

为什么要用到Redis,文章后面再说,先留个悬念。

页面截图

NuGet包

  • Microsoft.Extensions.Caching.StackExchangeRedis

  • Zack.ASPNETCore 杨中科封装的操作Redis包

分片上传是如何进行的?

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

具体来说,分片上传文件的原理如下:

  1. 客户端将大文件切割成若干个小文件块,并为每个文件块生成一个唯一的标识符,以便后续的合并操作。
  2. 客户端将每个小文件块上传到服务器,并将其标识符和其他必要的信息发送给服务器。
  3. 服务器接收到每个小文件块后,将其保存在临时文件夹中,并返回一个标识符给客户端,以便客户端后续的合并操作。
  4. 客户端将所有小文件块的标识符发送给服务器,并请求服务器将这些小文件块合并成一个完整的文件。
  5. 服务器接收到客户端的请求后,将所有小文件块按照其标识符顺序进行合并,并将合并后的文件保存在指定的位置。
  6. 客户端接收到服务器的响应后,确认文件上传成功。

总的来说,分片上传文件的原理就是将一个大文件分成若干个小文件块,分别上传到服务器,最后再将这些小文件块合并成一个完整的文件。

在了解原理之后开始实现代码。

后端实现

注册reidis服务

首先在Program.cs配置文件中注册reidis服务

builder.Services.AddScoped<IDistributedCacheHelper, DistributedCacheHelper>();
//注册redis服务
builder.Services.AddStackExchangeRedisCache(options =>
{
string connStr = builder.Configuration.GetSection("Redis").Value;
string password = builder.Configuration.GetSection("RedisPassword").Value;
//redis服务器地址
options.Configuration = $"{connStr},password={password}";
});

在appsettings.json中配置redis相关信息

  "Redis": "redis地址",
"RedisPassword": "密码"

保存文件的实现

在控制器中注入

private readonly IWebHostEnvironment _environment;
private readonly IDistributedCacheHelper _distributedCache;
public UpLoadController(IDistributedCacheHelper distributedCache, IWebHostEnvironment environment)
{
_distributedCache = distributedCache;
_environment = environment;
}

从redis中取文件名

 string GetTmpChunkDir(string fileName)
{
var s = _distributedCache.GetOrCreate<string>(fileName, ( e) =>
{
//滑动过期时间
//e.SlidingExpiration = TimeSpan.FromSeconds(1800);
//return Encoding.Default.GetBytes(Guid.NewGuid().ToString("N"));
return fileName.Split('.')[0];
}, 1800);
if (s != null) return fileName.Split('.')[0]; ;
return "";
}

实现保存文件方法

 		/// <summary>
/// 保存文件
/// </summary>
/// <param name="file">文件</param>
/// <param name="fileName">文件名</param>
/// <param name="chunkIndex">文件块</param>
/// <param name="chunkCount">分块数</param>
/// <returns></returns>
public async Task<JsonResult> SaveFile(IFormFile file, string fileName, int chunkIndex, int chunkCount)
{
try
{
//说明为空
if (file.Length == 0)
{
return Json(new
{
success = false,
mas = "文件为空!!!"
});
} if (chunkIndex == 0)
{
////第一次上传时,生成一个随机id,做为保存块的临时文件夹
//将文件名保存到redis中,时间是s
_distributedCache.GetOrCreate(fileName, (e) =>
{
//滑动过期时间
//e.SlidingExpiration = TimeSpan.FromSeconds(1800);
//return Encoding.Default.GetBytes(Guid.NewGuid().ToString("N"));
return fileName.Split('.')[0]; ;
}, 1800);
} if(!Directory.Exists(GetFilePath())) Directory.CreateDirectory(GetFilePath());
var fullChunkDir = GetFilePath() + dirSeparator + GetTmpChunkDir(fileName);
if(!Directory.Exists(fullChunkDir)) Directory.CreateDirectory(fullChunkDir); var blog = file.FileName;
var newFileName = blog + chunkIndex + Path.GetExtension(fileName);
var filePath = fullChunkDir + Path.DirectorySeparatorChar + newFileName; //如果文件块不存在则保存,否则可以直接跳过
if (!System.IO.File.Exists(filePath))
{
//保存文件块
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
} //所有块上传完成
if (chunkIndex == chunkCount - 1)
{
//也可以在这合并,在这合并就不用ajax调用CombineChunkFile合并
//CombineChunkFile(fileName);
} var obj = new
{
success = true,
date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
newFileName,
originalFileName = fileName,
size = file.Length,
nextIndex = chunkIndex + 1,
}; return Json(obj);
}
catch (Exception ex)
{
return Json(new
{
success = false,
msg = ex.Message,
});
}
}

讲解关键代码 Redis部分

当然也可以放到session里面,这里就不做演示了。

这是将文件名存入到redis中,作为唯一的key值,当然这里最好采用

Encoding.Default.GetBytes(Guid.NewGuid().ToString("N"));去随机生成一个id保存,为什么我这里直接用文件名,一开始写这个是为了在学校上机课时和室友之间互相传文件,所以没有考虑那么多,根据自己的需求来。

在第一次上传文件的时候,redis会保存该文件名,如果reids中存在该文件名,那么后面分的文件块就可以直接放到该文件名下。

 _distributedCache.GetOrCreate(fileName, (e) =>
{
//滑动过期时间
//e.SlidingExpiration = TimeSpan.FromSeconds(1800);
//return Encoding.Default.GetBytes(Guid.NewGuid().ToString("N"));
return fileName.Split('.')[0]; ;
}, 1800);

合并文件方法

//目录分隔符,兼容不同系统
static readonly char dirSeparator = Path.DirectorySeparatorChar;
//获取文件的存储路径
//用于保存的文件夹
private string GetFilePath()
{
return Path.Combine(_environment.WebRootPath, "UploadFolder");
}
 public async Task<JsonResult> CombineChunkFile(string fileName)
{
try
{
return await Task.Run(() =>
{
//获取文件唯一id值,这里是文件名
var tmpDir = GetTmpChunkDir(fileName);
//找到文件块存放的目录
var fullChunkDir = GetFilePath() + dirSeparator + tmpDir;
//开始时间
var beginTime = DateTime.Now;
//新的文件名
var newFileName = tmpDir + Path.GetExtension(fileName);
var destFile = GetFilePath() + dirSeparator + newFileName;
//获取临时文件夹内的所有文件块,排好序
var files = Directory.GetFiles(fullChunkDir).OrderBy(x => x.Length).ThenBy(x => x).ToList();
//将文件块合成一个文件
using (var destStream = System.IO.File.OpenWrite(destFile))
{
files.ForEach(chunk =>
{
using (var chunkStream = System.IO.File.OpenRead(chunk))
{
chunkStream.CopyTo(destStream);
} System.IO.File.Delete(chunk); });
Directory.Delete(fullChunkDir);
}
//结束时间
var totalTime = DateTime.Now.Subtract(beginTime).TotalSeconds;
return Json(new
{
success = true,
destFile = destFile.Replace('\\', '/'),
msg = $"合并完成 ! {totalTime} s",
});
});
}catch (Exception ex)
{
return Json(new
{
success = false,
msg = ex.Message,
});
}
finally
{
_distributedCache.Remove(fileName);
}
}

前端实现

原理

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

首先引入Jquery

<script src="~/lib/jquery/dist/jquery.min.js"></script>

然后随便写一个上传页面

<div class="dropzone" id="dropzone">
将文件拖拽到这里上传<br>
或者<br>
<input type="file" id="file1">
<button for="file-input" id="btnfile" value="Upload" class="button">选择文件</button>
<div id="progress">
<div id="progress-bar"></div>
</div>
<div id="fName" style="font-size:16px"></div>
<div id="percent">0%</div>
</div>
<button id="btnQuxiao" class="button2" disabled>暂停上传</button>
<div id="completedChunks"></div>

css实现

稍微让页面能够看得下去

<style>
.dropzone {
border: 2px dashed #ccc;
padding: 25px;
text-align: center;
font-size: 20px;
margin-bottom: 20px;
position: relative;
} .dropzone:hover {
border-color: #aaa;
} #file1 {
display: none;
} #progress {
position: absolute;
bottom: -10px;
left: 0;
width: 100%;
height: 10px;
background-color: #f5f5f5;
border-radius: 5px;
overflow: hidden;
} #progress-bar {
height: 100%;
background-color: #4CAF50;
width: 0%;
transition: width 0.3s ease-in-out;
} #percent {
position: absolute;
bottom: 15px;
right: 10px;
font-size: 16px;
color: #999;
}
.button{
background-color: greenyellow;
}
.button, .button2 {
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
} .button2 {
background-color: grey;
}
</style>

Jqueuy代码实现

<script>
$(function(){
var pause = false;//是否暂停
var $btnQuxiao = $("#btnQuxiao"); //暂停上传
var $file; //文件
var $completedChunks = $('#completedChunks');//上传完成块数
var $progress = $('#progress');//上传进度条
var $percent = $('#percent');//上传百分比
var MiB = 1024 * 1024;
var chunkSize = 8.56 * MiB;//xx MiB
var chunkIndex = 0;//上传到的块
var totalSize;//文件总大小
var totalSizeH;//文件总大小M
var chunkCount;//分块数
var fileName;//文件名
var dropzone = $('#dropzone'); //拖拽
var $fileInput = $('#file1'); //file元素
var $btnfile = $('#btnfile'); //选择文件按钮
//通过自己的button按钮去打开选择文件的功能
$btnfile.click(function(){
$fileInput.click();
})
dropzone.on('dragover', function () {
$(this).addClass('hover');
return false;
});
dropzone.on('dragleave', function () {
$(this).removeClass('hover');
return false;
});
dropzone.on('drop', function (e) {
setBtntrue();
e.preventDefault();
$(this).removeClass('hover');
var val = $('#btnfile').val()
if (val == 'Upload') {
$file = e.originalEvent.dataTransfer.files[0];
if ($file === undefined) {
$completedChunks.html('请选择文件 !');
return false;
} totalSize = $file.size;
chunkCount = Math.ceil(totalSize / chunkSize * 1.0);
totalSizeH = (totalSize / MiB).toFixed(2);
fileName = $file.name;
$("#fName").html(fileName); $('#btnfile').val("Pause")
pause = false;
chunkIndex = 0;
}
postChunk();
});
$fileInput.change(function () {
setBtntrue();
console.log("开始上传文件!")
var val = $('#btnfile').val()
if (val == 'Upload') {
$file = $fileInput[0].files[0];
if ($file === undefined) {
$completedChunks.html('请选择文件 !');
return false;
} totalSize = $file.size;
chunkCount = Math.ceil(totalSize / chunkSize * 1.0);
totalSizeH = (totalSize / MiB).toFixed(2);
fileName = $file.name;
$("#fName").html(fileName); $('#btnfile').val("Pause")
pause = false;
chunkIndex = 0;
}
postChunk();
})
function postChunk() {
console.log(pause)
if (pause)
return false; var isLastChunk = chunkIndex === chunkCount - 1;
var fromSize = chunkIndex * chunkSize;
var chunk = !isLastChunk ? $file.slice(fromSize, fromSize + chunkSize) : $file.slice(fromSize, totalSize); var fd = new FormData();
fd.append('file', chunk);
fd.append('chunkIndex', chunkIndex);
fd.append('chunkCount', chunkCount);
fd.append('fileName', fileName); $.ajax({
url: '/UpLoad/SaveFile',
type: 'POST',
data: fd,
cache: false,
contentType: false,
processData: false,
success: function (d) {
if (!d.success) {
$completedChunks.html(d.msg);
return false;
} chunkIndex = d.nextIndex; //递归出口
if (isLastChunk) {
$completedChunks.html('合并 .. ');
$btnfile.val('Upload');
setBtntrue(); //合并文件
$.post('/UpLoad/CombineChunkFile', { fileName: fileName }, function (d) {
$completedChunks.html(d.msg);
$completedChunks.append('destFile: ' + d.destFile);
$btnfile.val('Upload');
setBtnfalse()
$fileInput.val('');//清除文件
$("#fName").html("");
});
}
else {
postChunk();//递归上传文件块
//$completedChunks.html(chunkIndex + '/' + chunkCount );
$completedChunks.html((chunkIndex * chunkSize / MiB).toFixed(2) + 'M/' + totalSizeH + 'M');
} var completed = chunkIndex / chunkCount * 100;
$percent.html(completed.toFixed(2) + '%').css('margin-left', parseInt(completed / 100 * $progress.width()) + 'px');
$progress.css('background', 'linear-gradient(to right, #ff0084 ' + completed + '%, #e8c5d7 ' + completed + '%)');
},
error: function (ex) {
$completedChunks.html('ex:' + ex.responseText);
}
});
}
$btnQuxiao.click(function(){
var val = $('#btnfile').val();
if (val == 'Pause') {
$btnQuxiao.css('background-color', 'grey');
val = 'Resume';
pause = true;
} else if (val === 'Resume') {
$btnQuxiao.css('background-color', 'greenyellow');
val = 'Pause';
pause = false;
}
else {
$('#btnfile').val("-");
}
console.log(val + "" + pause)
$('#btnfile').val(val)
postChunk();
})
//设置按钮可用
function setBtntrue(){
$btnQuxiao.prop('disabled', false)
$btnQuxiao.css('background-color', 'greenyellow');
}
//设置按钮不可用
function setBtnfalse() {
$btnQuxiao.prop('disabled', true)
$btnQuxiao.css('background-color', 'grey');
}
})
</script>

合并文件请求

var isLastChunk = chunkIndex === chunkCount - 1;

当isLastChunk 为true时,执行合并文件,这里就不会再去请求保存文件了。

总结

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

有问题评论,看到了会回复。

参考资料

基于.NET Core + Jquery实现文件断点分片上传的更多相关文章

  1. ThinkPHP+JQuery实现文件的异步上传

    前端代码 <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF- ...

  2. 大文件传输 分片上传 上传id 分片号 授权给第三方上传

    https://www.zhihu.com/question/39593108 作者:ZeroOne链接:https://www.zhihu.com/question/39593108/answer/ ...

  3. 【Android实战】----基于Retrofit实现多图片/文件、图文上传

    本文代码详见:https://github.com/honghailiang/RetrofitUpLoadImage 一.再次膜拜下Retrofit Retrofit不管从性能还是使用方便性上都非常屌 ...

  4. 基于commons-net实现ftp创建文件夹、上传、下载功能

    原文:http://www.open-open.com/code/view/1420774470187 package com.demo.ftp; import java.io.FileInputSt ...

  5. js+php大文件分片上传

    1 背景 用户本地有一份txt或者csv文件,无论是从业务数据库导出.还是其他途径获取,当需要使用蚂蚁的大数据分析工具进行数据加工.挖掘和共创应用的时候,首先要将本地文件上传至ODPS,普通的小文件通 ...

  6. nodeJs + js 大文件分片上传

    简单的文件上传 一.准备文件上传的条件: 1.安装nodejs环境 2.安装vue环境 3.验证环境是否安装成功 二.实现上传步骤 1.前端部分使用 vue-cli 脚手架,搭建一个 demo 版本, ...

  7. java下载网络大文件之内存不够的解决办法(包含分片上传分片下载)

    一.背景 2020年11月份的时候,我做过一个项目,涉及到网络文件,比如第三方接口提供一个文件的下载地址,使用java去下载,当时我全部加在到JVM内存里面,话说,单单是80M的下载单线程没问题,但是 ...

  8. blob 对象 实现分片上传 及 显示进度条

    blob对象介绍 一个 Blob对象表示一个不可变的, 原始数据的类似文件对象.Blob表示的数据不一定是一个JavaScript原生格式 blob对象本质上是js中的一个对象,里面可以储存大量的二进 ...

  9. ASP.NET CORE使用WebUploader对大文件分片上传,并通过ASP.NET CORE SignalR实时反馈后台处理进度给前端展示

    本次,我们来实现一个单个大文件上传,并且把后台对上传文件的处理进度通过ASP.NET CORE SignalR反馈给前端展示,比如上传一个大的zip压缩包文件,后台进行解压缩,并且对压缩包中的文件进行 ...

  10. 基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件

    目录 1. 前言 2. 关于vue-simple-uploader 3. 基于vue-simple-uploader封装全局上传组件 4. 文件上传流程概览 5. 文件分片 6. MD5的计算过程 7 ...

随机推荐

  1. Linux 根文件系统的移植(从入门到精通)

    一.简介 提到操作系统的安装,还得从大学的时候说起,刚入学的时,朋友的系统本崩了,跑去电脑城换个系统花了40大洋,震惊了贫穷的我.好像发现了商机,果断开始了折腾自己的电脑,然后用朋友的电脑进行测试,由 ...

  2. 都2024年了,你还不知道git worktree么?

    三年前 python 大佬吉多·范罗苏姆(为 Python 程序设计语言的最初设计者及主要架构师)才知道 git worktree ,我现在才知道,我觉得没啥丢人的. 应用场景 如果你正在 featu ...

  3. Masscan入门手册

    相关文章 https://www.cnblogs.com/huim/p/12116004.html https://4hou.win/wordpress/?cat=3080 Nmap vs Massc ...

  4. 程序员天天 CURD,怎么才能成长,职业发展的思考(3)

    公司赚钱流程中,你在哪一个环节 思考你在哪一个环节 在一家提供互联网产品或服务的公司中,开发出产品或提供服务,卖产品卖服务给客户,赚取金钱获得利润,有了利润这家公司就能给员工发工资,公司才能存活下去, ...

  5. TCP协议分析工具TcpEngine V1.2.0使用教程

    概述 目前主流的网络数据分析工具主要有两类,一类是http协议分析工具,如fiddler,这类工具擅长对字符串类型协议分析:另一类是原始网络数据包的监听分析,如Wireshark,这类工具擅长分析网络 ...

  6. Python基础篇(安装)

    Python简介 Python是Guido van Rossum发布于1991年的一种计算机程序设计语言.是一种动态的.面向对象的脚本语言,是一种解释型的,弱类型的高级计算机语言.需要注意的是pyth ...

  7. 使用IIS部署WebDAV

    服务器开启WebDAV 在服务器安装IIS的同时 要启用Windows身份验证与WebDAV发布 如果不是服务器版本,参照下图 在IIS中新建WebDAV网站 配置好本地目录与端口 启用Windows ...

  8. pageoffice6 版本实现word 文件添加水印

    在很多场景下,Word文档正式发文之前,或者说形成最终文档之前,常常需要往Word文件中添加水印,并且会根据文件类型或内容的不同,需要添加的水印也不一样. 添加水印是Word软件里的一个简单功能,直接 ...

  9. SaltStack 常用的一些命令

    以下是 SaltStack 常用的一些命令: 查看帮助信息:salt --help检查Salt支持的操作系统:salt '*' test.ping查看Minion的版本号:salt '*' test. ...

  10. Android 13 - Media框架(19)- ACodec(一)

    关注公众号免费阅读全文,进入音视频开发技术分享群! 这一节我们将会一起了解 ACodec 的设计方式,在看具体的实现细节前我们要先了解它内部的状态转换机制,这也是ACodec的核心难点之一. 1.AH ...