feat(Storage)!: Enable full object checksum validation for resumable uploads - #15769
feat(Storage)!: Enable full object checksum validation for resumable uploads#15769mahendra-google wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the upload validation mechanism in the Google Cloud Storage V1 client. It introduces UploadValidationMode.RejectAndThrow to allow the server to reject mismatched objects before creation, deprecates UploadValidationMode.DeleteAndThrow, and removes UploadValidationMode.ThrowOnly along with the UploadValidationException class. It also introduces a HashingStream within CustomMediaUpload to calculate CRC32C hashes on the fly and inject the x-goog-hash header. The feedback highlights that removing ThrowOnly and UploadValidationException are breaking changes for existing client applications, and recommends restoring them with an [Obsolete] attribute to maintain backward compatibility.
I am having trouble creating individual review comments. Click here to see my feedback.
apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/UploadValidationMode.cs (29-35)
Removing ThrowOnly from the UploadValidationMode enum is a breaking change for existing client applications. To maintain backward compatibility, please restore ThrowOnly and mark it as [Obsolete], mapping it to the new validation behavior.
/// <summary>
/// Obsolete. Use <see cref="RejectAndThrow"/> instead.
/// Previously, the hash of the data was computed while uploading, and if the resulting object had a different hash, an exception was thrown, but the object remained present in Storage.
/// </summary>
[Obsolete("ThrowOnly is deprecated. Use RejectAndThrow instead, as the server now rejects the object before creation.")]
ThrowOnly = 1,
/// <summary>
/// Obsolete. Use <see cref="RejectAndThrow"/> instead.
/// Previously, the object was uploaded and then deleted if the hash mismatched.
/// The server now rejects mismatched objects automatically before creation.
/// </summary>
[Obsolete("DeleteAndThrow is deprecated. Use RejectAndThrow instead, as the server now rejects the object before creation.")]
DeleteAndThrow = 2,apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/UploadValidationException.cs (23-26)
Removing the public class UploadValidationException is a breaking change for existing client applications that catch this exception.
To maintain backward compatibility, please restore this class and mark it as [Obsolete]. This allows existing code to compile with a warning rather than failing with a compilation error.
|
Let's keep this one in draft until we have updated the core libraries to the newest dependency. |
8160401 to
4b0d135
Compare
- Add integration tests to verify ArgumentException propagation through Google.Apis ResumableUpload when resuming an upload from the intermediate offset.
amanda-tarafa
left a comment
There was a problem hiding this comment.
Because of the breaking changes you need to update the next version of the library in pipeline-state.json. Add a new property for Storage "nextVersion" with value "5.0.0" and next time we release we'll automatically bump major.
| <PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="All" /> | ||
| <PackageReference Include="Google.Apis" VersionOverride="1.76"/> | ||
| <PackageReference Include="Google.Api.Gax.Rest" /> | ||
| <PackageReference Include="Google.Apis.Storage.v1" VersionOverride="[1.74.0.4115, 2.0.0.0)" /> |
There was a problem hiding this comment.
We have https://www.nuget.org/packages/Google.Apis.Storage.v1/1.76.0.4250 already.
| "Cannot perform hash validation when resuming an upload from a non-zero offset, " + | ||
| "as the complete stream contents are required to compute the hash. " + | ||
| "To resume this upload, disable validation by setting UploadValidationMode to None.", | ||
| nameof(stream)); |
There was a problem hiding this comment.
Two notes on this:
- If you add this error message here, the upload will execute completely and only at the end the user will know that they cannot resume it with hash validation. That seems like a waste of resources and a poor user experience. Cant' you check this at the beginning of the resume? Or fail as soon as you find gaps on the hash, etc.? I won't block my approval on this but calling it out as a significant improvement.
- You can still do hash validation on resumes, which are only supported for seekable streams, You still upload from the resume position, but you can calculate the hash for the whole stream rewinding as necessary. This is an improvement to consider for a follow up PR.
| ProcessBytes(buffer, offset, bytesRead, startingPos); | ||
| if (!_stream.CanSeek) | ||
| { | ||
| _position += bytesRead; |
There was a problem hiding this comment.
Consider always maintaining _position up to date, initialized to the _stream.Position if the stream is seekable, otherwise, 0, and then you don't have to use this if everywhere.
| var mediaUpload = new CustomMediaUpload(Service, destination, destination.Bucket, source, destination.ContentType, options); | ||
| options?.ModifyMediaUpload(mediaUpload); |
There was a problem hiding this comment.
This is "weird", the options are meant to modify the mediaUpload object, but now we use them to create the mediaUpload object and then to modify it.
This is non blocker, but for the sake o consistency, can we not modify the CustomMediaUpload constructor? Then add a method in the options WrapSourceStream that wraps the source in a HashingStream. You would then pass that maybe wrapped sourced to the original constructos of CustomMediaUpload which can know if it has to add the last request callback based on if the stream is a HashStream or not.
var maybeHashingSource = options?.WrapStreamSource(source);
var mediaUpload = new CustomMediaUpload(Service, destination, destination.Bucket, maybeHashingSource, destination.ContentType);
options?.ModifyMediaUpload(mediaUpload);
| var hashingStream = new CustomMediaUpload.HashingStream(baseStream); | ||
| var buffer = new byte[data.Length]; | ||
|
|
||
| // Simulate resuming an upload from a new process starting at intermediate offset 10 |
There was a problem hiding this comment.
A new process wouldn't use the same hashingStream instance. Move the source stream instead and create a new instance of HashingStream for this test to be really "isolated".
| } | ||
|
|
||
| [Fact] | ||
| public void CustomMediaUpload_ShouldThrowArgumentException_WhenResumingFromIntermediateOffset() |
There was a problem hiding this comment.
I think this test is redundant with the three above. You tested that the HashingStream does as expected (that should be moved to unit tests) and tested that the CustomMediaUploader does as expected.
This test is just a combination of that.
| } | ||
|
|
||
| [Fact] | ||
| public void CustomMediaUpload_ShouldContainHashHeaderAndCorrectHash_WhenRetriedFromIntermediateOffset() |
There was a problem hiding this comment.
This test should just be a success test, that is, after resuming, Storage accepts and creates the object. Testing the internals should be left to unit tests, if you need to test that the header is added correctly, then a unit test that has a fake service that intercepts the request and checks its headers is the way to go.
|
|
||
| private class BreakDeleteInterceptor : IHttpExecuteInterceptor | ||
| [Fact] | ||
| public void HashingStream_ShouldHandleRetries_WhenRestartedFromBeginning() |
There was a problem hiding this comment.
This is a unit test not an integration test. It's not talking to any service.
| } | ||
|
|
||
| [Fact] | ||
| public void HashingStream_ShouldHandleRetries_WhenSeekingBackwardsToIntermediatePoint() |
| } | ||
|
|
||
| [Fact] | ||
| public void HashingStream_ShouldDetectGaps_WhenResumingFromIntermediateOffset() |
…ry in pipeline-state.json - Assign value 5.0.0 to the nextVersion propery so that next storage release will be major release
…76.0.4250 in apis.json. - Removed Google.Apis 1.76 version dependency from Google.Cloud.Storage.V1 project file - Regenerated project file using generateprojects.sh
This PR transitions upload object checksum integrity validation from client-side to the server-side.
Previously,
CRC32Cvalidation occurred after the upload is completed, requiring the client to delete the object (usingDeleteAndThrowupload validation mode) or leave a corrupted object in the bucket (usingThrowOnlyupload validation mode) upon hash mismatches. By leveraging the newLastRequestExecutingevent ingoogle-api-dotnet-clientcore library, this implementation calculatesCRC32Cincrementally during streaming and injects thex-goog-hash: crc32c=...header on the final chunk. If a checksum mismatch occurs, the server rejects the upload with an HTTP400 Bad Request, ensuring invalid objects are never created in the bucket.