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.
本篇完。

