feat: support presigned multipart upload (PMPU) on GCS - #10521
Open
ggoggam wants to merge 1 commit into
Open
Conversation
lakeFS already emulates multipart upload on GCS by writing each part as its own object and composing them on complete, but the presign multipart flow was S3-only: the GS adapter returned ErrOperationNotSupported from GetPresignUploadPartURL and never advertised PreSignSupportMultipart, so clients fell back to a single-part upload through the server. A part upload is a plain PUT of the part's object, so it can be pre-signed the same way a regular write is. GetPresignUploadPartURL now signs a V4 PUT URL for the object UploadPart writes to, and the namespace info reports multipart support so the API and lakectl use it. Presign multipart is off when a customer-supplied encryption key is configured: a pre-signed PUT writes the part without the key, and GCS requires the same key across all compose sources and the destination. It can also be turned off explicitly with the new blockstore.gs.disable_pre_signed_multipart setting, matching S3. Client side, presignedURLExpired only recognized X-Amz-Date/X-Amz-Expires, so every GCS URL looked expired and each part paid for a needless refresh round-trip. It now reads the X-Goog- parameters as well.
ggoggam
marked this pull request as draft
August 9, 2026 15:03
ggoggam
marked this pull request as ready for review
August 9, 2026 15:05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Change Description
Background
lakeFS already emulates multipart upload on GCS:
CreateMultiPartUploaddrops a marker object, each part is written as its own object (<key>.part_00001), andCompleteMultiPartUploadcomposes them. What was missing is the presign multipart flow, which was S3-only — the GS adapter returnedErrOperationNotSupportedfromGetPresignUploadPartURLand never setPreSignSupportMultipart.The effect is that on GCS,
POST /repositories/{repo}/branches/{branch}/staging/pmpuanswers 501, andhelpers.PreSignUploader(solakectl fs upload --pre-sign,lakectl local, and anything else on the Go client) falls back to a single pre-signed PUT for the whole object, no matter how large.Since a GCS part upload is just a PUT of the part's own object, it can be pre-signed exactly the way a regular write already is.
New Feature
Presigned multipart upload on the GS block adapter.
gs.Adapter.GetPresignUploadPartURLsigns a V4 PUT URL forformatMultipartFilename(uploadID, partNumber)— the same objectUploadPartwrites to — so the rest of the flow (ListParts, validation, compose, abort) is unchanged. Signing options were factored intonewSignedURLOptions, shared withGetPreSignedURL.GetStorageNamespaceInforeportsPreSignSupportMultipart, which is what the API and lakectl gate on.blockstore.gs.disable_pre_signed_multipartsetting, mirroring the existing S3 one.blockstore.gs.server_side_encryption_customer_suppliedis set. A pre-signed PUT writes the part without the customer-supplied key, and GCS requires the same key across every compose source and the destination, so the complete would fail. The adapter reports no support and returnsErrOperationNotSupportedrather than handing out a URL that produces an unusable upload.One client-side fix comes along with it:
helpers.presignedURLExpiredonly recognizedX-Amz-Date/X-Amz-Expires, so a GCS URL parsed as "expired" and every part paid for a needlessUploadPartrefresh round-trip before uploading. It now reads theX-Goog-parameters too.Testing Details
pkg/block/gs: newTestPresignMultipartUploadruns the whole flow against fake-gcs-server — create, pre-sign each part, PUT the bytes straight at the store over HTTP, list parts, complete, read the object back and compare.TestGetPresignUploadPartURLcovers the signed URL targeting the right part object, part-number bounds, both disable switches, and the CSEK case.pkg/block/blocktest:Adapter_PresignUploadPartURLadded to the shared multipart suite, asserting every adapter agrees with its ownPreSignSupportMultipartflag. Runs for gs/s3/azure/local/mem.pkg/api/helpers:TestPresignedURLExpiredcovers S3 and GCS URLs, fresh and stale, plus the unparseable cases that should fail safe.esti:skipPresignMultipartnow gates on the capability the server reports instead ofblockstore_type == "s3", so the existing esti presign multipart tests cover GCS deployments and still skip where the flow is disabled.go test ./pkg/block/... ./pkg/config/... ./pkg/api/helpers/...andgolangci-lint run ./pkg/...(plus--build-tags esti ./esti/...) pass locally.One note on the emulator: fake-gcs-server does not return an
ETagheader on an XML API upload, so the end-to-end test reads the stored part ETags viaListPartsrather than the ones a real GCS PUT response would carry. The ETag-is-hex-MD5 assumption the complete path validates against is the same one the existing GCS single-object presign upload already relies on.Breaking Change?
No. Additive — a new capability on a blockstore that previously reported none, and a new opt-out setting. Existing GCS deployments get presigned multipart uploads for large files; setting
blockstore.gs.disable_pre_signed_multipart: truerestores the previous behavior. No API, CLI, or client surface changed.Additional info
The API surface already existed and needed no changes —
CreatePresignMultipartUpload/UploadPart/CompletePresignMultipartUpload/AbortPresignMultipartUploadall route through the block adapter and simply work once the adapter advertises support.Contact Details
@ggoggam on GitHub.