Uploading in parts
A large file over a slow or unreliable connection is hard to send in one go: when the connection drops two gigabytes in, an ordinary upload has to start again from the beginning. An upload session sends the file in parts instead. What has arrived stays stored, so after a dropped connection, or even a restart of the server, the client asks how much is stored and carries on from there. Nothing shows the new version until the session is completed, and then it becomes the name's current version all at once, as an upload's does.
Every built-in provider supports sessions: SQL Server, PostgreSQL, SQLite, the file system, S3, Azure Blob and in-memory.
This page is about calling sessions from your own code. For uploads from browsers and apps, map resumable upload endpoints, which tus clients such as Uppy use.
A session from start to finish
// 1. Start: say what the file is. Nothing is stored yet.
ResourceUploadSession upload = await container.StartUploadAsync("video.mp4", new ResourceUploadSessionOptions
{
Headers = new ResourceHeaders { ContentType = "video/mp4", ContentLength = fileLength },
Metadata = new Dictionary<string, string> { ["camera"] = "front door" }
});
// 2. Append parts, each from where the last one ended.
foreach (Stream part in parts)
upload = (await container.AppendUploadAsync("video.mp4", upload.UploadId, upload.Offset, part))!;
// 3. Complete: the version becomes current.
ResourceInfo? stored = await container.CompleteUploadAsync("video.mp4", upload.UploadId);
StartUploadAsynctakes the version's content type, metadata and expiry, and the length the file is expected to have. It returns aResourceUploadSessionwith the session'sUploadId, which every later call names, and itsOffset: how many bytes are stored, 0 at the start. Keep the upload ID somewhere that survives a restart, such as your own database or the client that's uploading.AppendUploadAsyncstores a part. It takes the offset the part starts at, which must be the session's currentOffset, and returns the session with its new offset. A part can be any size, from a few bytes to many gigabytes, and parts don't have to be the same size.CompleteUploadAsyncmakes the version current and returns it, asUploadAsyncwould have. The version's ID is the session's upload ID.CancelUploadAsyncgives up on a session and deletes what it stored.
Resuming after a failure
An append stores its part as it arrives, about a MiB at a time with SQL Server, PostgreSQL and SQLite and up to 64 MiB at a time with the file system, S3 and Azure Blob, so an append that fails partway keeps what it stored. (The in-memory provider stores an append whole or not at all.) To resume, ask the session where it ended, and send the rest from there:
ResourceUploadSession? current = await container.GetUploadAsync("video.mp4", uploadId);
if (current is null)
{
// The session was completed, canceled, or deleted after a day without appends: start again.
}
else
{
fileStream.Position = current.Offset;
await container.AppendUploadAsync("video.mp4", uploadId, current.Offset, fileStream);
}
An append whose offset isn't where the stored content ends throws ResourceUploadOffsetException, and stores
nothing. Its CurrentOffset says where to resume. That's what happens when an append did store its part but its
answer never reached you, and you send the same part again: the second copy is refused, so the content is never
stored twice.
Completing
Completing reads the stored content once from the start, to measure its length, compute its hash and pass it to your upload inspector, if you have one. A session can't hash its content as the parts arrive, because the parts can come from different processes, and a hash's running state can't be handed from one to the next. So completing a large file takes about as long as reading it back: a few seconds per gigabyte from a local database.
The write's mode and conditions go to CompleteUploadAsync, because that's when they're decided:
ResourceInfo? stored = await container.CompleteUploadAsync("video.mp4", uploadId, new ResourceCompleteUploadOptions
{
Mode = ResourceWriteMode.Replace,
Conditions = new ResourceConditions { IfMatchVersionId = versionTheUserSaw }
});
ResourceWriteMode.Create, the default, refuses a name that already has a current version, withResourceConflictException.StartUploadAsyncdoesn't check this, and another upload can create the name while the session is open, so aCreatecompletion can still be refused.ResourceWriteMode.Replacemakes the session's version current over the existing one, and older versions are pruned as after any upload.- Conditions are checked against the name's current version as the
session's version becomes current. They need
ResourceWriteMode.Replace, as for an upload; withCreate,CompleteUploadAsyncthrowsArgumentException.
When the mode, the conditions, the expected length or the container's policy refuses the completion, the session
stays as it was, so you can complete it again, for example with Replace, or after appending the rest. These checks
come before the content is read, so a completion that's refused costs little. When the upload inspector refuses the
content, the session is deleted, since that content is never to be stored.
Limits and cleanup
- Container policies apply as for an upload.
StartUploadAsyncchecks that the container isn't read-only, that it allows the content type, and that the expected length fits. An append that would go pastMaxResourceSize, or pastMaxContainerSize, throwsResourcePolicyViolationException; part of what arrived before the limit may stay stored. Completing checks the container's size and count limits with the real length. - A session's content doesn't count toward the container's usage until the session completes, so it doesn't take room from other uploads meanwhile, and its appends are held only to the container's limits as a whole, not to the room that's left. Limit who can start sessions as you limit who can upload.
- The expected length, when you give
StartUploadAsyncone, is checked against the container's size limits at the start, and again as the session completes: completing a session whose appended content is a different length throwsResourceContentLengthMismatchException, so a client that completes too early doesn't store a cut-off file.ResourceUploadSession.DeclaredLengthreturns this length, so a client that resumes knows when it's done. - A session nothing is appended to for 24 hours is deleted by the expiry sweep, along with what it
stored. Without
UseExpiry, nothing sweeps sessions: callDeleteExpiredAsyncon a schedule, or cancel the sessions you abandon, or they stay. With a provider of your own that doesn't support expiry, cancel them: there's no sweep to run. - A session isn't moved or copied with its name: moving or renaming the resource leaves a session for the old name where it is.
- With the file system, completing copies the session's content into the version's file, through your application, so the content is written twice. A session holds as much as the disk has room for.
- With S3 and Azure Blob, a session holds as much as one upload: 5 TiB with S3 and about 48 TiB with Azure Blob. An
append past that throws
ResourceUploadLimitException; what the session stored stays, but it takes no more content. The session stores its appends in pieces of up to 64 MiB, and once it holds as many as the store can join, the next append first has the store join those since the last join into one, which takes longer. Appends smaller than 8 MiB each cost more there too, since each reads back the short piece the last one stored. See the providers' pages for the details.
What each call returns when there's no session
GetUploadAsync, AppendUploadAsync and CompleteUploadAsync return null, and CancelUploadAsync returns
false, when there's no session with that ID for that name: it was completed, canceled or swept, or never existed.
Telemetry
Each call records a span: resourcefull upload_start, upload_get, upload_append (with resourcefull.offset),
upload_complete and upload_cancel. Appends add to resourcefull.client.appended, the bytes stored by appends,
and a completion adds the version's length to resourcefull.client.uploaded, as an upload does. See
Telemetry.