.NET或.NET Core Web APi基於tus協議實現斷點續傳

Jeffcky發表於2020-08-11

前言

前兩天我採用技巧式方案基本實現大檔案分片上傳,這裡只是重點在於個人思路和親身實踐,若在實際生產環境要求比較高的話肯定不行,仍存在一些問題需要深入處理,本文繼續在之前基礎上給出基於tus協議的輪子方案,本打算再次嘗試利用.NET Core實現此協議,但在github上一搜尋早在2016年就已有此協議對應的.NET和.NET Core方案,並且一直更新到最近的.NET Core 3.x版本,完全滿足各位所需,本文是我寫出的一點demo。

基於tus協議實現斷點續傳演示

 

基於tus協議tusdotnet方案基本demo

關於此協議實現原理這裡不做闡述,請參照上述github地址自行了解,本文只是給出.NET Core方案下的基本demo,我們上傳一個大檔案然後通過進度顯示上傳進度以及對上傳可暫停可繼續,專業點講就是斷點續傳,首先肯定是引入tus指令碼和需要用到的bootstrap樣式,我們將進度條預設隱藏,當上傳時才顯示,所以我們給出如下HTML。

<div class="form-horizontal" style="margin-top:80px;">
    <div class="form-group" id="progress-group" style="display:none;">
        <div id="size"></div>
        <div class="progress">
            <div id="progress" class="progress-bar progress-bar-success progress-bar-animated progress-bar-striped" role="progressbar"
                 aria-valuemin="0" aria-valuemax="100">
                <span id="percentage"></span>
            </div>
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-10">
            <input name="file" id="file" type="file" />
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" id="submit" value="上傳" class="btn btn-success" />
            <input type="button" id="pause" value="暫停" class="btn btn-danger" />
            <input type="button" id="continue" value="繼續" class="btn btn-info" />
        </div>
    </div>
</div>

接下來就是使用引入的tus指令碼,也沒什麼太多要講解的,直接上程式碼,這裡稍微注意的是在如下後設資料(metadata)屬性物件定義給出實際檔名,便於在後臺最終將上傳的檔案轉換為目標檔案,至少得知道副檔名,對吧。

<script type="text/javascript">
    $(function () {
        var upload;

        //上傳
        $('#submit').click(function () {

            $('#progress-group').show();

            var file = $('#file')[0].files[0];

            // 建立tus上傳物件
            upload = new tus.Upload(file, {
                // 檔案伺服器上傳終結點地址設定
                endpoint: "files/",
                // 重試延遲設定
                retryDelays: [0, 3000, 5000, 10000, 20000],
                // 附件伺服器所需的後設資料
                metadata: {
                    name: file.name,
                    contentType: file.type || 'application/octet-stream',
                    emptyMetaKey: ''
                },
                // 回撥無法通過重試解決的錯誤
                onError: function (error) {
                    console.log("Failed because: " + error)
                },
                // 上傳進度回撥
                onProgress: onProgress,
                // 上傳完成後回撥
                onSuccess: function () {
                    console.log("Download %s from %s", upload.file.name, upload.url)
                }
            })

            upload.start()
        });

        //暫停
        $('#pause').click(function () {
            upload.abort()
        });

        //繼續
        $('#continue').click(function () {
            upload.start()
        });

        //上傳進度展示
        function onProgress(bytesUploaded, bytesTotal) {
            var percentage = (bytesUploaded / bytesTotal * 100).toFixed(2);
            $('#progress').attr('aria-valuenow', percentage);
            $('#progress').css('width', percentage + '%');

            $('#percentage').html(percentage + '%');

            var uploadBytes = byteToSize(bytesUploaded);
            var totalBytes = byteToSize(bytesTotal);

            $('#size').html(uploadBytes + '/' + totalBytes);
        }

        //將位元組轉換為Byte、KB、MB等
        function byteToSize(bytes, separator = '', postFix = '') {
            if (bytes) {
                const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
                const i = Math.min(parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString(), 10), sizes.length - 1);
                return `${(bytes / (1024 ** i)).toFixed(i ? 1 : 0)}${separator}${sizes[i]}${postFix}`;
            }
            return 'n/a';
        }
    });

</script>

接下來進入後臺,首先安裝對應tus協議實現包,如下:

接下來則是新增tus中介軟體,說白了就是對tus的配置,各種配置都可滿足你所需,這裡我只實現了檔案上傳完成後將上傳檔案轉換為目標檔案的處理,緊接著將如下實現tus配置以單例形式注入即可

private DefaultTusConfiguration CreateTusConfiguration(IServiceProvider serviceProvider)
{
    var env = (IWebHostEnvironment)serviceProvider.GetRequiredService(typeof(IWebHostEnvironment));

    //檔案上傳路徑
    var tusFiles = Path.Combine(env.WebRootPath, "tusfiles");

    return new DefaultTusConfiguration
    {
        UrlPath = "/files",
        //檔案儲存路徑
        Store = new TusDiskStore(tusFiles),
        //後設資料是否允許空值
        MetadataParsingStrategy = MetadataParsingStrategy.AllowEmptyValues,
        //檔案過期後不再更新
        Expiration = new AbsoluteExpiration(TimeSpan.FromMinutes(5)),
        //事件處理(各種事件,滿足你所需)
        Events = new Events
        {
            //上傳完成事件回撥
            OnFileCompleteAsync = async ctx =>
            {
                //獲取上傳檔案
                var file = await ctx.GetFileAsync();

                //獲取上傳檔案後設資料
                var metadatas = await file.GetMetadataAsync(ctx.CancellationToken);
                
                //獲取上述檔案後設資料中的目標檔名稱
                var fileNameMetadata = metadatas["name"];

                //目標檔名以base64編碼,所以這裡需要解碼
                var fileName = fileNameMetadata.GetString(Encoding.UTF8);

                var extensionName = Path.GetExtension(fileName);

                //將上傳檔案轉換為實際目標檔案
                File.Move(Path.Combine(tusFiles, ctx.FileId), Path.Combine(tusFiles, $"{ctx.FileId}{extensionName}"));
            }
        }
    };
}

然後獲取並使用上述新增的tus配置服務

app.UseTus(httpContext => Task.FromResult(httpContext.RequestServices.GetService<DefaultTusConfiguration>()));

在指令碼中我們看到有個endpoint屬性,此屬性表示上傳到伺服器的上傳結點地址,因為在上到伺服器時我們可能需對此請求進行額外處理,比如後設資料中的檔名是否已提供等等,所以我們在使用結點對映時,新增對上述結點名稱的對映,如下:

endpoints.MapGet("/files/{fileId}", DownloadFileEndpoint.HandleRoute);

該對映第二個引數為RequestDelegate,這個引數用過.NET Core的童鞋都知道,這裡我是直接拷貝該包的路由實現,如下:

public static class DownloadFileEndpoint
{
    public static async Task HandleRoute(HttpContext context)
    {
        var config = context.RequestServices.GetRequiredService<DefaultTusConfiguration>();

        if (!(config.Store is ITusReadableStore store))
        {
            return;
        }

        var fileId = (string)context.Request.RouteValues["fileId"];
        var file = await store.GetFileAsync(fileId, context.RequestAborted);

        if (file == null)
        {
            context.Response.StatusCode = 404;
            await context.Response.WriteAsync($"File with id {fileId} was not found.", context.RequestAborted);
            return;
        }

        var fileStream = await file.GetContentAsync(context.RequestAborted);
        var metadata = await file.GetMetadataAsync(context.RequestAborted);

        context.Response.ContentType = GetContentTypeOrDefault(metadata);
        context.Response.ContentLength = fileStream.Length;

        if (metadata.TryGetValue("name", out var nameMeta))
        {
            context.Response.Headers.Add("Content-Disposition",
                new[] { $"attachment; filename=\"{nameMeta.GetString(Encoding.UTF8)}\"" });
        }

        using (fileStream)
        {
            await fileStream.CopyToAsync(context.Response.Body, 81920, context.RequestAborted);
        }
    }

    private static string GetContentTypeOrDefault(Dictionary<string, Metadata> metadata)
    {
        if (metadata.TryGetValue("contentType", out var contentType))
        {
            return contentType.GetString(Encoding.UTF8);
        }

        return "application/octet-stream";
    }
}

檔案上傳大小限制說明

我們知道無論是.NET還是.NET Core對於檔案上傳大小都有預設限制大小,這裡對.NET Core中檔案大小各種環境配置做一個統一說明,如果你將.NET Core寄宿在IIS上執行,那麼請修改web.config配置檔案大小限制

<system.webServer>
  <security>
    <requestFiltering>
      //若不配置,預設是28.6兆
      <requestLimits maxAllowedContentLength="1073741824" />
    </requestFiltering>
  </security>
</system.webServer>

如果在開發環境預設使用IIS執行應用程式,請通過如下根據實際情況配置檔案上傳大小

services.Configure<IISServerOptions>(options =>
 {
      options.MaxRequestBodySize = int.MaxValue;
 });

如果程式執行在Kestrel伺服器,那麼請通過如下根據實際情況配置檔案上傳大小

services.Configure<KestrelServerOptions>(options =>
{
     //若不配置,預設是30兆(沒記錯的話)
     options.Limits.MaxRequestBodySize = int.MaxValue; 
});

如果是通過表單上傳檔案,那麼請通過如下根據實際情況配置檔案上傳大小

services.Configure<FormOptions>(x =>
{
     x.ValueLengthLimit = int.MaxValue;
    //如果不配置,預設是128兆(沒記錯的話)
     x.MultipartBodyLengthLimit = int.MaxValue; 
     x.MultipartHeadersLengthLimit = int.MaxValue;
}); 

總結 

為了更好體驗可以再加上當前網路寬頻情況或剩餘多少分鐘,更詳細內容請參考:https://github.com/tusdotnet/tusdotnet https://github.com/tus/tus-js-client,關於大檔案上傳處理到此結束,希望對那些苦苦尋找最終解決方案而無助的童鞋們提供最佳輪子,謝謝。接下來主要精力都會放在同步發表微信公眾號文章和錄製相關視訊放在b站上,歡迎各位童鞋關注。

相關文章