/*
Copyright 2012 Google Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Http;
using Google.Apis.Logging;
using Google.Apis.Media;
using Google.Apis.Requests;
using Google.Apis.Services;
using Google.Apis.Testing;
using Google.Apis.Util;
namespace Google.Apis.Upload
{
///
/// Media upload which uses Google's resumable media upload protocol to upload data.
///
///
/// See: https://developers.google.com/drive/manage-uploads#resumable for more information on the protocol.
///
public abstract class ResumableUpload
{
#region Constants
/// The class logger.
private static readonly ILogger Logger = ApplicationContext.Logger.ForType();
private const int KB = 0x400;
private const int MB = 0x100000;
/// Minimum chunk size (except the last one). Default value is 256*KB.
public const int MinimumChunkSize = 256 * KB;
/// Default chunk size. Default value is 10*MB.
public const int DefaultChunkSize = 10 * MB;
///
/// Defines how many bytes are read from the input stream in each stream read action.
/// The read will continue until we read or we reached the end of the stream.
///
internal int BufferSize = 4 * KB;
/// Indicates the stream's size is unknown.
private const int UnknownSize = -1;
/// Content-Range header value for the body upload of zero length files.
private const string ZeroByteContentRangeHeader = "bytes */0";
#endregion // Constants
#region Construction
///
/// Creates a instance.
///
/// The data to be uploaded. Must not be null.
/// The options for the upload operation. May be null.
protected ResumableUpload(Stream contentStream, ResumableUploadOptions options)
{
contentStream.ThrowIfNull(nameof(contentStream));
ContentStream = contentStream;
// Check if the stream length is known.
StreamLength = ContentStream.CanSeek ? ContentStream.Length : UnknownSize;
HttpClient = options?.ConfigurableHttpClient ?? new HttpClientFactory().CreateHttpClient(new CreateHttpClientArgs { ApplicationName = "ResumableUpload", GZipEnabled = true });
Options = options;
}
///
/// Creates a instance for a resumable upload session which has already been initiated.
///
///
/// See https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload#start-resumable for more information about initiating
/// resumable upload sessions and saving the session URI, or upload URI.
///
/// The session URI of the resumable upload session. Must not be null.
/// The data to be uploaded. Must not be null.
/// The options for the upload operation. May be null.
/// The instance which can be used to upload the specified content.
public static ResumableUpload CreateFromUploadUri(
Uri uploadUri,
Stream contentStream,
ResumableUploadOptions options = null)
{
uploadUri.ThrowIfNull(nameof(uploadUri));
return new InitiatedResumableUpload(uploadUri, contentStream, options);
}
private sealed class InitiatedResumableUpload : ResumableUpload
{
private Uri _initiatedUploadUri;
public InitiatedResumableUpload(Uri uploadUri, Stream contentStream, ResumableUploadOptions options)
: base(contentStream, options)
{
_initiatedUploadUri = uploadUri;
}
public override Task InitiateSessionAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(_initiatedUploadUri);
}
}
#endregion // Construction
#region Properties
///
/// Gets the options used to control the resumable upload.
///
protected ResumableUploadOptions Options { get; }
///
/// Gets the HTTP client to use to make requests.
///
internal ConfigurableHttpClient HttpClient { get; }
/// Gets or sets the stream to upload.
public Stream ContentStream { get; }
///
/// Gets or sets the length of the steam. Will be if the media content length is
/// unknown.
///
internal long StreamLength { get; set; }
///
/// Gets or sets the content of the last buffer request to the server or null. It is used when the media
/// content length is unknown, for resending it in case of server error.
/// Only used with a non-seekable stream.
///
private byte[] LastMediaRequest { get; set; }
///
/// Gets or sets the last request length.
/// Only used with a non-seekable stream.
///
private int LastMediaLength { get; set; }
///
/// Gets or sets the resumable session URI.
/// See https://developers.google.com/drive/manage-uploads#save-session-uri" for more details.
///
private Uri UploadUri { get; set; }
/// Gets or sets the amount of bytes the server had received so far.
private long BytesServerReceived { get; set; }
/// Gets or sets the amount of bytes the client had sent so far.
private long BytesClientSent { get; set; }
/// Change this value ONLY for testing purposes!
[VisibleForTestOnly]
protected int chunkSize = DefaultChunkSize;
///
/// Gets or sets the size of each chunk sent to the server.
/// Chunks (except the last chunk) must be a multiple of to be compatible with
/// Google upload servers.
///
public int ChunkSize
{
get { return chunkSize; }
set
{
if (value < MinimumChunkSize)
{
throw new ArgumentOutOfRangeException("ChunkSize");
}
chunkSize = value;
}
}
#endregion // Properties
#region Events
/// Event called whenever the progress of the upload changes.
public event Action ProgressChanged;
#endregion //Events
#region Error handling (Exception and 5xx)
///
/// Callback class that is invoked on abnormal response or an exception.
/// This class changes the request to query the current status of the upload in order to find how many bytes
/// were successfully uploaded before the error occurred.
/// See https://developers.google.com/drive/manage-uploads#resume-upload for more details.
///
class ServerErrorCallback : IHttpUnsuccessfulResponseHandler, IHttpExceptionHandler, IDisposable
{
private ResumableUpload Owner { get; set; }
///
/// Constructs a new callback and register it as unsuccessful response handler and exception handler on the
/// configurable message handler.
///
public ServerErrorCallback(ResumableUpload resumable)
{
this.Owner = resumable;
Owner.HttpClient.MessageHandler.AddUnsuccessfulResponseHandler(this);
Owner.HttpClient.MessageHandler.AddExceptionHandler(this);
}
public Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args)
{
var result = false;
var statusCode = (int)args.Response.StatusCode;
// Handle the error if and only if all the following conditions occur:
// - there is going to be an actual retry
// - the message request is for media upload with the current Uri (remember that the message handler
// can be invoked from other threads \ messages, so we should call server error callback only if the
// request is in the current context).
// - we got a 5xx server error.
if (args.SupportsRetry && args.Request.RequestUri.Equals(Owner.UploadUri) && statusCode / 100 == 5)
{
result = OnServerError(args.Request);
}
TaskCompletionSource tcs = new TaskCompletionSource();
tcs.SetResult(result);
return tcs.Task;
}
public Task HandleExceptionAsync(HandleExceptionArgs args)
{
var result = args.SupportsRetry && !args.CancellationToken.IsCancellationRequested &&
args.Request.RequestUri.Equals(Owner.UploadUri) ? OnServerError(args.Request) : false;
TaskCompletionSource tcs = new TaskCompletionSource();
tcs.SetResult(result);
return tcs.Task;
}
/// Changes the request in order to resume the interrupted upload.
private bool OnServerError(HttpRequestMessage request)
{
// Clear all headers and set Content-Range and Content-Length headers.
var range = String.Format("bytes */{0}", Owner.StreamLength < 0 ? "*" : Owner.StreamLength.ToString());
request.Headers.Clear();
request.Method = System.Net.Http.HttpMethod.Put;
request.SetEmptyContent().Headers.Add("Content-Range", range);
return true;
}
public void Dispose()
{
Owner.HttpClient.MessageHandler.RemoveUnsuccessfulResponseHandler(this);
Owner.HttpClient.MessageHandler.RemoveExceptionHandler(this);
}
}
#endregion
#region Progress Monitoring
/// Class that communicates the progress of resumable uploads to a container.
private class ResumableUploadProgress : IUploadProgress
{
///
/// Create a ResumableUploadProgress instance.
///
/// The status of the upload.
/// The number of bytes sent so far.
public ResumableUploadProgress(UploadStatus status, long bytesSent)
{
Status = status;
BytesSent = bytesSent;
}
///
/// Create a ResumableUploadProgress instance.
///
/// An exception that occurred during the upload.
/// The number of bytes sent before this exception occurred.
public ResumableUploadProgress(Exception exception, long bytesSent)
{
Status = UploadStatus.Failed;
BytesSent = bytesSent;
Exception = exception;
}
public UploadStatus Status { get; private set; }
public long BytesSent { get; private set; }
public Exception Exception { get; private set; }
}
///
/// Current state of progress of the upload.
///
///
private ResumableUploadProgress Progress { get; set; }
///
/// Updates the current progress and call the event to notify listeners.
///
private void UpdateProgress(ResumableUploadProgress progress)
{
Progress = progress;
ProgressChanged?.Invoke(progress);
}
///
/// Get the current progress state.
///
/// An IUploadProgress describing the current progress of the upload.
///
public IUploadProgress GetProgress()
{
return Progress;
}
#endregion
#region UploadSessionData
///
/// Event called when an UploadUri is created.
/// Not needed if the application program will not support resuming after a program restart.
///
///
/// Within the event, persist the UploadUri to storage.
/// It is strongly recommended that the full path filename (or other media identifier) is also stored so that it can be compared to the current open filename (media) upon restart.
///
public event Action UploadSessionData;
///
/// Data to be passed to the application program to allow resuming an upload after a program restart.
///
private class ResumeableUploadSessionData : IUploadSessionData
{
///
/// Create a ResumeableUploadSessionData instance to pass the UploadUri to the client.
///
/// The resumable session URI.
public ResumeableUploadSessionData(Uri uploadUri)
{
UploadUri = uploadUri;
}
public Uri UploadUri { get; private set; }
}
///
/// Send data (UploadUri) to application so it can store it to persistent storage.
///
private void SendUploadSessionData(ResumeableUploadSessionData sessionData)
{
UploadSessionData?.Invoke(sessionData);
}
#endregion
#region Upload Implementation
///
/// Uploads the content to the server. This method is synchronous and will block until the upload is completed.
///
///
/// In case the upload fails the will contain the exception that
/// cause the failure.
///
public IUploadProgress Upload()
{
return UploadAsync(CancellationToken.None).Result;
}
/// Uploads the content asynchronously to the server.
public Task UploadAsync()
{
return UploadAsync(CancellationToken.None);
}
/// Uploads the content to the server using the given cancellation token.
///
/// In case the upload fails will contain the exception that
/// cause the failure. The only exception which will be thrown is
/// which indicates that the task was canceled.
///
/// A cancellation token to cancel operation.
public async Task UploadAsync(CancellationToken cancellationToken)
{
BytesServerReceived = 0;
UpdateProgress(new ResumableUploadProgress(UploadStatus.Starting, 0));
try
{
UploadUri = await InitiateSessionAsync(cancellationToken).ConfigureAwait(false);
if (ContentStream.CanSeek)
{
SendUploadSessionData(new ResumeableUploadSessionData(UploadUri));
}
Logger.Debug("MediaUpload[{0}] - Start uploading...", UploadUri);
}
catch (Exception ex)
{
Logger.Error(ex, "MediaUpload - Exception occurred while initializing the upload");
UpdateProgress(new ResumableUploadProgress(ex, BytesServerReceived));
return Progress;
}
return await UploadCoreAsync(cancellationToken).ConfigureAwait(false);
}
///
/// Resumes the upload from the last point it was interrupted.
/// Use when resuming and the program was not restarted.
///
public IUploadProgress Resume()
{
return ResumeAsync(null, CancellationToken.None).Result;
}
///
/// Resumes the upload from the last point it was interrupted.
/// Use when the program was restarted and you wish to resume the upload that was in progress when the program was halted.
/// Implemented only for ContentStreams where .CanSeek is True.
///
///
/// In your application's UploadSessionData Event Handler, store UploadUri.AbsoluteUri property value (resumable session URI string value)
/// to persistent storage for use with Resume() or ResumeAsync() upon a program restart.
/// It is strongly recommended that the FullPathFilename of the media file that is being uploaded is saved also so that a subsequent execution of the
/// program can compare the saved FullPathFilename value to the FullPathFilename of the media file that it has opened for uploading.
/// You do not need to seek to restart point in the ContentStream file.
///
/// VideosResource.InsertMediaUpload UploadUri property value that was saved to persistent storage during a prior execution.
public IUploadProgress Resume(Uri uploadUri)
{
return ResumeAsync(uploadUri, CancellationToken.None).Result;
}
///
/// Asynchronously resumes the upload from the last point it was interrupted.
///
///
/// You do not need to seek to restart point in the ContentStream file.
///
public Task ResumeAsync()
{
return ResumeAsync(null, CancellationToken.None);
}
///
/// Asynchronously resumes the upload from the last point it was interrupted.
/// Use when resuming and the program was not restarted.
///
///
/// You do not need to seek to restart point in the ContentStream file.
///
/// A cancellation token to cancel the asynchronous operation.
public Task ResumeAsync(CancellationToken cancellationToken)
{
return ResumeAsync(null, cancellationToken);
}
///
/// Asynchronously resumes the upload from the last point it was interrupted.
/// Use when resuming and the program was restarted.
/// Implemented only for ContentStreams where .CanSeek is True.
///
///
/// In your application's UploadSessionData Event Handler, store UploadUri.AbsoluteUri property value (resumable session URI string value)
/// to persistent storage for use with Resume() or ResumeAsync() upon a program restart.
/// It is strongly recommended that the FullPathFilename of the media file that is being uploaded is saved also so that a subsequent execution of the
/// program can compare the saved FullPathFilename value to the FullPathFilename of the media file that it has opened for uploading.
/// You do not need to seek to restart point in the ContentStream file.
///
/// VideosResource.InsertMediaUpload UploadUri property value that was saved to persistent storage during a prior execution.
public Task ResumeAsync(Uri uploadUri)
{
return ResumeAsync(uploadUri, CancellationToken.None);
}
///
/// Asynchronously resumes the upload from the last point it was interrupted.
/// Use when the program was restarted and you wish to resume the upload that was in progress when the program was halted.
/// Implemented only for ContentStreams where .CanSeek is True.
///
///
/// In your application's UploadSessionData Event Handler, store UploadUri.AbsoluteUri property value (resumable session URI string value)
/// to persistent storage for use with Resume() or ResumeAsync() upon a program restart.
/// It is strongly recommended that the FullPathFilename of the media file that is being uploaded is saved also so that a subsequent execution of the
/// program can compare the saved FullPathFilename value to the FullPathFilename of the media file that it has opened for uploading.
/// You do not need to seek to restart point in the ContentStream file.
///
/// VideosResource.InsertMediaUpload UploadUri property value that was saved to persistent storage during a prior execution.
/// A cancellation token to cancel the asynchronous operation.
public async Task ResumeAsync(Uri uploadUri, CancellationToken cancellationToken)
{
// When called with uploadUri parameter of non-null value, the UploadUri is being
// provided upon a program restart to resume a previously interrupted upload.
if (uploadUri != null)
{
if (ContentStream.CanSeek)
{
Logger.Info("Resuming after program restart: UploadUri={0}", uploadUri);
UploadUri = uploadUri;
}
else
{
throw new NotImplementedException("Resume after program restart not allowed when ContentStream.CanSeek is false");
}
}
if (UploadUri == null)
{
Logger.Info("There isn't any upload in progress, so starting to upload again");
return await UploadAsync(cancellationToken).ConfigureAwait(false);
}
// The first "resuming" request is to query the server in which point the upload was interrupted.
var range = String.Format("bytes */{0}", StreamLength < 0 ? "*" : StreamLength.ToString());
HttpRequestMessage request = new RequestBuilder()
{
BaseUri = UploadUri,
Method = HttpConsts.Put
}.CreateRequest();
request.SetEmptyContent().Headers.Add("Content-Range", range);
try
{
HttpResponseMessage response;
using (var callback = new ServerErrorCallback(this))
{
response = await HttpClient.SendAsync(request, cancellationToken)
.ConfigureAwait(false);
}
if (await HandleResponse(response).ConfigureAwait(false))
{
// All the media was successfully upload.
UpdateProgress(new ResumableUploadProgress(UploadStatus.Completed, BytesServerReceived));
return Progress;
}
}
catch (TaskCanceledException ex)
{
Logger.Error(ex, "MediaUpload[{0}] - Task was canceled", UploadUri);
UpdateProgress(new ResumableUploadProgress(ex, BytesServerReceived));
throw ex;
}
catch (Exception ex)
{
Logger.Error(ex, "MediaUpload[{0}] - Exception occurred while resuming uploading media", UploadUri);
UpdateProgress(new ResumableUploadProgress(ex, BytesServerReceived));
return Progress;
}
// Continue to upload the media stream.
return await UploadCoreAsync(cancellationToken).ConfigureAwait(false);
}
/// The core logic for uploading a stream. It is used by the upload and resume methods.
private async Task UploadCoreAsync(CancellationToken cancellationToken)
{
try
{
using (var callback = new ServerErrorCallback(this))
{
while (!await SendNextChunkAsync(ContentStream, cancellationToken).ConfigureAwait(false))
{
UpdateProgress(new ResumableUploadProgress(UploadStatus.Uploading, BytesServerReceived));
}
UpdateProgress(new ResumableUploadProgress(UploadStatus.Completed, BytesServerReceived));
}
}
catch (TaskCanceledException ex)
{
Logger.Error(ex, "MediaUpload[{0}] - Task was canceled", UploadUri);
UpdateProgress(new ResumableUploadProgress(ex, BytesServerReceived));
throw ex;
}
catch (Exception ex)
{
Logger.Error(ex, "MediaUpload[{0}] - Exception occurred while uploading media", UploadUri);
UpdateProgress(new ResumableUploadProgress(ex, BytesServerReceived));
}
return Progress;
}
///
/// Initiates the resumable upload session and returns the session URI, or upload URI.
/// See https://developers.google.com/drive/manage-uploads#start-resumable and
/// https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload#start-resumable for more information.
///
/// The token to monitor for cancellation requests.
///
/// The task containing the session URI to use for the resumable upload.
///
public abstract Task InitiateSessionAsync(CancellationToken cancellationToken = default(CancellationToken));
///
/// Process a response from the final upload chunk call.
///
/// The response body from the final uploaded chunk.
protected virtual void ProcessResponse(HttpResponseMessage httpResponse)
{
}
/// Uploads the next chunk of data to the server.
/// True if the entire media has been completely uploaded.
protected async Task SendNextChunkAsync(Stream stream, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
HttpRequestMessage request = new RequestBuilder()
{
BaseUri = UploadUri,
Method = HttpConsts.Put
}.CreateRequest();
// Prepare next chunk to send.
int contentLength = ContentStream.CanSeek
? PrepareNextChunkKnownSize(request, stream, cancellationToken)
: PrepareNextChunkUnknownSize(request, stream, cancellationToken);
BytesClientSent = BytesServerReceived + contentLength;
Logger.Debug("MediaUpload[{0}] - Sending bytes={1}-{2}", UploadUri, BytesServerReceived,
BytesClientSent - 1);
HttpResponseMessage response = await HttpClient.SendAsync(request, cancellationToken)
.ConfigureAwait(false);
return await HandleResponse(response).ConfigureAwait(false);
}
/// Handles a media upload HTTP response.
/// True if the entire media has been completely uploaded.
private async Task HandleResponse(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
MediaCompleted(response);
return true;
}
else if (response.StatusCode == (HttpStatusCode)308)
{
// The upload protocol uses 308 to indicate that there is more data expected from the server.
// If the server has received no bytes, it indicates this by not including
// a Range header in the response..
var range = response.Headers.FirstOrDefault(x => x.Key == "Range").Value?.First();
BytesServerReceived = GetNextByte(range);
Logger.Debug("MediaUpload[{0}] - {1} Bytes were sent successfully", UploadUri, BytesServerReceived);
return false;
}
throw await ExceptionForResponseAsync(response).ConfigureAwait(false);
}
///
/// Creates a instance using the error response from the server.
///
/// The error response.
/// An exception which can be thrown by the caller.
protected Task ExceptionForResponseAsync(HttpResponseMessage response)
{
return MediaApiErrorHandling.ExceptionForResponseAsync(Options?.Serializer, Options?.ServiceName, response);
}
/// A callback when the media was uploaded successfully.
private void MediaCompleted(HttpResponseMessage response)
{
Logger.Debug("MediaUpload[{0}] - media was uploaded successfully", UploadUri);
ProcessResponse(response);
BytesServerReceived = StreamLength;
// Clear the last request byte array.
LastMediaRequest = null;
}
/// Prepares the given request with the next chunk in case the steam length is unknown.
private int PrepareNextChunkUnknownSize(HttpRequestMessage request, Stream stream,
CancellationToken cancellationToken)
{
if (LastMediaRequest == null)
{
// Initialise state
// ChunkSize + 1 to give room for one extra byte for end-of-stream checking
LastMediaRequest = new byte[ChunkSize + 1];
LastMediaLength = 0;
}
// Re-use any bytes the server hasn't received
int copyCount = (int)(BytesClientSent - BytesServerReceived)
+ Math.Max(0, LastMediaLength - ChunkSize);
if (LastMediaLength != copyCount)
{
Buffer.BlockCopy(LastMediaRequest, LastMediaLength - copyCount, LastMediaRequest, 0, copyCount);
LastMediaLength = copyCount;
}
// Read any more required bytes from stream, to form the next chunk
while (LastMediaLength < ChunkSize + 1 && StreamLength == UnknownSize)
{
cancellationToken.ThrowIfCancellationRequested();
int readSize = Math.Min(BufferSize, ChunkSize + 1 - LastMediaLength);
int len = stream.Read(LastMediaRequest, LastMediaLength, readSize);
LastMediaLength += len;
if (len == 0)
{
// Stream ended, so we know the length
StreamLength = BytesServerReceived + LastMediaLength;
}
}
// Set Content-Length and Content-Range.
int contentLength = Math.Min(ChunkSize, LastMediaLength);
var byteArrayContent = new ByteArrayContent(LastMediaRequest, 0, contentLength);
byteArrayContent.Headers.Add("Content-Range", GetContentRangeHeader(BytesServerReceived, contentLength));
request.Content = byteArrayContent;
return contentLength;
}
/// Prepares the given request with the next chunk in case the steam length is known.
private int PrepareNextChunkKnownSize(HttpRequestMessage request, Stream stream,
CancellationToken cancellationToken)
{
int chunkSize = (int)Math.Min(StreamLength - BytesServerReceived, (long)ChunkSize);
// Stream length is known and it supports seek and position operations.
// We can change the stream position and read bytes from the last point.
byte[] buffer = new byte[Math.Min(chunkSize, BufferSize)];
// If the number of bytes received by the server isn't equal to the amount of bytes the client sent, we
// need to change the position of the input stream, otherwise we can continue from the current position.
if (stream.Position != BytesServerReceived)
{
stream.Position = BytesServerReceived;
}
MemoryStream ms = new MemoryStream(chunkSize);
int bytesRead = 0;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
// Read from input stream and write to output stream.
// TODO(peleyal): write a utility similar to (.NET 4 Stream.CopyTo method).
int len = stream.Read(buffer, 0, (int)Math.Min(buffer.Length, chunkSize - bytesRead));
if (len == 0) break;
ms.Write(buffer, 0, len);
bytesRead += len;
}
// Set the stream position to beginning and wrap it with stream content.
ms.Position = 0;
request.Content = new StreamContent(ms);
request.Content.Headers.Add("Content-Range", GetContentRangeHeader(BytesServerReceived, chunkSize));
return chunkSize;
}
/// Returns the next byte index need to be sent.
private long GetNextByte(string range)
{
return range == null ? 0 : long.Parse(range.Substring(range.IndexOf('-') + 1)) + 1;
}
///
/// Build a content range header of the form: "bytes X-Y/T" where:
///
/// - X is the first byte being sent.
/// - Y is the last byte in the range being sent (inclusive).
/// - T is the total number of bytes in the range or * for unknown size.
///
///
///
/// See: RFC2616 HTTP/1.1, Section 14.16 Header Field Definitions, Content-Range
/// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
///
/// Start of the chunk.
/// Size of the chunk being sent.
/// The content range header value.
private string GetContentRangeHeader(long chunkStart, long chunkSize)
{
string strLength = StreamLength < 0 ? "*" : StreamLength.ToString();
// If a file of length 0 is sent, one chunk needs to be sent with 0 size.
// This chunk cannot be specified with the standard (inclusive) range header.
// In this case, use * to indicate no bytes sent in the Content-Range header.
if (chunkStart == 0 && chunkSize == 0 && StreamLength == 0)
{
return ZeroByteContentRangeHeader;
}
else
{
long chunkEnd = chunkStart + chunkSize - 1;
return String.Format("bytes {0}-{1}/{2}", chunkStart, chunkEnd, strLength);
}
}
#endregion Upload Implementation
}
///
/// Media upload which uses Google's resumable media upload protocol to upload data.
///
///
/// See: https://developers.google.com/drive/manage-uploads#resumable for more information on the protocol.
///
///
/// The type of the body of this request. Generally this should be the metadata related to the content to be
/// uploaded. Must be serializable to/from JSON.
///
public class ResumableUpload : ResumableUpload
{
#region Constants
/// Payload description headers, describing the content itself.
private const string PayloadContentTypeHeader = "X-Upload-Content-Type";
/// Payload description headers, describing the content itself.
private const string PayloadContentLengthHeader = "X-Upload-Content-Length";
/// Specify the type of this upload (this class supports resumable only).
private const string UploadType = "uploadType";
/// The uploadType parameter value for resumable uploads.
private const string Resumable = "resumable";
#endregion // Constants
#region Construction
///
/// Create a resumable upload instance with the required parameters.
///
/// The client service.
/// The path for this media upload method.
/// The HTTP method to start this upload.
/// The stream containing the content to upload.
/// Content type of the content to be uploaded. Some services
/// may allow this to be null; others require a content type to be specified and will
/// fail when the upload is started if the value is null.
///
/// Caller is responsible for maintaining the open until the upload is
/// completed.
/// Caller is responsible for closing the .
///
protected ResumableUpload(IClientService service, string path, string httpMethod, Stream contentStream, string contentType)
: base(contentStream,
new ResumableUploadOptions
{
HttpClient = service.HttpClient,
Serializer = service.Serializer,
ServiceName = service.Name
})
{
service.ThrowIfNull(nameof(service));
path.ThrowIfNull(nameof(path));
httpMethod.ThrowIfNullOrEmpty(nameof(httpMethod));
contentStream.ThrowIfNull(nameof(contentStream));
this.Service = service;
this.Path = path;
this.HttpMethod = httpMethod;
this.ContentType = contentType;
}
#endregion // Construction
#region Properties
/// Gets or sets the service.
public IClientService Service { get; private set; }
///
/// Gets or sets the path of the method (combined with
/// ) to produce
/// absolute Uri.
///
public string Path { get; private set; }
/// Gets or sets the HTTP method of this upload (used to initialize the upload).
public string HttpMethod { get; private set; }
/// Gets or sets the stream's Content-Type.
public string ContentType { get; private set; }
/// Gets or sets the body of this request.
public TRequest Body { get; set; }
#endregion // Properties
#region Upload Implementation
///
public override async Task InitiateSessionAsync(CancellationToken cancellationToken = default(CancellationToken))
{
HttpRequestMessage request = CreateInitializeRequest();
Options?.ModifySessionInitiationRequest?.Invoke(request);
var response = await Service.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
throw await ExceptionForResponseAsync(response).ConfigureAwait(false);
}
return response.Headers.Location;
}
/// Creates a request to initialize a request.
private HttpRequestMessage CreateInitializeRequest()
{
var builder = new RequestBuilder()
{
BaseUri = new Uri(Service.BaseUri),
Path = Path,
Method = HttpMethod,
};
// init parameters
builder.AddParameter(RequestParameterType.Query, "key", Service.ApiKey);
builder.AddParameter(RequestParameterType.Query, UploadType, Resumable);
SetAllPropertyValues(builder);
HttpRequestMessage request = builder.CreateRequest();
if (ContentType != null)
{
request.Headers.Add(PayloadContentTypeHeader, ContentType);
}
// if the length is unknown at the time of this request, omit "X-Upload-Content-Length" header
if (ContentStream.CanSeek)
{
request.Headers.Add(PayloadContentLengthHeader, StreamLength.ToString());
}
Service.SetRequestSerailizedContent(request, Body);
return request;
}
///
/// Reflectively enumerate the properties of this object looking for all properties containing the
/// RequestParameterAttribute and copy their values into the request builder.
///
private void SetAllPropertyValues(RequestBuilder requestBuilder)
{
Type myType = this.GetType();
var properties = myType.GetProperties();
foreach (var property in properties)
{
var attribute = Utilities.GetCustomAttribute(property);
if (attribute != null)
{
string name = attribute.Name ?? property.Name.ToLower();
object value = property.GetValue(this, null);
if (value != null)
{
var valueAsEnumerable = value as IEnumerable;
if (!(value is string) && valueAsEnumerable != null)
{
foreach (var elem in valueAsEnumerable)
{
requestBuilder.AddParameter(attribute.Type, name, Utilities.ConvertToString(elem));
}
}
else
{
// Otherwise just convert it to a string.
requestBuilder.AddParameter(attribute.Type, name, Utilities.ConvertToString(value));
}
}
}
}
}
#endregion Upload Implementation
}
///
/// Media upload which uses Google's resumable media upload protocol to upload data.
/// The version with two types contains both a request object and a response object.
///
///
/// See: https://developers.google.com/gdata/docs/resumable_upload for
/// information on the protocol.
///
///
/// The type of the body of this request. Generally this should be the metadata related
/// to the content to be uploaded. Must be serializable to/from JSON.
///
///
/// The type of the response body.
///
public class ResumableUpload : ResumableUpload
{
#region Construction
///
/// Create a resumable upload instance with the required parameters.
///
/// The client service.
/// The path for this media upload method.
/// The HTTP method to start this upload.
/// The stream containing the content to upload.
/// Content type of the content to be uploaded.
///
/// The stream must support the "Length" property.
/// Caller is responsible for maintaining the open until the
/// upload is completed.
/// Caller is responsible for closing the .
///
protected ResumableUpload(IClientService service, string path, string httpMethod,
Stream contentStream, string contentType)
: base(service, path, httpMethod, contentStream, contentType) { }
#endregion // Construction
#region Properties
///
/// The response body.
///
///
/// This property will be set during upload. The event
/// is triggered when this has been set.
///
public TResponse ResponseBody { get; private set; }
#endregion // Properties
#region Events
/// Event which is called when the response metadata is processed.
public event Action ResponseReceived;
#endregion // Events
#region Overrides
/// Process the response body
protected override void ProcessResponse(HttpResponseMessage response)
{
base.ProcessResponse(response);
ResponseBody = Service.DeserializeResponse(response).Result;
ResponseReceived?.Invoke(ResponseBody);
}
#endregion // Overrides
}
}