Skip to content

Commit 6a2841e

Browse files
committed
Derived uploader for signed URLs
1 parent 6d2d838 commit 6a2841e

File tree

3 files changed

+207
-106
lines changed

3 files changed

+207
-106
lines changed

apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.IntegrationTests/UrlSignerTest.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ private void ResumableUploadTest_InitDelayTest()
524524
url = _fixture.UrlSigner.Sign(bucket, name, duration, UrlSigner.ResumableHttpMethod);
525525

526526
// Verify that the URL works initially.
527-
var uploader = TmpResumableUpload.CreateFromSignedUrl(url, new MemoryStream(data));
527+
var uploader = SignedUrlResumableUpload.Create(url, new MemoryStream(data));
528528
var progress = await uploader.UploadAsync();
529529
Assert.Equal(UploadStatus.Completed, progress.Status);
530530

@@ -537,7 +537,7 @@ private void ResumableUploadTest_InitDelayTest()
537537
},
538538
afterDelay: async () =>
539539
{
540-
var uploader = TmpResumableUpload.CreateFromSignedUrl(url, new MemoryStream(data));
540+
var uploader = SignedUrlResumableUpload.Create(url, new MemoryStream(data));
541541

542542
// Verify that the URL no longer works.
543543
var progress = await uploader.UploadAsync();
@@ -563,7 +563,7 @@ private void ResumableUploadResumeTest_InitDelayTest()
563563
beforeDelay: async duration =>
564564
{
565565
url = _fixture.UrlSigner.Sign(bucket, name, duration, UrlSigner.ResumableHttpMethod);
566-
var sessionUri = await TmpResumableUpload.GetSessionUriAsync(url);
566+
var sessionUri = await SignedUrlResumableUpload.InitiateSessionAsync(url);
567567

568568
// Verify that the URL works initially.
569569
var uploader = TmpResumableUpload.CreateFromSessionUri(sessionUri, new MemoryStream(data));
@@ -578,7 +578,7 @@ private void ResumableUploadResumeTest_InitDelayTest()
578578
afterDelay: async () =>
579579
{
580580
// Verify that the URL no longer works.
581-
await Assert.ThrowsAsync<GoogleApiException>(() => TmpResumableUpload.GetSessionUriAsync(url));
581+
await Assert.ThrowsAsync<GoogleApiException>(() => SignedUrlResumableUpload.InitiateSessionAsync(url));
582582

583583
var obj = await _fixture.Client.ListObjectsAsync(bucket, name).FirstOrDefault(o => o.Name == name);
584584
Assert.Null(obj);
@@ -621,16 +621,16 @@ private void ResumableUploadWithCustomerSuppliedEncryptionKeysTest_InitDelayTest
621621
});
622622

623623
// Verify that the URL works initially.
624-
var uploader = TmpResumableUpload.CreateFromSignedUrl(
624+
var uploader = SignedUrlResumableUpload.Create(
625625
url,
626626
new MemoryStream(data),
627-
new ResumableUploadSessionOptions
627+
new ResumableUploadOptions
628628
{
629-
ModifySessionUriRequest = sessionUriRequest =>
629+
ModifySessionInitiationRequest = initiateRequest =>
630630
{
631-
sessionUriRequest.Headers.Add("x-goog-encryption-algorithm", "AES256");
632-
sessionUriRequest.Headers.Add("x-goog-encryption-key", key);
633-
sessionUriRequest.Headers.Add("x-goog-encryption-key-sha256", hash);
631+
initiateRequest.Headers.Add("x-goog-encryption-algorithm", "AES256");
632+
initiateRequest.Headers.Add("x-goog-encryption-key", key);
633+
initiateRequest.Headers.Add("x-goog-encryption-key-sha256", hash);
634634
}
635635
});
636636
var progress = await uploader.UploadAsync();
@@ -661,16 +661,16 @@ await Assert.ThrowsAsync<GoogleApiException>(
661661
},
662662
afterDelay: async () =>
663663
{
664-
var uploader = TmpResumableUpload.CreateFromSignedUrl(
664+
var uploader = SignedUrlResumableUpload.Create(
665665
url,
666666
new MemoryStream(data),
667-
new ResumableUploadSessionOptions
667+
new ResumableUploadOptions
668668
{
669-
ModifySessionUriRequest = sessionUriRequest =>
669+
ModifySessionInitiationRequest = initiateRequest =>
670670
{
671-
sessionUriRequest.Headers.Add("x-goog-encryption-algorithm", "AES256");
672-
sessionUriRequest.Headers.Add("x-goog-encryption-key", key);
673-
sessionUriRequest.Headers.Add("x-goog-encryption-key-sha256", hash);
671+
initiateRequest.Headers.Add("x-goog-encryption-algorithm", "AES256");
672+
initiateRequest.Headers.Add("x-goog-encryption-key", key);
673+
initiateRequest.Headers.Add("x-goog-encryption-key-sha256", hash);
674674
}
675675
});
676676

apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/RemoveMe/ResumableUpload.cs

Lines changed: 81 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -35,121 +35,116 @@ limitations under the License.
3535

3636
namespace Google.Apis.Upload
3737
{
38-
public sealed class ResumableUploadSessionOptions
38+
/// <summary>
39+
/// Options for <c>ResumableUpload</c> operations.
40+
/// </summary>
41+
public sealed class ResumableUploadOptions
3942
{
43+
/// <summary>
44+
/// Gets or sets the HTTP client to use when starting the upload sessions and uploading data.
45+
/// </summary>
4046
public HttpClient HttpClient { get; set; }
4147

4248
internal ConfigurableHttpClient ConfigurableHttpClient { get { return HttpClient as ConfigurableHttpClient; } }
4349

44-
public Action<HttpRequestMessage> ModifySessionUriRequest { get; set; }
50+
/// <summary>
51+
/// Gets or sets the callback for modifying the session initiation request.
52+
/// See https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload#start-resumable for more information.
53+
/// </summary>
54+
/// <remarks>
55+
/// Note: If these options are used with a <see cref="TmpResumableUpload"/> created using <see cref="TmpResumableUpload.CreateFromSessionUri"/>,
56+
/// this property will be ignored as the session has already been initiated.
57+
/// </remarks>
58+
public Action<HttpRequestMessage> ModifySessionInitiationRequest { get; set; }
4559

60+
/// <summary>
61+
/// Gets or sets the serializer to use when parsing error responses.
62+
/// </summary>
4663
public ISerializer Serializer { get; set; }
4764

65+
/// <summary>
66+
/// Gets or sets the name of the service performing the upload.
67+
/// </summary>
68+
/// <remarks>
69+
/// This will be used to set the <see cref="GoogleApiException.ServiceName"/> in the event of an error.
70+
/// </remarks>
4871
public string ServiceName { get; set; }
4972
}
5073

74+
/// <summary>
75+
/// Media upload which uses Google's resumable media upload protocol to upload data.
76+
/// </summary>
77+
/// <remarks>
78+
/// See: https://developers.google.com/drive/manage-uploads#resumable for more information on the protocol.
79+
/// </remarks>
5180
public class TmpResumableUpload
5281
{
53-
protected ConfigurableHttpClient HttpClient { get; }
54-
55-
private ResumableUploadSessionOptions Options { get; }
56-
57-
private string SignedUrl { get; set; }
82+
internal ConfigurableHttpClient HttpClient { get; }
5883

84+
/// <summary>
85+
/// Gets the options used to control the resumable upload.
86+
/// </summary>
87+
protected ResumableUploadOptions Options { get; }
5988

60-
protected TmpResumableUpload(Stream contentStream, ResumableUploadSessionOptions options)
89+
/// <summary>
90+
/// Creates a <see cref="TmpResumableUpload"/> instance.
91+
/// </summary>
92+
/// <param name="contentStream">The data to be uploaded.</param>
93+
/// <param name="options">The options for the upload operation.</param>
94+
protected TmpResumableUpload(Stream contentStream, ResumableUploadOptions options)
6195
{
96+
contentStream.ThrowIfNull(nameof(contentStream));
6297
ContentStream = contentStream;
6398
HttpClient = options?.ConfigurableHttpClient ?? new ConfigurableHttpClient(new ConfigurableMessageHandler(new HttpClientHandler()));
6499
Options = options;
65100
}
66101

67-
private TmpResumableUpload(string signedUrl, Stream contentStream, ResumableUploadSessionOptions options)
68-
: this(contentStream, options)
69-
{
70-
SignedUrl = signedUrl;
71-
}
72-
73-
private TmpResumableUpload(Uri sessionUri, Stream contentStream, ResumableUploadSessionOptions options)
102+
private TmpResumableUpload(Uri sessionUri, Stream contentStream, ResumableUploadOptions options)
74103
: this(contentStream, options)
75104
{
76105
UploadUri = sessionUri;
77106
}
78107

79108
/// <summary>
80-
/// Initializes the resumable upload by calling the resumable rest interface to get a unique upload location.
81-
/// See https://developers.google.com/drive/manage-uploads#start-resumable for more details.
109+
/// Initiates the resumable upload session and returns the session URI.
110+
/// See https://developers.google.com/drive/manage-uploads#start-resumable and
111+
/// https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload#start-resumable for more information.
82112
/// </summary>
113+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
83114
/// <returns>
84-
/// The unique upload location for this upload, returned in the Location header
115+
/// The task containing the session URI to use for the resumable upload.
85116
/// </returns>
86-
protected virtual async Task<Uri> GetSessionUriAsync(CancellationToken cancellationToken)
117+
protected virtual Task<Uri> InitiateSessionAsync(CancellationToken cancellationToken)
87118
{
88119
if (UploadUri != null)
89120
{
90-
return UploadUri;
91-
}
92-
93-
if (SignedUrl != null)
94-
{
95-
return await GetSessionUriAsync(SignedUrl, Options, cancellationToken);
121+
return Task.FromResult(UploadUri);
96122
}
97-
98-
throw new InvalidOperationException($"Could not obtain a session URI with the information in the {nameof(TmpResumableUpload)} instance.");
123+
124+
throw new InvalidOperationException($"Could not initiate the resumable upload session with the information in the {this.GetType().Name} instance.");
99125
}
100126

127+
/// <summary>
128+
/// Creates a <see cref="TmpResumableUpload"/> instance for a resumable upload session which has already been initiated.
129+
/// </summary>
130+
/// <remarks>
131+
/// See https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload#start-resumable for more information about initiating
132+
/// resumable upload sessions and saving the session URI.
133+
/// </remarks>
134+
/// <param name="sessionUri">The session URI of the resumable upload session.</param>
135+
/// <param name="contentStream">The data to be uploaded.</param>
136+
/// <param name="options">The options for the upload operation.</param>
137+
/// <returns>The instance which can be used to upload the specified content.</returns>
101138
public static TmpResumableUpload CreateFromSessionUri(
102139
Uri sessionUri,
103140
Stream contentStream,
104-
ResumableUploadSessionOptions options = null)
141+
ResumableUploadOptions options = null)
105142
{
106143
sessionUri.ThrowIfNull(nameof(sessionUri));
107144
return new TmpResumableUpload(sessionUri, contentStream, options);
108145
}
109-
110-
public static TmpResumableUpload CreateFromSignedUrl(
111-
string signedUrl,
112-
Stream contentStream,
113-
ResumableUploadSessionOptions options = null)
114-
{
115-
signedUrl.ThrowIfNull(nameof(signedUrl));
116-
return new TmpResumableUpload(signedUrl, contentStream, options);
117-
}
118-
119-
public static Uri GetSessionUri(string signedUrl, ResumableUploadSessionOptions options = null) =>
120-
GetResult(token => GetSessionUriAsync(signedUrl, options));
121-
122-
public static async Task<Uri> GetSessionUriAsync(
123-
string signedUrl,
124-
ResumableUploadSessionOptions options = null,
125-
CancellationToken cancellationToken = default(CancellationToken))
126-
{
127-
var httpClient = options?.HttpClient ?? new HttpClient();
128-
var request = new HttpRequestMessage(HttpMethod.Post, signedUrl);
129-
request.Headers.Add("x-goog-resumable", "start");
130-
options?.ModifySessionUriRequest?.Invoke(request);
131-
var result = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
132-
if (!result.IsSuccessStatusCode)
133-
{
134-
throw await MediaApiErrorHandling.ExceptionForResponseAsync(options?.Serializer, options?.ServiceName, result).ConfigureAwait(false);
135-
}
136-
return result.Headers.Location;
137-
}
138-
139-
private static T GetResult<T>(Func<CancellationToken, Task<T>> operation)
140-
{
141-
try
142-
{
143-
return operation(CancellationToken.None).Result;
144-
}
145-
catch (AggregateException e)
146-
{
147-
throw e.InnerExceptions.FirstOrDefault() ?? e;
148-
}
149-
}
150-
151-
152-
146+
147+
153148
/// <summary>The class logger.</summary>
154149
protected static ILogger Logger { get; } = ApplicationContext.Logger.ForType<TmpResumableUpload>();
155150

@@ -273,7 +268,7 @@ public async Task<IUploadProgress> UploadAsync(CancellationToken cancellationTok
273268

274269
try
275270
{
276-
UploadUri = await GetSessionUriAsync(cancellationToken).ConfigureAwait(false);
271+
UploadUri = await InitiateSessionAsync(cancellationToken).ConfigureAwait(false);
277272
if (ContentStream.CanSeek)
278273
{
279274
SendUploadSessionData(new ResumeableUploadSessionData(UploadUri));
@@ -376,6 +371,11 @@ protected async Task<bool> HandleResponse(HttpResponseMessage response)
376371
throw await ExceptionForResponseAsync(response).ConfigureAwait(false);
377372
}
378373

374+
/// <summary>
375+
/// Creates a <see cref="GoogleApiException"/> instance using the error response from the server.
376+
/// </summary>
377+
/// <param name="response">The error response.</param>
378+
/// <returns>An exception which can be thrown by the caller.</returns>
379379
protected Task<GoogleApiException> ExceptionForResponseAsync(HttpResponseMessage response)
380380
{
381381
return MediaApiErrorHandling.ExceptionForResponseAsync(Options?.Serializer, Options?.ServiceName, response);
@@ -662,7 +662,7 @@ public async Task<IUploadProgress> ResumeAsync(Uri uploadUri, CancellationToken
662662
/// were successfully uploaded before the error occurred.
663663
/// See https://developers.google.com/drive/manage-uploads#resume-upload for more details.
664664
/// </summary>
665-
protected sealed class ServerErrorCallback : IHttpUnsuccessfulResponseHandler, IHttpExceptionHandler, IDisposable
665+
private sealed class ServerErrorCallback : IHttpUnsuccessfulResponseHandler, IHttpExceptionHandler, IDisposable
666666
{
667667
private TmpResumableUpload Owner { get; set; }
668668

@@ -730,7 +730,7 @@ public void Dispose()
730730
#region Progress Monitoring
731731

732732
/// <summary>Class that communicates the progress of resumable uploads to a container.</summary>
733-
protected sealed class ResumableUploadProgress : IUploadProgress
733+
private sealed class ResumableUploadProgress : IUploadProgress
734734
{
735735
/// <summary>
736736
/// Create a ResumableUploadProgress instance.
@@ -764,12 +764,12 @@ public ResumableUploadProgress(Exception exception, long bytesSent)
764764
/// Current state of progress of the upload.
765765
/// </summary>
766766
/// <seealso cref="ProgressChanged"/>
767-
protected ResumableUploadProgress Progress { get; set; }
767+
private ResumableUploadProgress Progress { get; set; }
768768

769769
/// <summary>
770770
/// Updates the current progress and call the <see cref="ProgressChanged"/> event to notify listeners.
771771
/// </summary>
772-
protected void UpdateProgress(ResumableUploadProgress progress)
772+
private void UpdateProgress(ResumableUploadProgress progress)
773773
{
774774
Progress = progress;
775775
ProgressChanged?.Invoke(progress);
@@ -872,7 +872,7 @@ public abstract class TmpResumableUpload<TRequest> : TmpResumableUpload
872872
/// </remarks>
873873
protected TmpResumableUpload(IClientService service, string path, string httpMethod, Stream contentStream,
874874
string contentType)
875-
: base(contentStream, new ResumableUploadSessionOptions {
875+
: base(contentStream, new ResumableUploadOptions {
876876
HttpClient = service.HttpClient,
877877
Serializer = service.Serializer,
878878
ServiceName = service.Name
@@ -914,16 +914,11 @@ protected TmpResumableUpload(IClientService service, string path, string httpMet
914914

915915
#endregion // Properties
916916

917-
/// <summary>
918-
/// Initializes the resumable upload by calling the resumable rest interface to get a unique upload location.
919-
/// See https://developers.google.com/drive/manage-uploads#start-resumable for more details.
920-
/// </summary>
921-
/// <returns>
922-
/// The unique upload location for this upload, returned in the Location header
923-
/// </returns>
924-
protected override async Task<Uri> GetSessionUriAsync(CancellationToken cancellationToken)
917+
/// <inheritdoc/>
918+
protected override async Task<Uri> InitiateSessionAsync(CancellationToken cancellationToken)
925919
{
926920
HttpRequestMessage request = CreateInitializeRequest();
921+
Options?.ModifySessionInitiationRequest?.Invoke(request);
927922
var response = await Service.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
928923

929924
if (!response.IsSuccessStatusCode)

0 commit comments

Comments
 (0)