From d5d45081aa8add1c9556961b00f28e05ca90cd25 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Tue, 5 Mar 2024 14:44:09 +0000 Subject: [PATCH] feat: Allow a new upload session to be initiated as a single method call This allows the content length to be (optionally) specified, which is then propagated via X-Upload-Content-Length. This is currently just a prototype for comment - I'd want to add integration tests, some unit tests, and obviously XML docs. --- .../StorageClient.UploadObject.cs | 33 ++++++++++++ .../StorageClientImpl.UploadObject.cs | 53 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/StorageClient.UploadObject.cs b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/StorageClient.UploadObject.cs index 3be276bdb7bf..a804945723d8 100644 --- a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/StorageClient.UploadObject.cs +++ b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/StorageClient.UploadObject.cs @@ -153,5 +153,38 @@ public virtual Task UploadObjectAsync( { throw new NotImplementedException(); } + + /// + /// + /// + /// + /// + /// The token to monitor for cancellation requests. + /// + /// + public virtual Task InitiateUploadSessionAsync( + Object destination, + long? contentLength, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + /// + /// + /// + /// The name of the bucket containing the object. Must not be null. + /// The name of the object within the bucket. Must not be null. + /// The content type of the object. This should be a MIME type + /// such as "text/html" or "application/octet-stream". May be null. + /// + /// The token to monitor for cancellation requests. + /// + /// + public virtual Task InitiateUploadSessionAsync( + string bucket, + string objectName, + string contentType, + long? contentLength, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); } } diff --git a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/StorageClientImpl.UploadObject.cs b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/StorageClientImpl.UploadObject.cs index 3a9555e50d46..81a39ca2835c 100644 --- a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/StorageClientImpl.UploadObject.cs +++ b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/StorageClientImpl.UploadObject.cs @@ -17,8 +17,10 @@ using Google.Apis.Upload; using System; using System.IO; +using System.Runtime.InteropServices.WindowsRuntime; using System.Threading; using System.Threading.Tasks; +using static Google.Apis.Storage.v1.ObjectsResource; using Object = Google.Apis.Storage.v1.Data.Object; namespace Google.Cloud.Storage.V1 @@ -103,6 +105,29 @@ public override Task UploadObjectAsync( IProgress progress = null) => new UploadHelper(this, destination, source, options, progress).ExecuteAsync(cancellationToken); + /// + public override Task InitiateUploadSessionAsync(Object destination, long? contentLength, CancellationToken cancellationToken = default) + { + // We could potentially do a single validation, but the reasons for preventing negative and zero + // values are somewhat different. + GaxPreconditions.CheckNonNegative(contentLength, nameof(contentLength)); + if (contentLength == 0) + { + throw new ArgumentOutOfRangeException("A content length of 0 cannot be enforced. Use a null content length for 'any length'."); + } + ValidateObject(destination, nameof(destination)); + var upload = CreateObjectUploader(destination, new LengthOnlyStream(contentLength)); + return upload.InitiateSessionAsync(cancellationToken); + } + + /// + public override Task InitiateUploadSessionAsync(string bucket, string objectName, string contentType, long? contentLength, CancellationToken cancellationToken = default) + { + ValidateBucketName(bucket); + var obj = new Object { Bucket = bucket, Name = objectName, ContentType = contentType }; + return InitiateUploadSessionAsync(obj, contentLength, cancellationToken); + } + /// /// Helper class to provide common context between sync and async operations. Helps avoid quite so much duplicate code... /// @@ -192,5 +217,33 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) return result; } } + + private sealed class LengthOnlyStream : Stream + { + private readonly long? _length; + internal LengthOnlyStream(long? length) => _length = length; + + public override long Length => _length ?? throw new NotSupportedException(); + public override bool CanSeek => _length.HasValue; + + public override bool CanRead => throw new NotImplementedException(); + public override bool CanWrite => throw new NotImplementedException(); + + public override long Position + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public override void Flush() => throw new NotImplementedException(); + public override int Read(byte[] buffer, int offset, int count) => + throw new NotImplementedException(); + public override long Seek(long offset, SeekOrigin origin) => + throw new NotImplementedException(); + public override void SetLength(long value) => + throw new NotImplementedException(); + public override void Write(byte[] buffer, int offset, int count) => + throw new NotImplementedException(); + } } }