Multipart upload and the ETag-that-isn't-an-MD5

A multipart object's ETag is a hash of the part hashes with a "-N" suffix, NOT the MD5 of the file — so the same bytes uploaded with a different part size produce a different ETag.

Previously

With the index telling the truth and the object stored durably, the last piece of an object's life is the upload itself — and objects too big for one request expose a famous integrity gotcha.

Scene 09

Multipart upload and the ETag-that-isn't-an-MD5

  1. Watch
  2. Try it
  3. Predict
  4. Capture
Multipart uploadeach part ≥ 5 MiB · uploaded in parallel · server stitches on completepart size64 MiB → 4 partschange it → ETag changesSOURCE FILE200 MiB0/4 up#164 MiB#264 MiB#364 MiB#48 MiBparallel · resumableSERVERcollects parts,stitches on completeawaiting complete…MANIFEST(partNumber, ETag)no parts yet
What to watch for

A 200 MiB file is sliced into 4 parts. Each uploads in parallel along its own arrow and returns its own ETag into the manifest. Once all four land, the server runs CompleteMultipartUpload and stitches them into one object — whose ETag renders as a hash-of-hashes ending in "-4".

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Client.multipartUpload
slice the file, upload parts in parallel, then complete
def multipartUpload(file, partSize):
uploadId = s3.createMultipartUpload(key)
manifest = []
for n, chunk in enumerate(slice(file, partSize), 1):
# each part >= 5 MiB except the last, partNumber 1..10000
etag = s3.uploadPart(uploadId, n, chunk)
manifest.append((n, etag)) # (partNumber, ETag)
return s3.completeMultipartUpload(uploadId, manifest)
Server.completeMultipartUpload
stitch the parts and compute the object ETag
def completeMultipartUpload(uploadId, manifest):
parts = sortByPartNumber(manifest) # order, not arrival
stitchIntoObject(parts)
if len(parts) == 1 and wasSinglePut(uploadId):
objectEtag = md5(object.bytes) # == md5(file)
else:
joined = concat(md5Binary(p) for p in parts)
objectEtag = md5(joined) + "-" + str(len(parts))
return objectEtag

Where this sits in Build an S3-style distributed object store

Scene 09 of 12, in the Make it real act — Strong consistency, multipart upload, lifecycle & tiering.. Parallel resumable parts, a completion manifest, and why the object ETag is a hash-of-hashes ending in -N.

Up next. That orphaned-parts cleanup rule is one declarative lifecycle action — and lifecycle rules also drive the bigger cost lever: sliding objects down to cheaper storage as they cool.

All 12 scenes in Build an S3-style distributed object store · Every curriculum

Built with Arqly
Every scene in Build an S3-style distributed object store builds on the one before it.All 12 Build an S3-style distributed object store scenes