docs/proposals/multipart_transfer_mode.md
This is a proposal for a new transfer mode, designed to support multi-part HTTP uploads. This is a protocol extension to
Git LFS, defining a new transfer mode to be implemented by Git LFS clients and servers in addition to the current basic
transfer mode.
This proposal is based on the experimental multipart-basic transfer mode originally
implemented by datopian/giftless.
Many storage vendors and cloud vendors today offer an API to upload files in "parts" or "chunks", using multiple HTTP requests, allowing improved stability and performance. This is especially handy when files are multiple gigabytes in size, and a failure during the upload of a file would require re-uploading it, which could be extremely time consuming.
The purpose of the multipart transfer mode is to allow Git LFS servers and client facilitate direct-to-storage
uploads for backends supporting multipart or chunked uploads.
As the APIs offered by storage vendors differ greatly, multipart transfer mode will offer abstraction over most
of these complexities in hope of supporting as many storage vendors as possible.
Throughout this document, the following terms are in use:
batch request is sentbasic transfer APImultipartbasic requests except that {"transfers": ["multipart", "basic"]} is the
expected transfers value. Clients MUST retain basic as the fallback transfer mode to ensure compatibility with
servers not implementing this extension.{"operation": "download"} replies work exactly like basic download request with no change{"operation": "upload"} replies will break the upload into several actions:
parts (optional), a list of zero or more part upload actionsverify (optional), an action to verify the file is in storage, similar to basic upload verify actionsabort (optional), an action to abort the upload and clean up all unfinished chunks and statebasic transfers, if the file fully exists and is committed to storage, no actions will be provided
in the reply and the upload can simply be skippedverify action is provided, calling it is required and not optional. In some cases, this endpoint may be used
to finalize the upload.parts with a verify action may mean all parts have been uploaded but verify still
needs to be called by the client.basic protocol.Each one of the parts and the verify and abort actions contain instructions for the client on how to send a
request performing a the particular action. These are similar to basic transfer adapter actions but may include
some common, as well as action specific additional parameters.
All actions allow href, header and expires_in parameters just like basic transfer actions.
parts actionsEach parts action should include the pos and size attributes, in addition to the attributes specified
above:
pos indicate the position in bytes within the file in which the part should begin. If not specified, 0 (that is the
beginning of the file) is assumed.size is the size of the part in bytes. If size is omitted, default to read until the end of file.pos and size are omitted, the action is expected to be a single-part upload of the entire fileIn addition, parts actions may include the following parameters:
method, with PUT as the default method if none is specified. This allows customizing the HTTP method used when uploading
object parts.want_digest to specify an expected HTTP Digest header, as described below.verify actionThe verify action is similar to basic transfer mode verify, with the following additional parameters:
params - an object with additional parameters to send to the server when sending the verify request.
These parameters are to be sent to the server exactly as provided, as the value of the params JSON attribute.abort actionThe abort action may include the method attribute as specified for parts actions above.
The following is a ~10mb file upload request:
{
"transfers": ["multipart", "basic"],
"operation": "upload",
"objects": [
{
"oid": "20492a4d0d84f8beb1767f6616229f85d44c2827b64bdbfb260ee12fa1109e0e",
"size": 10000000
}
]
}
The following is a response for the same request, given an imaginary storage backend:
{
"transfer": "multipart",
"objects": [
{
"oid": "20492a4d0d84f8beb1767f6616229f85d44c2827b64bdbfb260ee12fa1109e0e",
"size": 10000000,
"actions": {
"parts": [
{
"href": "https://storage.cloud.example/storage/upload/20492a4d0d84?part=0",
"header": {
"Authorization": "Bearer someauthorizationtokenwillbesethere"
},
"pos": 0,
"size": 2500000,
"expires_in": 86400
},
{
"href": "https://storage.cloud.example/storage/upload/20492a4d0d84?part=1",
"header": {
"Authorization": "Bearer someauthorizationtokenwillbesethere"
},
"pos": 2500000,
"size": 2500000,
"expires_in": 86400
},
{
"href": "https://storage.cloud.example/storage/upload/20492a4d0d84?part=2",
"header": {
"Authorization": "Bearer someauthorizationtokenwillbesethere"
},
"pos": 5000000,
"size": 2500000,
"expires_in": 86400
},
{
"href": "https://storage.cloud.example/storage/upload/20492a4d0d84?part=3",
"header": {
"Authorization": "Bearer someauthorizationtokenwillbesethere"
},
"pos": 7500000,
"expires_in": 86400
}
],
"verify": {
"href": "https://lfs.mycompany.example/myorg/myrepo/multipart/verify",
"authenticated": true,
"header": {
"Authorization": "Basic 123abc123abc123abc123abc123="
},
"expires_in": 86400,
"params": {
"uploadId": "20492a4d0d84",
"partIds": [0, 1, 2, 3]
}
},
"abort": {
"href": "https://storage.cloud.example/storage/upload/20492a4d0d84",
"authenticated": true,
"header": {
"Authorization": "Basic 123abc123abc123abc123abc123="
},
"method": "DELETE",
"expires_in": 86400
}
}
}
]
}
verify request exampleGiven the batch response above, after all parts have been uploaded the client should send the following verify
request to https://lfs.mycompany.example/myorg/myrepo/multipart/verify:
POST /myorg/myrepo/multipart/verify
Host: lfs.mycompany.example
Authorization: Basic 123abc123abc123abc123abc123=
Content-type: application/json
{
"oid": "20492a4d0d84f8beb1767f6616229f85d44c2827b64bdbfb260ee12fa1109e0e",
"size": 10000000,
"params": {
"uploadId": "20492a4d0d84",
"partIds": [0, 1, 2, 3]
}
}
Assuming that all parts were uploaded successfully, the server should respond with a 200 OK response.
abort request exampleGiven the batch response above, the client may choose to cancel the upload by sending the following
abort request to https://storage.cloud.example/storage/upload/20492a4d0d84:
> DELETE /storage/upload/20492a4d0d84
> Host: storage.cloud.example
> Content-length: 0
Some storage backends will support, or even require, uploading clients to send a digest of the uploaded part when uploading the part. This is a useful capability even if not required, as it allows backends to validate each part separately as it is uploaded.
To support this, parts request objects may include a want_digest value, which is expected to be a list of digest
algorithms in the same format of the Want-Digest HTTP header specified by RFC-3230.
Any cryptographically secure digest algorithm registered with IANA
via the process outlined in RFC-3230 may be specified in want_digest. Algorithms
considered cryptographically insecure, including MD5 and SHA-1, should not be accepted. Namely, contentMD5 is
not an accepted value of want_digest.
If one or more digest algorithms with non-zero q-value is specified in want_digest, clients should select a favored
supported algorithm, calculate the part digest using that algorithm, and send it when uploading the part using the Digest HTTP
header as specified by RFC-3230 section 4.3.1.
While clients may include the part digest calculated using more than one algorithm, this is typically not required and should be avoided.
Note that if want_digest is specified but the client cannot support any of the requested algorithms, the client may
still choose to continue uploading parts without sending a Digest header. However, the storage server may choose
to reject the request in such cases.
want_digest in the replyWith SHA-512 as a preferred algorithm, and SHA-256 as a less preferred option if SHA-512 is not possible:
{
"actions": {
"parts": [
{
"href": "https://storage.cloud.example/storage/upload/20492a4d0d84?part=3",
"header": {
"Authorization": "Bearer someauthorizationtokenwillbesethere"
},
"pos": 7500001,
"want_digest": "sha-512;q=1.0, sha-256;q=0.5"
}
]
}
}
Following on the want_digest value specified in the last example, the client should now send the following headers
to the storage server when uploading the part, assuming SHA-512 is supported:
HTTP/1.1 PUT /storage/upload/20492a4d0d84?part=3
Authorization: Bearer someauthorizationtokenwillbesethere
Digest: SHA-512=thvDyvhfIqlvFe+A9MYgxAfm1q5thvDyvhfIqlvFe+A9MYgxAfm1q5=
For each one of the parts, as well as abort and verify requests sent by the client, the following responses are
to be expected:
Any response with a 20x status code is to be considered by clients as successful. This ambiguity is by design, to
support variances between vendors (which may use 200 or 201 to indicate a successful upload, for example).
Any other response is to be considered as an error, and it is up to the client to decide whether the request should be retried or not. Implementors are encouraged to follow standard HTTP error status code guidelines.
batch replies for partially uploaded contentWhen content was already partially uploaded, the server is expected to return a normal reply but omit request and parts
which do not need to be repeated. If the entire file has been uploaded, it is expected that no actions value will be
returned, in which case clients should simply skip the upload.
However, if parts of the file were successfully uploaded while others weren't, it is expected that a normal reply would
be returned, but with less parts to send.
verify HTTP 409 errorsAn HTTP 409 error on verify requests typically indicates that the file could not be fully committed or verified.
In this case, clients should follow the following process to try and recover from the error:
batch request to see if any parts of the file were not uploaded yet. If there are still parts to upload
(i.e. parts is not empty), proceed to upload them and re-do verifyparts is empty, it is possible that the file exists in storage but is corrupt / has wrong size. In this case
it is recommended to issue an abort and re-attempt the same upload againIt is up to the LFS server to decide the size of each file chunk.
As multipart uploads tend to require much more time than simple uploads, it is recommended to allow for longer expires_in
values than one would consider for basic uploads. It is possible that the process of uploading a single object in multiple
parts may take several hours from batch to verify.
basic transfer for small filesUsing multipart upload APIs has some complexity and speed overhead. For this reason, if a client specifies support for
both multipart and basic transfer modes in a batch request, and the object(s) uploaded are small enough to fit in a
single part upload, servers may choose to respond with a basic transfer mode even if multipart is supported:
For example a small (2mb) upload batch request:
{
"transfers": ["multipart", "basic"],
"operation": "upload",
"objects": [
{
"oid": "13aea96040f2133033d103008d5d96cfe98b3361f7202d77bea97b2424a7a6cd",
"size": 2000000
}
]
}
May be responded with:
{
"transfer": "basic",
"objects": [
...
]
}
Even if the server does support multipart, as basic can be preferable in this case.
While part requests are typically quite similar between vendors, the specifics of multipart upload initialization and
commit procedures are very specific to vendors. For this reason, in many cases, it will be up to the LFS server to
take care of initialization and commit code. This is fine, as long as actual uploaded data is sent directly to the
storage backend.
For example, in the case of Amazon S3:
These are very hard to abstract in a way that would allow clients to send them directly to the server. In addition, as we do not want to maintain any state in the server, there is a need to make two requests when finalizing the upload: one to fetch a list of uploaded chunks, and another to send this list to the S3 finalization endpoint.
For this reason, it is expected that any initialization actions will be handled by the Git LFS server during the
batch request handling. In most cases, the verify action will also be responsible for any finalization / commit actions.
The params attribute of the verify action is designed specifically to transfer some vendor-specific "state" between
initialization and finalization of the upload process.
abort actionsSome storage backends will accept a simple DELETE or POST request to a URL, with no request body, in order to abort
the upload. In such cases, abort may refer directly to the storage backend. However, in cases where aborting the upload
requires more complex logic or some payload in the request body, abort actions should point to an endpoint of the LFS
server, and it should be up to the LFS server to abort the upload and clean up any partially uploaded parts.
As abort requests do not have a body, any parameters required by the LFS server in order to complete the request should
be passed as part of the URL in the href parameter.
It should be noted that clients will not always be able to abort partial uploads cleanly. Implementors are expected to
ensure proper cleanup of partially uploaded files via other means, such as a periodical cron job that locates uncommitted
uploaded parts and deletes them.