TUSDOTNET
Tusdotnet是tus協議的一個dotnet實現。tus協議是用來規范文件上傳的整個過程,tus基於http協議,規定了一些上傳過程中的頭部(headers)和對上傳過程的描述。它實現了文件的斷點恢復上傳以及其他的一些實用的規范。我前面文章中,有關於tus的詳細文檔。在對tusdotnet文檔的翻譯過程中,我刪除了關於IIS的章節,因為IIS的章節單獨放在一章中,所以刪除IIS對於其他章節沒有任何影響。因為我本人從來不會將.net core的項目部署到IIS上。
Tusdotnet的官方文檔在這里:https://github.com/tusdotnet/tusdotnet/wiki
本章按照如下目錄進行翻譯:
配置
- 總體配置
- 跨域請求處理
用法
- 創建文件之前檢查文件的元數據
- 文件上傳完成后的處理
- 下載文件
- 刪除過期的未完成文件
總體配置
tusdotnet使用下面的方式很容易配置:
app.UseTus(context => new DefaultTusConfiguration {... });
上述代碼中提供的工廠(context => new ...
)會作用於每一個請求上。通過檢查傳入的HttpContext/IOwinRequest,可以為不同的客戶機返回不同的配置。
工廠返回的是一個單利的DefaultTusConfiguration實例,這個實例包含如下屬性:
public class DefaultTusConfiguration { /// <summary> /// 用於監聽上傳的URL路徑 (比如 "/files"). ///如果站點位於子路徑中(例如https://example.org/mysite),也必須包含它(例如/mysite/files)。 /// </summary> public virtual string UrlPath { get; set; } /// <summary> /// The store to use when storing files. /// </summary> public virtual ITusStore Store { get; set; } /// <summary> /// Callbacks to run during different stages of the tusdotnet pipeline. /// </summary> public virtual Events Events { get; set; } /// <summary> /// The maximum upload size to allow. Exceeding this limit will return a "413 Request Entity Too Large" error to the client. /// Set to null to allow any size. The size might still be restricted by the web server or operating system. /// This property will be preceded by <see cref="MaxAllowedUploadSizeInBytesLong" />. /// </summary> public virtual int? MaxAllowedUploadSizeInBytes { get; set; } /// <summary> /// The maximum upload size to allow. Exceeding this limit will return a "413 Request Entity Too Large" error to the client. /// Set to null to allow any size. The size might still be restricted by the web server or operating system. /// This property will take precedence over <see cref="MaxAllowedUploadSizeInBytes" />. /// </summary> public virtual long? MaxAllowedUploadSizeInBytesLong { get; set; } /// <summary> /// Set an expiration time where incomplete files can no longer be updated. /// This value can either be <code>AbsoluteExpiration</code> or <code>SlidingExpiration</code>. /// Absolute expiration will be saved per file when the file is created. /// Sliding expiration will be saved per file when the file is created and updated on each time the file is updated. /// Setting this property to null will disable file expiration. /// </summary> public virtual ExpirationBase Expiration { get; set; } }
根據所使用的存儲類型,可能還需要對存儲進行一些配置。tusdotnet附帶的磁盤存儲需要一個目錄路徑,以及是否應該在連接(指concatenation擴展)時刪除“部分(指Upload-Concat:partial)”文件。
Store = new TusDiskStore(@"C:\tusfiles\", deletePartialFilesOnConcat: true)
在上面的例子中,C:\tusfiles\是保存所有文件的地方,deletePartialFilesOnConcat: true表示,一旦創建了最終文件(Upload-Concat:final),就應該刪除部分文件(僅由連接擴展(concatenation extension)使用)。默認值為false,因此不會意外刪除任何文件。如果不確定,或者沒有使用連接擴展,則將其設置為false。有關詳細信息,請參見Custom data store -> ITusConcatenationStore.
跨域請求處理
為了能夠讓瀏覽器通過不同域來上傳文件,你需要個體tusdotnet配置跨域請求的相關設置。
關於跨域的配置非常簡單:
public void ConfigureServices(IServiceCollection services) { services.AddCors(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseCors(builder => builder .AllowAnyHeader() .AllowAnyMethod() .AllowAnyOrigin() .WithExposedHeaders(tusdotnet.Helpers.CorsHelper.GetExposedHeaders()) ); app.UseTus(...); }
在創建文件之前檢查文件的元數據
OnBeforeCreate事件用來在創建文件之前檢查文件的元數據
OnBeforeCreate事件在創建文件之前觸發。
在傳遞給回調函數的BeforeCreateContext上調用FailRequest將使用400錯誤碼來拒絕請求。多次調用FailRequest將串聯/連接錯誤消息。
app.UseTus(context => new DefaultTusConfiguration { UrlPath = "/files", Store = new TusDiskStore(@"C:\tusfiles\"), Events = new Events { OnBeforeCreateAsync = ctx => { if (!ctx.Metadata.ContainsKey("name")) { ctx.FailRequest("name metadata must be specified. "); } if (!ctx.Metadata.ContainsKey("contentType")) { ctx.FailRequest("contentType metadata must be specified. "); } return Task.CompletedTask; } });
文件上傳完成后的處理
OnFileCompleteAsync事件用於文件上傳完成后的處理
該事件會在文件上傳完成后觸發。
app.UseTus(request => new DefaultTusConfiguration { Store = new TusDiskStore(@"C:\tusfiles\"), UrlPath = "/files", Events = new Events { OnFileCompleteAsync = async ctx => { // ctx.FileId is the id of the file that was uploaded. // ctx.Store is the data store that was used (in this case an instance of the TusDiskStore) // A normal use case here would be to read the file and do some processing on it. var file = await ((ITusReadableStore)ctx.Store).GetFileAsync(ctx.FileId, ctx.CancellationToken); var result = await DoSomeProcessing(file, ctx.CancellationToken); if (!result.Success) { throw new MyProcessingException("Something went wrong during processing"); } } } });
下載文件
由於tus規范不包含下載文件tusdotnet將自動將所有GET請求轉發給下一個中間件,因此開發人員可以選擇允許文件下載。
下面的示例要求數據存儲實現ITusReadableStore (TusDiskStore實現了)。如果沒有,就必須找出文件的存儲位置,並以其他方式讀取它們。
app.Use(async (context, next) => { // /files/ is where we store files if (context.Request.Uri.LocalPath.StartsWith("/files/", StringComparison.Ordinal)) { // Try to get a file id e.g. /files/<fileId> var fileId = context.Request.Uri.LocalPath.Replace("/files/", "").Trim(); if (!string.IsNullOrEmpty(fileId)) { var store = new TusDiskStore(@"C:\tusfiles\"); var file = await store.GetFileAsync(fileId, context.Request.CallCancelled); if (file == null) { context.Response.StatusCode = 404; await context.Response.WriteAsync($"File with id {fileId} was not found.", context.Request.CallCancelled); return; } var fileStream = await file.GetContentAsync(context.Request.CallCancelled); var metadata = await file.GetMetadataAsync(context.Request.CallCancelled); // The tus protocol does not specify any required metadata. // "contentType" is metadata that is specific to this domain and is not required. context.Response.ContentType = metadata.ContainsKey("contentType") ? metadata["contentType"].GetString(Encoding.UTF8) : "application/octet-stream"; if (metadata.ContainsKey("name")) { var name = metadata["name"].GetString(Encoding.UTF8); context.Response.Headers.Add("Content-Disposition", new[] { $"attachment; filename=\"{name}\"" }); } await fileStream.CopyToAsync(context.Response.Body, 81920, context.Request.CallCancelled); return; } }
刪除過期的未完成的文件
如果正在使用的存儲支持ITusExpirationStore (TusDiskStore支持),則可以指定未在指定時間段內更新的不完整文件應該標記為過期。如果在ITusConfiguration上設置了過期屬性(Expiration-Property),並且存儲支持ITusExpirationStore,則tusdotnet將自動執行此操作。但是文件不會自動刪除。為了幫助刪除過期的不完整文件,ITusExpirationStore接口公開了兩個方法,GetExpiredFilesAsync和DeleteExpiredFilesAsync。前者用於獲取已過期文件的id列表,后者用於刪除過期文件。
如上所述,tusdotnet不會自動刪除過期的文件,所以這需要開發人員來實現。建議在web應用程序添加合適的端點(EndPoint)來運行你自己的代碼。這些代碼是用諸如crontab之類的工具(比如HangFire)來輪詢(或者其他的方式)刪除過期未完成上傳的文件。
示例程序:
IEnumerable<string> expiredFileIds = await tusDiskStore.GetExpiredFilesAsync(cancellationToken); // Do something with expiredFileIds. int numberOfRemovedFiles = await tusDiskStore.RemoveExpiredFilesAsync(cancellationToken); // Do something with numberOfRemovedFiles.
本篇完。