This branch is intended for the IETF "Resumable uploads for http" protocol (formerly known as "tus2"). For tus 1.x, see the main branch: https://github.com/tusdotnet/tusdotnet/
Please note that this is a POC/experimental implementation and breaking changes will happen.
Current Upload-Draft-Interop-Version: 7
Latest implemented spec commit: https://github.com/httpwg/http-extensions/commit/020f9f81345a101bc4b2ef050f876b6c38afde3b
Other implementations (both server and clients):
Usage from cURL:
curl-8.1.1_1-win64-mingw\bin\curl.exe -v --insecure -H "Content-Type: application/octet-stream" -H "Upload-Complete: ?1" --data-binary "@c:\testfile10mb.bin" https://localhost:5007/files-tus-2
See Source\tusdotnet.test\run-104-upload-resumption-supported-tests.ps1
for some more tests.
- Upload-Draft-Interop-Version: 6 - commit: cc7bea5075d61e8c1b05bfd26932b302074f61f8
- Upload-Draft-Interop-Version: 5 - commit: 303ef832440c2fe7bba652e11625e8d87f8e6764
- Upload-Draft-Interop-Version: 3 - commit: 568f9d2e0b794cb9b779944cddadee44d8a0b044
- Support for digest headers is not implemented
- Upload-Limit's
min-size
andmin-append-size
are only sent as a hint to the client but is never validated on requests, e.g. files smaller thanmin-size
can be uploaded
IIS
and http.sys
does not support sending 1xx responses and thus there is no support for feature detection for these web servers/stacks.
Running directly on Kestrel or behind a nginx reverse proxy works as expected.
For IIS
/http.sys
the client is required to perform a POST request with Upload-Complete: ?0
and an empty body to get the upload url.
See Github issues:
Clone this branch and include it in your project. All classes related to tus2 are found in the tusdotnet.tus2
namespace. Files are found in Source/tusdotnet/tus2
.
Navigate to Source\TestSites\AspNetCore_netcoreapp3.1_TestApp
(runs on .NET8):
dotnet run
Open a browser and go to https://localhost:5007
which will load the test page with the IETF branch of tus-js-client
Files will be saved in the location found in Source\TestSites\AspNetCore_netcoreapp3.1_TestApp\appsettings.json
-> FolderDiskPath
In Startup.cs add the following:
public void ConfigureServices(IServiceCollection services)
{
services.AddTus2(options =>
{
// Shorthand for adding a scoped implementation of Tus2DiskStorage to the DI container
options.AddDiskStorage(@"C:\path\to\save\files");
// Adds MyTusHandler as transient
options.AddHandler<MyTusHandler>();
});
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapTus2<MyTusHandler>("/files-tus-2");
});
}
Define a class called MyTusHandler
that inherits from tusdotnet.Tus2.TusHandler
and override the methods you would like to handle. The TusHandler
base class will handle communication with the storage so remember to call the base implementation in your override. Note that one does not need to override all methods, just the ones one wishes to handle differently than the default behavior. In most cases it is enough to override the FileComplete
method which is called when the upload is complete.
public class MyTusHandler : TusHandler
{
private readonly ILogger _logger;
private readonly Tus2StorageFacade _storage;
public MyTusHandler(ILoggerFactory loggerFactory, Tus2StorageFacade storage)
: base(storage)
{
_logger = loggerFactory.CreateLogger(nameof(MyTusHandler));
_storage = storage;
}
public override bool AllowClientToDeleteFile => true;
// Set to false to not report progress back to the client. This is needed as some clients (e.g. the one in golang) errors out after a fixed number of 104 responses.
// Default value is true.
public override bool EnableReportingOfProgress => true;
// Limits that apply for this handler to handle files. Limits or any of its properties can be null. Note that these are note validated on the next request and are just kept as a hint for the client.
// Default value is null (i.e. no limits).
public override TusHandlerLimits? Limits => new()
{
Expiration = TimeSpan.FromHours(24),
MaxSize = 1024 * 1024 * 1024,
MaxAppendSize = 100 * 1024 * 1024,
MinSize = 1024,
MinAppendSize = 5 * 1024 * 1024
};
public override async Task<CreateFileProcedureResponse> CreateFile(CreateFileContext context)
{
_logger.LogInformation("Creating file {UploadToken}", context.Headers.UploadToken);
var response = await _storage.CreateFile(context);
_logger.LogInformation("File created? {Success}", response.Status == System.Net.HttpStatusCode.Created);
return response;
}
public override async Task<UploadTransferProcedureResponse> WriteData(WriteDataContext context)
{
_logger.LogInformation("Receiving upload, starting at {UploadOffset}", context.Headers.UploadOffset);
var response = await base.WriteData(context);
_logger.LogInformation("Was success? {Success}", response.Status == System.Net.HttpStatusCode.Created);
return response;
}
public override async Task<UploadRetrievingProcedureResponse> RetrieveOffset(RetrieveOffsetContext context)
{
_logger.LogInformation("Retrieving offset for {UploadToken}", context.Headers.UploadToken);
var response = await base.RetrieveOffset(context);
_logger.LogInformation("Offset is {UploadOffset}", response.UploadOffset);
return response;
}
public override async Task<UploadCancellationProcedureResponse> Delete(DeleteContext context)
{
_logger.LogInformation("Deleting file {UploadToken}", context.Headers.UploadToken);
var response = await base.Delete(context);
_logger.LogInformation("File deleted? {Deleted}", response.Status == System.Net.HttpStatusCode.NoContent);
return response;
}
public override Task FileComplete(FileCompleteContext context)
{
_logger.LogInformation("File {UploadToken} is complete", context.Headers.UploadToken);
return base.FileComplete(context);
}
}
The tus2 implementation also supports creating storage instances using a factory. The factory supports creating "named storage" which allows to separate different storage options into different instances similar to HttpClientFactory.
services.AddTus2(options =>
{
// SimpleTus2StorageFactory being a class implementing ITus2StorageFactory.
// Same as adding a scoped instance of <ITus2StorageFactory, SimpleTus2StorageFactory()>
options.AddStorageFactory(new SimpleTus2StorageFactory());
options.AddHandler<MyTusHandler>();
});
app.UseEndpoints(endpoints =>
{
endpoints.MapTus2<MyTusHandler>("/files-tus-2");
});
// Handler constructor needs to be updated to use the storage factory and a possible name of the storage.
public class MyTusHandler : TusHandler
{
public MyTusHandler(ITus2ConfigurationManager config)
: base(config, "MyStorage") // "MyStorage" is optional and will provide the string "MyStorage" to the factory.
{
}
...
}
In tus2 locks are not used. Instead all previous upload requests for a single Upload-Token
must be terminated when a new request for the same Upload-Token
is received. In tusdotnet this is handled by the IOngoingUploadManager
. By default, an OngoingUploadManagerInMemory
instance will be used. If you run your setup in a cluster you will need to switch to either OngoingUploadManagerDiskBased
and point it to a shared disk or implement your own.
services.AddTus2(options =>
{
// Add disk based ongoing upload manager as a scoped instance.
options.AddDiskBasedUploadManager(@"C:\tusfiles");
// The above is the same as calling:
options.AddUploadManager(new OngoingUploadManagerDiskBased(new() { SharedDiskPath = @"C:\tusfiles" }));
// OR use your own:
// Add an instance as a scoped instance...
options.AddUploadManager(new RedisOngoingUploadManager("connection string"));
// ... or add ongoing upload manager factory as a scoped instance.
builder.AddUploadManagerFactory(new RedisOngoingUploadManagerFactory());
});
Register the OngoingUploadManagerDiskBased
in your DI and tusdotnet will automatically solve the new locking behavior. You can also implement your own implementation of IOngoingUploadManager
and use that.
When adding tus2 to your DI container the following is added:
- Tus2Storage instance (if
builder.AddStorage
is used) - Tus2StorageFacade instance (if
builder.AddStorage
is used) - Any factories registered (both for storage and ongoing upload manager)
- ITus2ConfigurationManager instance which can grab the storage and ongoing upload manager
Tus2StorageFacade is a wrapper around Tus2Storage which makes is easier to work with the entire tus2 flow instead of just calling methods directly on the storage.
services.AddTus2(options =>
{
// Defaults
options.AddDiskStorage(@"C:\tusfiles");
options.AddUploadManager(new OngoingUploadManagerDiskBased(new() { SharedDiskPath = System.IO.Path.GetTempPath() }));
// Storage factory
options.AddStorageFactory(new SimpleTus2StorageFactory());
});
public class MyService
{
private readonly ITus2ConfigurationManager _config;
private readonly Tus2Storage _defaultStorage;
private readonly Tus2StorageFacade _defaultStorageFacade;
private readonly IOngoingUploadManager _defaultUploadManager;
public MyService(
ITus2ConfigurationManager config,
IOngoingUploadManager defaultUploadManager,
Tus2StorageFacade facade,
TUs2Storage storage)
{
_config = config;
_defaultUploadManager = defaultUploadManager;
_defaultStorageFacade = facade;
_defaultStorage = storage;
}
public async Task MyMethod()
{
var defaultStorage = await _config.GetDefaultStorage();
var defaultStorage2 = await _config.GetDefaultStorage();
// Calls SimpleTus2StorageFactory.CreateNamedStorage with name "MyProfile".
var myProfileStorage = await _config.GetNamedStorage("MyProfile");
var defaultUploadManager = await _config.GetDefaultUploadManager();
// True, the storage factory is only called once per scope.
Assert.AreEqual(defaultStorage, defaultStorage2);
// True
Assert.AreEqual(defaultUploadManager, _defaultUploadManager);
// True, the facade is just a wrapper around the storage
Assert.AreEqual(_defaultStorageFacade.Storage, _defaultStorage);
}
}
Test site only is available for ASP.NET Core 8 (.NET 8) as the tus2 implementation requires .NET classes only found in Core 3.1 and later.
This project is licensed under the MIT license, see LICENSE.
Discussion can be held in this issue: #164