基於.NET Core + Jquery實現文件斷點分片上傳

来源:https://www.cnblogs.com/ZYPLJ/archive/2023/03/27/17263430.html
-Advertisement-
Play Games

基於.NET Core + Jquery實現文件斷點分片上傳 前言 該項目是基於.NET Core 和 Jquery實現的文件分片上傳,沒有經過測試,因為博主沒有那麼大的文件去測試,目前上傳2G左右的文件是沒有問題的。 使用到的技術 Redis緩存技術 Jquery ajax請求技術 為什麼要用到R ...


基於.NET Core + Jquery實現文件斷點分片上傳

前言

該項目是基於.NET Core 和 Jquery實現的文件分片上傳,沒有經過測試,因為博主沒有那麼大的文件去測試,目前上傳2G左右的文件是沒有問題的。

使用到的技術

  • Redis緩存技術
  • Jquery ajax請求技術

為什麼要用到Redis,文章後面再說,先留個懸念。

頁面截圖

image

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);
            }
}

前端實現

原理

原理就是獲取文件,然後切片,通過分片然後遞歸去請求後端保存文件的介面。

image

image

首先引入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時,執行合併文件,這裡就不會再去請求保存文件了。

總結

分片上傳文件原理很簡單,根據原理去實現代碼,慢慢的摸索很快就會熟練掌握,當然本文章有很多寫的不好的地方可以指出來,畢竟博主還只是學生,需要不斷的學習。

有問題評論,看到了會回覆。

參考資料


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用 VLD 記憶體泄漏檢測工具輔助開發時整理的學習筆記。本篇介紹 VLD 配置文件中配置項 AggregateDuplicates 的使用方法。 ...
  • 本篇將對 Yarn 調度器中的資源搶占方式進行探究。分析當集群資源不足時,占用量資源少的隊列,是如何從其他隊列中搶奪資源的。我們將深入源碼,一步步分析搶奪資源的具體邏輯。 ...
  • 什麼是 Spdlog 日誌庫 Spdlog 是一個 C++ 的日誌庫,它具有高效、易用、跨平臺等特點。它可以寫入到控制台、文件等輸出目標,支持多種日誌級別、多線程安全等功能,非常適合在 C++ 項目中使用。 Spdlog 日誌庫的歷史和背景 Spdlog 日誌庫最初由 Gabi Melman 開發, ...
  • Sass IT寶庫整理的SASS快速參考備忘單,列出了 SASS 最有用的功能Sass 基礎,為開發人員分享快速參考備忘單。 Sass 是 Syntactically Awesome Stylesheets 的簡寫,是一個最初由 Hampton Catlin 設計並由 Natalie Weizenb ...
  • 內容援引自JavaGuide、嗶哩嗶哩黑馬程式員資料庫從入門到精通,感謝各位大神原創分享 資料庫Mysql 常見的關係型資料庫包括mysql、SQL Server、Oracle、常見的非關係型資料庫Redis、MongDB等。 特點 Mysql開源免費,生態完善,支持事務、高可用(讀寫分離、分庫分表 ...
  • Spdlog 是一個快速、非同步的 C++ 日誌庫,被廣泛應用於 C++ 項目中。在這篇文章中,我們將探討 Spdlog 日誌庫的實現原理。 Spdlog 的結構 Spdlog 由五個主要組件構成:Loggers、Sinks、Formatters、Async Logger 和 Registry。每個組 ...
  • TiDB 基礎使用 TiDB dashboard使用 TiDB Dashboard 是 TiDB 自 4.0 版本起提供的圖形化界面,可用於監控及診斷 TiDB 集群。TiDB Dashboard 內置於 TiDB 的 PD 組件中,無需獨立部署。 集群概況 查看集群整體 QPS 數值、執行耗時、消 ...
  • python中index()、find()方法,具體內容如下: index() 方法檢測字元串中是否包含子字元串 str ,該方法與 python find()方法一樣,只不過如果str不在 string中會報一個異常。影響後面程式執行 index()方法語法:str.index(str, beg= ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...