基於.NET Core + Jquery實現文件斷點分片上傳 前言 該項目是基於.NET Core 和 Jquery實現的文件分片上傳,沒有經過測試,因為博主沒有那麼大的文件去測試,目前上傳2G左右的文件是沒有問題的。 使用到的技術 Redis緩存技術 Jquery ajax請求技術 為什麼要用到R ...
基於.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服務
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時,執行合併文件,這裡就不會再去請求保存文件了。
總結
分片上傳文件原理很簡單,根據原理去實現代碼,慢慢的摸索很快就會熟練掌握,當然本文章有很多寫的不好的地方可以指出來,畢竟博主還只是學生,需要不斷的學習。
有問題評論,看到了會回覆。