Implement Put Block From URL - #2681
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for Block Blob Stage Block From URL (Put Block From URL) in Azurite by downloading the source content via the existing blob download path (so SAS/range/source conditions can be applied) and persisting the staged data through the normal extent + uncommitted-block flow.
Changes:
- Implements
stageBlockFromURLinBlockBlobHandlerby fetching the source via HTTP and staging it as an uncommitted block. - Introduces a new
SourceConditionNotMet(412) storage error for unmet source conditional headers during staging. - Expands the block blob API test suite with stageBlockFromURL coverage (range, full copy, unmet condition, missing source).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/blob/apis/blockblob.test.ts | Adds tests validating stageBlockFromURL behavior (ranges, full copy, 412 on unmet source condition, 404 on missing source). |
| src/blob/handlers/BlockBlobHandler.ts | Implements the stageBlockFromURL handler: validates input, downloads source data, persists it, and returns MD5. |
| src/blob/errors/StorageErrorFactory.ts | Adds getSourceConditionNotMet() (412) error factory helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const blobCtx = new BlobStorageContext(context); | ||
| const accountName = blobCtx.account!; | ||
| const containerName = blobCtx.container!; | ||
| const blobName = blobCtx.blob!; | ||
| const date = blobCtx.startTime!; | ||
|
|
| const sourceConditions = options.sourceModifiedAccessConditions || {}; | ||
| if (sourceConditions.sourceIfMatch !== undefined) { | ||
| headers["if-match"] = sourceConditions.sourceIfMatch; | ||
| } | ||
| if (sourceConditions.sourceIfNoneMatch !== undefined) { | ||
| headers["if-none-match"] = sourceConditions.sourceIfNoneMatch; | ||
| } | ||
| if (sourceConditions.sourceIfModifiedSince !== undefined) { | ||
| headers["if-modified-since"] = new Date( | ||
| sourceConditions.sourceIfModifiedSince | ||
| ).toUTCString(); | ||
| } | ||
| if (sourceConditions.sourceIfUnmodifiedSince !== undefined) { | ||
| headers["if-unmodified-since"] = new Date( | ||
| sourceConditions.sourceIfUnmodifiedSince | ||
| ).toUTCString(); | ||
| } |
| const currentServer = blobCtx.request!.getHeader("Host") || ""; | ||
| if (currentServer !== url.host) { | ||
| this.logger.error( | ||
| `BlockBlobHandler:stageBlockFromURL() Source ${url} is not on the same Azurite instance as target account ${accountName}`, | ||
| context.contextId | ||
| ); | ||
| throw StorageErrorFactory.getCannotVerifyCopySource( | ||
| context.contextId!, | ||
| 404, | ||
| "The specified resource does not exist" | ||
| ); | ||
| } |
|
Addressed the review in aa00b52:
|
| const scheme = "encrypted" in rawRequest.socket ? "https" : "http"; | ||
| const pinnedUrl = | ||
| `${scheme}://127.0.0.1:${rawRequest.socket.localPort}` + | ||
| `${url.pathname}${url.search}`; |
| if (options.sourceRange !== undefined) { | ||
| headers.range = options.sourceRange; | ||
| } |
|
Second round addressed in 381b126: the source fetch now pins to the local address the request arrived on (bracketing IPv6 literals) instead of hard-coded 127.0.0.1, so non-loopback |
|
|
||
| // Fetch the source range over loopback so that SAS authentication, | ||
| // range handling, and source conditions reuse the download path. | ||
| const headers: { [key: string]: string } = {}; |
|
Addressed in 8847aec: the pinned loopback fetch now sends the original source URL host as the |
| const currentServer = blobCtx.request!.getHeader("Host") || ""; | ||
| if (currentServer !== url.host) { |
|
Addressed in e665103: the same-instance comparison now lowercases the client-supplied Host header before comparing against the already-lowercased URL host, with a mixed-case Host regression test. |
Stage the block by fetching the copy source over loopback so that SAS authentication, x-ms-source-range, and the x-ms-source-if-* conditions are enforced by the existing download path, then persist it through the same extent flow as Put Block. Only sources on the same Azurite instance are supported, matching copyFromURL. The response carries the MD5 of the staged content and source condition failures return 412 SourceConditionNotMet as on the real service. Validated with the blockblob test suite and end to end with S3Proxy's native multipart part copy, which previously fell back to streamed emulation on Azurite's 501. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
e665103 to
d2654cb
Compare
|
Squashed after AI review succeeded. |
Stage the block by fetching the copy source over loopback so that SAS authentication, x-ms-source-range, and the x-ms-source-if-* conditions are enforced by the existing download path, then persist it through the same extent flow as Put Block. Only sources on the same Azurite instance are supported, matching copyFromURL. The response carries the MD5 of the staged content and source condition failures return 412 SourceConditionNotMet as on the real service.
Validated with the blockblob test suite and end to end with S3Proxy's native multipart part copy, which previously fell back to streamed emulation on Azurite's 501.