Google Api
This commit is contained in:
parent
e307811d89
commit
83f9fc1bd8
123 changed files with 12961 additions and 60 deletions
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Copyright 2013 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;
|
||||
|
||||
namespace Google.Apis.Download
|
||||
{
|
||||
/// <summary>Enum which represents the status of the current download.</summary>
|
||||
public enum DownloadStatus
|
||||
{
|
||||
/// <summary>The download has not started.</summary>
|
||||
NotStarted,
|
||||
|
||||
/// <summary>Data is being downloaded.</summary>
|
||||
Downloading,
|
||||
|
||||
/// <summary>The download was completed successfully.</summary>
|
||||
Completed,
|
||||
|
||||
/// <summary>The download failed.</summary>
|
||||
Failed
|
||||
};
|
||||
|
||||
/// <summary>Reports download progress.</summary>
|
||||
public interface IDownloadProgress
|
||||
{
|
||||
/// <summary>Gets the current status of the upload.</summary>
|
||||
DownloadStatus Status { get; }
|
||||
|
||||
/// <summary>Gets the number of bytes received from the server.</summary>
|
||||
long BytesDownloaded { get; }
|
||||
|
||||
/// <summary>Gets an exception if one occurred.</summary>
|
||||
Exception Exception { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright 2013 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Download
|
||||
{
|
||||
/// <summary>Media download which uses download file part by part, by <see cref="ChunkSize"/>.</summary>
|
||||
public interface IMediaDownloader
|
||||
{
|
||||
/// <summary>An event which notifies when the download status has been changed.</summary>
|
||||
event Action<IDownloadProgress> ProgressChanged;
|
||||
|
||||
/// <summary>Gets or sets the chunk size to download, it defines the size of each part.</summary>
|
||||
int ChunkSize { get; set; }
|
||||
|
||||
/// <summary>Downloads synchronously the given URL to the given stream.</summary>
|
||||
IDownloadProgress Download(string url, Stream stream);
|
||||
|
||||
/// <summary>Downloads asynchronously the given URL to the given stream.</summary>
|
||||
Task<IDownloadProgress> DownloadAsync(string url, Stream stream);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads asynchronously the given URL to the given stream. This download method supports a cancellation
|
||||
/// token to cancel a request before it was completed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In case the download fails <see cref="IDownloadProgress.Exception "/> will contain the exception that
|
||||
/// cause the failure. The only exception which will be thrown is
|
||||
/// <see cref="System.Threading.Tasks.TaskCanceledException"/> which indicates that the task was canceled.
|
||||
/// </remarks>
|
||||
Task<IDownloadProgress> DownloadAsync(string url, Stream stream, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
367
Yavsc/GoogleApiSupport/Google.Apis/Download/MediaDownloader.cs
Normal file
367
Yavsc/GoogleApiSupport/Google.Apis/Download/MediaDownloader.cs
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
/*
|
||||
Copyright 2013 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.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Media;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Util;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Google.Apis.Download
|
||||
{
|
||||
/// <summary>
|
||||
/// A media downloader implementation which handles media downloads.
|
||||
/// </summary>
|
||||
public class MediaDownloader : IMediaDownloader
|
||||
{
|
||||
static MediaDownloader()
|
||||
{
|
||||
UriPatcher.PatchUriQuirks();
|
||||
}
|
||||
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<MediaDownloader>();
|
||||
|
||||
/// <summary>The service which this downloader belongs to.</summary>
|
||||
private readonly IClientService service;
|
||||
|
||||
private const int MB = 0x100000;
|
||||
|
||||
/// <summary>Maximum chunk size. Default value is 10*MB.</summary>
|
||||
public const int MaximumChunkSize = 10 * MB;
|
||||
|
||||
private int chunkSize = MaximumChunkSize;
|
||||
/// <summary>
|
||||
/// Gets or sets the amount of data that will be downloaded before notifying the caller of
|
||||
/// the download's progress.
|
||||
/// Must not exceed <see cref="MaximumChunkSize"/>.
|
||||
/// Default value is <see cref="MaximumChunkSize"/>.
|
||||
/// </summary>
|
||||
public int ChunkSize
|
||||
{
|
||||
get { return chunkSize; }
|
||||
set
|
||||
{
|
||||
if (value > MaximumChunkSize)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("ChunkSize");
|
||||
}
|
||||
chunkSize = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The range header for the request, if any. This can be used to download specific parts
|
||||
/// of the requested media.
|
||||
/// </summary>
|
||||
public RangeHeaderValue Range { get; set; }
|
||||
|
||||
#region Progress
|
||||
|
||||
/// <summary>
|
||||
/// Download progress model, which contains the status of the download, the amount of bytes whose where
|
||||
/// downloaded so far, and an exception in case an error had occurred.
|
||||
/// </summary>
|
||||
private class DownloadProgress : IDownloadProgress
|
||||
{
|
||||
/// <summary>Constructs a new progress instance.</summary>
|
||||
/// <param name="status">The status of the download.</param>
|
||||
/// <param name="bytes">The number of bytes received so far.</param>
|
||||
public DownloadProgress(DownloadStatus status, long bytes)
|
||||
{
|
||||
Status = status;
|
||||
BytesDownloaded = bytes;
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new progress instance.</summary>
|
||||
/// <param name="exception">An exception which occurred during the download.</param>
|
||||
/// <param name="bytes">The number of bytes received before the exception occurred.</param>
|
||||
public DownloadProgress(Exception exception, long bytes)
|
||||
{
|
||||
Status = DownloadStatus.Failed;
|
||||
BytesDownloaded = bytes;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the status of the download.</summary>
|
||||
public DownloadStatus Status { get; private set; }
|
||||
|
||||
/// <summary>Gets or sets the amount of bytes that have been downloaded so far.</summary>
|
||||
public long BytesDownloaded { get; private set; }
|
||||
|
||||
/// <summary>Gets or sets the exception which occurred during the download or <c>null</c>.</summary>
|
||||
public Exception Exception { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the current progress and call the <see cref="ProgressChanged"/> event to notify listeners.
|
||||
/// </summary>
|
||||
private void UpdateProgress(IDownloadProgress progress)
|
||||
{
|
||||
ProgressChanged?.Invoke(progress);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Constructs a new downloader with the given client service.</summary>
|
||||
public MediaDownloader(IClientService service)
|
||||
{
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback for modifying requests made when downloading.
|
||||
/// </summary>
|
||||
public Action<HttpRequestMessage> ModifyRequest { get; set; }
|
||||
|
||||
#region IMediaDownloader Overrides
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<IDownloadProgress> ProgressChanged;
|
||||
|
||||
#region Download (sync and async)
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDownloadProgress Download(string url, Stream stream)
|
||||
{
|
||||
return DownloadCoreAsync(url, stream, CancellationToken.None).Result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IDownloadProgress> DownloadAsync(string url, Stream stream)
|
||||
{
|
||||
return await DownloadAsync(url, stream, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IDownloadProgress> DownloadAsync(string url, Stream stream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await DownloadCoreAsync(url, stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// CountedBuffer bundles together a byte buffer and a count of valid bytes.
|
||||
/// </summary>
|
||||
private class CountedBuffer
|
||||
{
|
||||
public byte[] Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How many bytes at the beginning of Data are valid.
|
||||
/// </summary>
|
||||
public int Count { get; private set; }
|
||||
|
||||
public CountedBuffer(int size)
|
||||
{
|
||||
Data = new byte[size];
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the buffer contains no data.
|
||||
/// </summary>
|
||||
public bool IsEmpty { get { return Count == 0; } }
|
||||
|
||||
/// <summary>
|
||||
/// Read data from stream until the stream is empty or the buffer is full.
|
||||
/// </summary>
|
||||
/// <param name="stream">Stream from which to read.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
public async Task Fill(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
// ReadAsync may return if it has *any* data available, so we loop.
|
||||
while (Count < Data.Length)
|
||||
{
|
||||
int read = await stream.ReadAsync(Data, Count, Data.Length - Count, cancellationToken).ConfigureAwait(false);
|
||||
if (read == 0) { break; }
|
||||
Count += read;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the first n bytes of the buffer. Move any remaining valid bytes to the beginning.
|
||||
/// Trying to remove more bytes than the buffer contains just clears the buffer.
|
||||
/// </summary>
|
||||
/// <param name="n">The number of bytes to remove.</param>
|
||||
public void RemoveFromFront(int n)
|
||||
{
|
||||
if (n >= Count)
|
||||
{
|
||||
Count = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Some valid data remains.
|
||||
Array.Copy(Data, n, Data, 0, Count - n);
|
||||
Count -= n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The core download logic. We download the media and write it to an output stream
|
||||
/// ChunkSize bytes at a time, raising the ProgressChanged event after each chunk.
|
||||
///
|
||||
/// The chunking behavior is largely a historical artifact: a previous implementation
|
||||
/// issued multiple web requests, each for ChunkSize bytes. Now we do everything in
|
||||
/// one request, but the API and client-visible behavior are retained for compatibility.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the resource to download.</param>
|
||||
/// <param name="stream">The download will download the resource into this stream.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to cancel this download in the middle.</param>
|
||||
/// <returns>A task with the download progress object. If an exception occurred during the download, its
|
||||
/// <see cref="IDownloadProgress.Exception "/> property will contain the exception.</returns>
|
||||
private async Task<IDownloadProgress> DownloadCoreAsync(string url, Stream stream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
url.ThrowIfNull("url");
|
||||
stream.ThrowIfNull("stream");
|
||||
if (!stream.CanWrite)
|
||||
{
|
||||
throw new ArgumentException("stream doesn't support write operations");
|
||||
}
|
||||
|
||||
// Add alt=media to the query parameters.
|
||||
var uri = new UriBuilder(url);
|
||||
if (uri.Query == null || uri.Query.Length <= 1)
|
||||
{
|
||||
uri.Query = "alt=media";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove the leading '?'. UriBuilder.Query doesn't round-trip.
|
||||
uri.Query = uri.Query.Substring(1) + "&alt=media";
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, uri.ToString());
|
||||
request.Headers.Range = Range;
|
||||
ModifyRequest?.Invoke(request);
|
||||
|
||||
// Number of bytes sent to the caller's stream.
|
||||
long bytesReturned = 0;
|
||||
|
||||
try
|
||||
{
|
||||
// Signal SendAsync to return as soon as the response headers are read.
|
||||
// We'll stream the content ourselves as it becomes available.
|
||||
var completionOption = HttpCompletionOption.ResponseHeadersRead;
|
||||
|
||||
using (var response = await service.HttpClient.SendAsync(request, completionOption, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw await MediaApiErrorHandling.ExceptionForResponseAsync(service, response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
OnResponseReceived(response);
|
||||
|
||||
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
// We send ChunkSize bytes at a time to the caller, but we keep ChunkSize + 1 bytes
|
||||
// buffered. That way we can tell when we've reached the end of the response, even if the
|
||||
// response length is evenly divisible by ChunkSize, and we can avoid sending a Downloading
|
||||
// event followed by a Completed event with no bytes downloaded in between.
|
||||
//
|
||||
// This maintains the client-visible behavior of a previous implementation.
|
||||
var buffer = new CountedBuffer(ChunkSize + 1);
|
||||
|
||||
while (true)
|
||||
{
|
||||
await buffer.Fill(responseStream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Send one chunk to the caller's stream.
|
||||
int bytesToReturn = Math.Min(ChunkSize, buffer.Count);
|
||||
OnDataReceived(buffer.Data, bytesToReturn);
|
||||
await stream.WriteAsync(buffer.Data, 0, bytesToReturn, cancellationToken).ConfigureAwait(false);
|
||||
bytesReturned += bytesToReturn;
|
||||
|
||||
buffer.RemoveFromFront(ChunkSize);
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
// We had <= ChunkSize bytes buffered, so we've read and returned the entire response.
|
||||
// Skip sending a Downloading event. We'll send Completed instead.
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProgress(new DownloadProgress(DownloadStatus.Downloading, bytesReturned));
|
||||
}
|
||||
}
|
||||
OnDownloadCompleted();
|
||||
|
||||
var finalProgress = new DownloadProgress(DownloadStatus.Completed, bytesReturned);
|
||||
UpdateProgress(finalProgress);
|
||||
return finalProgress;
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
Logger.Error(ex, "Download media was canceled");
|
||||
UpdateProgress(new DownloadProgress(ex, bytesReturned));
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Exception occurred while downloading media");
|
||||
var progress = new DownloadProgress(ex, bytesReturned);
|
||||
UpdateProgress(progress);
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a successful HTTP response is received, allowing subclasses to examine headers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For unsuccessful responses, an appropriate exception is thrown immediately, without this method
|
||||
/// being called.
|
||||
/// </remarks>
|
||||
/// <param name="response">HTTP response received.</param>
|
||||
protected virtual void OnResponseReceived(HttpResponseMessage response)
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an HTTP response is received, allowing subclasses to examine data before it's
|
||||
/// written to the client stream.
|
||||
/// </summary>
|
||||
/// <param name="data">Byte array containing the data downloaded.</param>
|
||||
/// <param name="length">Length of data downloaded in this chunk, in bytes.</param>
|
||||
protected virtual void OnDataReceived(byte[] data, int length)
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a download has completed, allowing subclasses to perform any final validation
|
||||
/// or transformation.
|
||||
/// </summary>
|
||||
protected virtual void OnDownloadCompleted()
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Yavsc/GoogleApiSupport/Google.Apis/ETagAction.cs
Normal file
46
Yavsc/GoogleApiSupport/Google.Apis/ETagAction.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2011 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.
|
||||
*/
|
||||
|
||||
namespace Google.Apis
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the behaviour/header used for sending an etag along with a request.
|
||||
/// </summary>
|
||||
public enum ETagAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The default etag behaviour will be determined by the type of the request.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// The ETag won't be added to the header of the request.
|
||||
/// </summary>
|
||||
Ignore,
|
||||
|
||||
/// <summary>
|
||||
/// The ETag will be added as an "If-Match" header.
|
||||
/// A request sent with an "If-Match" header will only succeed if both ETags are identical.
|
||||
/// </summary>
|
||||
IfMatch,
|
||||
|
||||
/// <summary>
|
||||
/// The ETag will be added as an "If-None-Match" header.
|
||||
/// A request sent with an "If-Match" header will only succeed if both ETags are not identical.
|
||||
/// </summary>
|
||||
IfNoneMatch,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright 2015 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.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Google.Apis.Json;
|
||||
using Google.Apis.Requests;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Util;
|
||||
|
||||
namespace Google.Apis.Media
|
||||
{
|
||||
/// <summary>
|
||||
/// Common error handling code for the Media API.
|
||||
/// </summary>
|
||||
internal static class MediaApiErrorHandling
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a suitable exception for an HTTP response, attempting to parse the body as
|
||||
/// JSON but falling back to just using the text as the message.
|
||||
/// </summary>
|
||||
internal static Task<GoogleApiException> ExceptionForResponseAsync(
|
||||
IClientService service,
|
||||
HttpResponseMessage response)
|
||||
{
|
||||
return ExceptionForResponseAsync(service.Serializer, service.Name, response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a suitable exception for an HTTP response, attempting to parse the body as
|
||||
/// JSON but falling back to just using the text as the message.
|
||||
/// </summary>
|
||||
internal static async Task<GoogleApiException> ExceptionForResponseAsync(
|
||||
ISerializer serializer,
|
||||
string name,
|
||||
HttpResponseMessage response)
|
||||
{
|
||||
// If we can't even read the response, let that exception bubble up, just as it would have done
|
||||
// if the error had been occurred when sending the request.
|
||||
string responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
RequestError parsedError = null;
|
||||
string message = responseText;
|
||||
try
|
||||
{
|
||||
var parsedResponse = (serializer ?? NewtonsoftJsonSerializer.Instance).Deserialize<StandardResponse<object>>(responseText);
|
||||
if (parsedResponse != null && parsedResponse.Error != null)
|
||||
{
|
||||
parsedError = parsedResponse.Error;
|
||||
message = parsedError.ToString();
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Just make do with a null RequestError, and the message set to the body of the response.
|
||||
// The contents of the caught exception aren't particularly useful - we don't need to include it
|
||||
// as a cause, for example. The expectation is that the exception returned by this method (below)
|
||||
// will be thrown by the caller.
|
||||
}
|
||||
return new GoogleApiException(name ?? "", message)
|
||||
{
|
||||
Error = parsedError,
|
||||
HttpStatusCode = response.StatusCode
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
383
Yavsc/GoogleApiSupport/Google.Apis/Requests/BatchRequest.cs
Normal file
383
Yavsc/GoogleApiSupport/Google.Apis/Requests/BatchRequest.cs
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
/*
|
||||
Copyright 2013 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.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Testing;
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// A batch request which represents individual requests to Google servers. You should add a single service
|
||||
/// request using the <see cref="Queue"/> method and execute all individual requests using
|
||||
/// <see cref="ExecuteAsync()"/>. More information about the batch protocol is available in
|
||||
/// https://developers.google.com/storage/docs/json_api/v1/how-tos/batch.
|
||||
/// <remarks>
|
||||
/// Current implementation doesn't retry on unsuccessful individual response and doesn't support requests with
|
||||
/// different access tokens (different users or scopes).
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public sealed class BatchRequest
|
||||
{
|
||||
private const string DefaultBatchUrl = "https://www.googleapis.com/batch";
|
||||
private const int QueueLimit = 1000;
|
||||
|
||||
private readonly IList<InnerRequest> allRequests = new List<InnerRequest>();
|
||||
|
||||
private readonly string batchUrl;
|
||||
private readonly IClientService service;
|
||||
|
||||
// For testing
|
||||
internal string BatchUrl => batchUrl;
|
||||
|
||||
/// <summary>A concrete type callback for an individual response.</summary>
|
||||
/// <typeparam name="TResponse">The response type.</typeparam>
|
||||
/// <param name="content">The content response or <c>null</c> if the request failed.</param>
|
||||
/// <param name="error">Error or <c>null</c> if the request succeeded.</param>
|
||||
/// <param name="index">The request index.</param>
|
||||
/// <param name="message">The HTTP individual response.</param>
|
||||
public delegate void OnResponse<in TResponse>
|
||||
(TResponse content, RequestError error, int index, HttpResponseMessage message) where TResponse : class;
|
||||
|
||||
#region Inner Request
|
||||
|
||||
/// <summary>This inner class represents an individual inner request.</summary>
|
||||
private class InnerRequest
|
||||
{
|
||||
/// <summary>Gets or sets the client service request.</summary>
|
||||
public IClientServiceRequest ClientRequest { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the response class type.</summary>
|
||||
public Type ResponseType { get; set; }
|
||||
|
||||
/// <summary>A callback method which will be called after an individual response was parsed.</summary>
|
||||
/// <param name="content">The content response or <c>null</c> if the request failed.</param>
|
||||
/// <param name="error">Error or <c>null</c> if the request succeeded.</param>
|
||||
/// <param name="index">The request index.</param>
|
||||
/// <param name="message">The HTTP individual response.</param>
|
||||
public virtual void OnResponse(object content, RequestError error, int index, HttpResponseMessage message)
|
||||
{
|
||||
// Set ETag on the response.
|
||||
var eTagValue = message.Headers.ETag != null ? message.Headers.ETag.Tag : null;
|
||||
var eTagContainer = content as IDirectResponseSchema;
|
||||
if (eTagContainer != null && eTagContainer.ETag == null && eTagValue != null)
|
||||
{
|
||||
eTagContainer.ETag = eTagValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This generic inner class represents an individual inner request with a generic response type.
|
||||
/// </summary>
|
||||
private class InnerRequest<TResponse> : InnerRequest
|
||||
where TResponse : class
|
||||
{
|
||||
/// <summary>Gets or sets a concrete type callback for an individual response. </summary>
|
||||
public OnResponse<TResponse> OnResponseCallback { get; set; }
|
||||
|
||||
public override void OnResponse(object content, RequestError error, int index,
|
||||
HttpResponseMessage message)
|
||||
{
|
||||
base.OnResponse(content, error, index, message);
|
||||
if (OnResponseCallback == null)
|
||||
return;
|
||||
|
||||
OnResponseCallback(content as TResponse, error, index, message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new batch request using the given service. See
|
||||
/// <see cref="BatchRequest(IClientService, string)"/> for more information.
|
||||
/// </summary>
|
||||
public BatchRequest(IClientService service)
|
||||
: this(service, (service as BaseClientService)?.BatchUri ?? DefaultBatchUrl) { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new batch request using the given service. The service's HTTP client is used to create a
|
||||
/// request to the given server URL and its serializer members are used to serialize the request and
|
||||
/// deserialize the response.
|
||||
/// </summary>
|
||||
public BatchRequest(IClientService service, string batchUrl)
|
||||
{
|
||||
this.batchUrl = batchUrl;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/// <summary>Gets the count of all queued requests.</summary>
|
||||
public int Count
|
||||
{
|
||||
get { return allRequests.Count; }
|
||||
}
|
||||
|
||||
/// <summary>Queues an individual request.</summary>
|
||||
/// <typeparam name="TResponse">The response's type.</typeparam>
|
||||
/// <param name="request">The individual request.</param>
|
||||
/// <param name="callback">A callback which will be called after a response was parsed.</param>
|
||||
public void Queue<TResponse>(IClientServiceRequest request, OnResponse<TResponse> callback)
|
||||
where TResponse : class
|
||||
{
|
||||
if (Count > QueueLimit)
|
||||
{
|
||||
throw new InvalidOperationException("A batch request cannot contain more than 1000 single requests");
|
||||
}
|
||||
|
||||
allRequests.Add(new InnerRequest<TResponse>
|
||||
{
|
||||
ClientRequest = request,
|
||||
ResponseType = typeof(TResponse),
|
||||
OnResponseCallback = callback,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Asynchronously executes the batch request.</summary>
|
||||
public Task ExecuteAsync()
|
||||
{
|
||||
return ExecuteAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>Asynchronously executes the batch request.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel operation.</param>
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Count < 1)
|
||||
return;
|
||||
|
||||
ConfigurableHttpClient httpClient = service.HttpClient;
|
||||
|
||||
var requests = from r in allRequests
|
||||
select r.ClientRequest;
|
||||
HttpContent outerContent = await CreateOuterRequestContent(requests).ConfigureAwait(false);
|
||||
var result = await httpClient.PostAsync(new Uri(batchUrl), outerContent, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
result.EnsureSuccessStatusCode();
|
||||
|
||||
// Get the boundary separator.
|
||||
const string boundaryKey = "boundary=";
|
||||
var fullContent = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var contentType = result.Content.Headers.GetValues("Content-Type").First();
|
||||
var boundary = contentType.Substring(contentType.IndexOf(boundaryKey) + boundaryKey.Length);
|
||||
|
||||
int requestIndex = 0;
|
||||
// While there is still content to read, parse the current HTTP response.
|
||||
while (true)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var startIndex = fullContent.IndexOf("--" + boundary);
|
||||
if (startIndex == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
fullContent = fullContent.Substring(startIndex + boundary.Length + 2);
|
||||
var endIndex = fullContent.IndexOf("--" + boundary);
|
||||
if (endIndex == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
HttpResponseMessage responseMessage = ParseAsHttpResponse(fullContent.Substring(0, endIndex));
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
// Parse the current content object.
|
||||
var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var content = service.Serializer.Deserialize(responseContent,
|
||||
allRequests[requestIndex].ResponseType);
|
||||
|
||||
allRequests[requestIndex].OnResponse(content, null, requestIndex, responseMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse the error from the current response.
|
||||
var error = await service.DeserializeError(responseMessage).ConfigureAwait(false);
|
||||
allRequests[requestIndex].OnResponse(null, error, requestIndex, responseMessage);
|
||||
}
|
||||
|
||||
requestIndex++;
|
||||
fullContent = fullContent.Substring(endIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Parses the given string content to a HTTP response.</summary>
|
||||
[VisibleForTestOnly]
|
||||
internal static HttpResponseMessage ParseAsHttpResponse(string content)
|
||||
{
|
||||
var response = new HttpResponseMessage();
|
||||
|
||||
using (var reader = new StringReader(content))
|
||||
{
|
||||
string line = reader.ReadLine();
|
||||
|
||||
// Extract empty lines.
|
||||
while (string.IsNullOrEmpty(line))
|
||||
line = reader.ReadLine();
|
||||
|
||||
// Extract the outer header.
|
||||
while (!string.IsNullOrEmpty(line))
|
||||
line = reader.ReadLine();
|
||||
|
||||
// Extract the status code.
|
||||
line = reader.ReadLine();
|
||||
while (string.IsNullOrEmpty(line))
|
||||
line = reader.ReadLine();
|
||||
int code = int.Parse(line.Split(' ')[1]);
|
||||
response.StatusCode = (HttpStatusCode)code;
|
||||
|
||||
// Extract the headers.
|
||||
IDictionary<string, string> headersDic = new Dictionary<string, string>();
|
||||
while (!string.IsNullOrEmpty((line = reader.ReadLine())))
|
||||
{
|
||||
var separatorIndex = line.IndexOf(':');
|
||||
var key = line.Substring(0, separatorIndex).Trim();
|
||||
var value = line.Substring(separatorIndex + 1).Trim();
|
||||
// Check if the header already exists, and if so append its value
|
||||
// to the existing value. Fixes issue #548.
|
||||
if (headersDic.ContainsKey(key)) {
|
||||
headersDic[key] = headersDic[key] + ", " + value;
|
||||
} else {
|
||||
headersDic.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the content.
|
||||
string mediaType = null;
|
||||
if (headersDic.ContainsKey("Content-Type"))
|
||||
{
|
||||
mediaType = headersDic["Content-Type"].Split(';', ' ')[0];
|
||||
headersDic.Remove("Content-Type");
|
||||
}
|
||||
response.Content = new StringContent(reader.ReadToEnd(), Encoding.UTF8, mediaType);
|
||||
|
||||
// Add the headers to the response.
|
||||
foreach (var keyValue in headersDic)
|
||||
{
|
||||
HttpHeaders headers = response.Headers;
|
||||
// Check if we need to add the current header to the content headers.
|
||||
if (typeof(HttpContentHeaders).GetProperty(keyValue.Key.Replace("-", "")) != null)
|
||||
{
|
||||
headers = response.Content.Headers;
|
||||
}
|
||||
|
||||
// Use TryAddWithoutValidation rather than Add because Mono's validation is
|
||||
// improperly strict. https://bugzilla.xamarin.com/show_bug.cgi?id=39569
|
||||
if (!headers.TryAddWithoutValidation(keyValue.Key, keyValue.Value))
|
||||
{
|
||||
throw new FormatException(String.Format(
|
||||
"Could not parse header {0} from batch reply", keyValue.Key));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(peleyal): ContentLength header is x while the "real" content that we read from the stream is
|
||||
// Content.ReadStringAsAsync().Length is x+2
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the batch outer request content which includes all the individual requests to Google servers.
|
||||
/// </summary>
|
||||
[VisibleForTestOnly]
|
||||
internal async static Task<HttpContent> CreateOuterRequestContent(IEnumerable<IClientServiceRequest> requests)
|
||||
{
|
||||
var mixedContent = new MultipartContent("mixed");
|
||||
foreach (var request in requests)
|
||||
{
|
||||
mixedContent.Add(await CreateIndividualRequest(request).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
// Batch request currently doesn't support GZip. Uncomment when the issue will be resolved.
|
||||
// https://code.google.com/p/google-api-dotnet-client/issues/detail?id=409
|
||||
/*if (service.GZipEnabled)
|
||||
{
|
||||
var content = HttpServiceExtenstions.CreateZipContent(await mixedContent.ReadAsStringAsync()
|
||||
.ConfigureAwait(false));
|
||||
content.Headers.ContentType = mixedContent.Headers.ContentType;
|
||||
return content;
|
||||
}*/
|
||||
return mixedContent;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Creates the individual server request.</summary>
|
||||
[VisibleForTestOnly]
|
||||
internal static async Task<HttpContent> CreateIndividualRequest(IClientServiceRequest request)
|
||||
{
|
||||
HttpRequestMessage requestMessage = request.CreateRequest(false);
|
||||
string requestContent = await CreateRequestContentString(requestMessage).ConfigureAwait(false);
|
||||
|
||||
var content = new StringContent(requestContent);
|
||||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/http");
|
||||
return content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a string representation that includes the request's headers and content based on the input HTTP
|
||||
/// request message.
|
||||
/// </summary>
|
||||
[VisibleForTestOnly]
|
||||
internal static async Task<string> CreateRequestContentString(HttpRequestMessage requestMessage)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendFormat("{0} {1}", requestMessage.Method, requestMessage.RequestUri.AbsoluteUri);
|
||||
|
||||
// Add Headers.
|
||||
foreach (var otherHeader in requestMessage.Headers)
|
||||
{
|
||||
sb.Append(Environment.NewLine)
|
||||
.AppendFormat(("{0}: {1}"), otherHeader.Key, String.Join(", ", otherHeader.Value.ToArray()));
|
||||
}
|
||||
|
||||
// Add content headers.
|
||||
if (requestMessage.Content != null)
|
||||
{
|
||||
foreach (var contentHeader in requestMessage.Content.Headers)
|
||||
{
|
||||
sb.Append(Environment.NewLine)
|
||||
.AppendFormat("{0}: {1}", contentHeader.Key, String.Join(", ", contentHeader.Value.ToArray()));
|
||||
}
|
||||
}
|
||||
|
||||
// Content.
|
||||
if (requestMessage.Content != null)
|
||||
{
|
||||
sb.Append(Environment.NewLine);
|
||||
var content = await requestMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
sb.Append("Content-Length: ").Append(content.Length);
|
||||
|
||||
sb.Append(Environment.NewLine).Append(Environment.NewLine).Append(content);
|
||||
}
|
||||
|
||||
return sb.Append(Environment.NewLine).ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,366 @@
|
|||
/*
|
||||
Copyright 2011 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.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Discovery;
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Testing;
|
||||
using Google.Apis.Util;
|
||||
using Google.Apis.Requests.Parameters;
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an abstract, strongly typed request base class to make requests to a service.
|
||||
/// Supports a strongly typed response.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">The type of the response object</typeparam>
|
||||
public abstract class ClientServiceRequest<TResponse> : IClientServiceRequest<TResponse>
|
||||
{
|
||||
/// <summary>The class logger.</summary>
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<ClientServiceRequest<TResponse>>();
|
||||
|
||||
/// <summary>The service on which this request will be executed.</summary>
|
||||
private readonly IClientService service;
|
||||
|
||||
/// <summary>Defines whether the E-Tag will be used in a specified way or be ignored.</summary>
|
||||
public ETagAction ETagAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback for modifying HTTP requests made by this service request.
|
||||
/// </summary>
|
||||
public Action<HttpRequestMessage> ModifyRequest { get; set; }
|
||||
|
||||
#region IClientServiceRequest Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract string MethodName { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract string RestPath { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract string HttpMethod { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDictionary<string, IParameter> RequestParameters { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IClientService Service
|
||||
{
|
||||
get { return service; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Creates a new service request.</summary>
|
||||
protected ClientServiceRequest(IClientService service)
|
||||
{
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes request's parameters. Inherited classes MUST override this method to add parameters to the
|
||||
/// <see cref="RequestParameters"/> dictionary.
|
||||
/// </summary>
|
||||
protected virtual void InitParameters()
|
||||
{
|
||||
RequestParameters = new Dictionary<string, IParameter>();
|
||||
}
|
||||
|
||||
#region Execution
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TResponse Execute()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var response = ExecuteUnparsedAsync(CancellationToken.None).Result)
|
||||
{
|
||||
return ParseResponse(response).Result;
|
||||
}
|
||||
}
|
||||
catch (AggregateException aex)
|
||||
{
|
||||
// If an exception was thrown during the tasks, unwrap and throw it.
|
||||
throw aex.InnerException;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Stream ExecuteAsStream()
|
||||
{
|
||||
// TODO(peleyal): should we copy the stream, and dispose the response?
|
||||
try
|
||||
{
|
||||
// Sync call.
|
||||
var response = ExecuteUnparsedAsync(CancellationToken.None).Result;
|
||||
return response.Content.ReadAsStreamAsync().Result;
|
||||
}
|
||||
catch (AggregateException aex)
|
||||
{
|
||||
// If an exception was thrown during the tasks, unwrap and throw it.
|
||||
throw aex.InnerException;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<TResponse> ExecuteAsync()
|
||||
{
|
||||
return await ExecuteAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<TResponse> ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using (var response = await ExecuteUnparsedAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await ParseResponse(response).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Stream> ExecuteAsStreamAsync()
|
||||
{
|
||||
return await ExecuteAsStreamAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Stream> ExecuteAsStreamAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO(peleyal): should we copy the stream, and dispose the response?
|
||||
var response = await ExecuteUnparsedAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>Sync executes the request without parsing the result. </summary>
|
||||
private async Task<HttpResponseMessage> ExecuteUnparsedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using (var request = CreateRequest())
|
||||
{
|
||||
return await service.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Parses the response and deserialize the content into the requested response object. </summary>
|
||||
private async Task<TResponse> ParseResponse(HttpResponseMessage response)
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await service.DeserializeResponse<TResponse>(response).ConfigureAwait(false);
|
||||
}
|
||||
var error = await service.DeserializeError(response).ConfigureAwait(false);
|
||||
throw new GoogleApiException(service.Name, error.ToString())
|
||||
{
|
||||
Error = error,
|
||||
HttpStatusCode = response.StatusCode
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc/>
|
||||
public HttpRequestMessage CreateRequest(Nullable<bool> overrideGZipEnabled = null)
|
||||
{
|
||||
var builder = CreateBuilder();
|
||||
var request = builder.CreateRequest();
|
||||
object body = GetBody();
|
||||
request.SetRequestSerailizedContent(service, body, overrideGZipEnabled.HasValue
|
||||
? overrideGZipEnabled.Value : service.GZipEnabled);
|
||||
AddETag(request);
|
||||
ModifyRequest?.Invoke(request);
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="Google.Apis.Requests.RequestBuilder"/> which is used to generate a request.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A new builder instance which contains the HTTP method and the right Uri with its path and query parameters.
|
||||
/// </returns>
|
||||
private RequestBuilder CreateBuilder()
|
||||
{
|
||||
var builder = new RequestBuilder()
|
||||
{
|
||||
BaseUri = new Uri(Service.BaseUri),
|
||||
Path = RestPath,
|
||||
Method = HttpMethod,
|
||||
};
|
||||
|
||||
// Init parameters.
|
||||
if (service.ApiKey != null)
|
||||
{
|
||||
builder.AddParameter(RequestParameterType.Query, "key", service.ApiKey);
|
||||
}
|
||||
var parameters = ParameterUtils.CreateParameterDictionary(this);
|
||||
AddParameters(builder, ParameterCollection.FromDictionary(parameters));
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>Generates the right URL for this request.</summary>
|
||||
protected string GenerateRequestUri()
|
||||
{
|
||||
return CreateBuilder().BuildUri().ToString();
|
||||
}
|
||||
|
||||
/// <summary>Returns the body of this request.</summary>
|
||||
/// <returns>The body of this request.</returns>
|
||||
protected virtual object GetBody()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#region ETag
|
||||
|
||||
/// <summary>
|
||||
/// Adds the right ETag action (e.g. If-Match) header to the given HTTP request if the body contains ETag.
|
||||
/// </summary>
|
||||
private void AddETag(HttpRequestMessage request)
|
||||
{
|
||||
IDirectResponseSchema body = GetBody() as IDirectResponseSchema;
|
||||
if (body != null && !string.IsNullOrEmpty(body.ETag))
|
||||
{
|
||||
var etag = body.ETag;
|
||||
ETagAction action = ETagAction == ETagAction.Default ? GetDefaultETagAction(HttpMethod) : ETagAction;
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case ETagAction.IfMatch:
|
||||
request.Headers.IfMatch.Add(new EntityTagHeaderValue(etag));
|
||||
break;
|
||||
case ETagAction.IfNoneMatch:
|
||||
request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(etag));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// When ETag is invalid we are going to create a request anyway.
|
||||
// See https://code.google.com/p/google-api-dotnet-client/issues/detail?id=464 for more details.
|
||||
catch (FormatException ex)
|
||||
{
|
||||
Logger.Error(ex, "Can't set {0}. Etag is: {1}.", action, etag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the default ETagAction for a specific HTTP verb.</summary>
|
||||
[VisibleForTestOnly]
|
||||
public static ETagAction GetDefaultETagAction(string httpMethod)
|
||||
{
|
||||
switch (httpMethod)
|
||||
{
|
||||
// Incoming data should only be updated if it has been changed on the server.
|
||||
case HttpConsts.Get:
|
||||
return ETagAction.IfNoneMatch;
|
||||
|
||||
// Outgoing data should only be committed if it hasn't been changed on the server.
|
||||
case HttpConsts.Put:
|
||||
case HttpConsts.Post:
|
||||
case HttpConsts.Patch:
|
||||
case HttpConsts.Delete:
|
||||
return ETagAction.IfMatch;
|
||||
|
||||
default:
|
||||
return ETagAction.Ignore;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parameters
|
||||
|
||||
/// <summary>Adds path and query parameters to the given <c>requestBuilder</c>.</summary>
|
||||
private void AddParameters(RequestBuilder requestBuilder, ParameterCollection inputParameters)
|
||||
{
|
||||
foreach (var parameter in inputParameters)
|
||||
{
|
||||
IParameter parameterDefinition;
|
||||
|
||||
if (!RequestParameters.TryGetValue(parameter.Key, out parameterDefinition))
|
||||
{
|
||||
throw new GoogleApiException(Service.Name,
|
||||
String.Format("Invalid parameter \"{0}\" was specified", parameter.Key));
|
||||
}
|
||||
|
||||
string value = parameter.Value;
|
||||
if (!ParameterValidator.ValidateParameter(parameterDefinition, value))
|
||||
{
|
||||
throw new GoogleApiException(Service.Name,
|
||||
string.Format("Parameter validation failed for \"{0}\"", parameterDefinition.Name));
|
||||
}
|
||||
|
||||
if (value == null) // If the parameter is null, use the default value.
|
||||
{
|
||||
value = parameterDefinition.DefaultValue;
|
||||
}
|
||||
|
||||
switch (parameterDefinition.ParameterType)
|
||||
{
|
||||
case "path":
|
||||
requestBuilder.AddParameter(RequestParameterType.Path, parameter.Key, value);
|
||||
break;
|
||||
case "query":
|
||||
// If the parameter is optional and no value is given, don't add to url.
|
||||
if (!Object.Equals(value, parameterDefinition.DefaultValue) || parameterDefinition.IsRequired)
|
||||
{
|
||||
requestBuilder.AddParameter(RequestParameterType.Query, parameter.Key, value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new GoogleApiException(service.Name,
|
||||
string.Format("Unsupported parameter type \"{0}\" for \"{1}\"",
|
||||
parameterDefinition.ParameterType, parameterDefinition.Name));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is a required parameter which wasn't set.
|
||||
foreach (var parameter in RequestParameters.Values)
|
||||
{
|
||||
if (parameter.IsRequired && !inputParameters.ContainsKey(parameter.Name))
|
||||
{
|
||||
throw new GoogleApiException(service.Name,
|
||||
string.Format("Parameter \"{0}\" is missing", parameter.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
Copyright 2013 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.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
using System.IO.Compression;
|
||||
|
||||
using Google.Apis.Services;
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>Extension methods to <see cref="System.Net.Http.HttpRequestMessage"/>.</summary>
|
||||
static class HttpRequestMessageExtenstions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the content of the request by the given body and the the required GZip configuration.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="service">The service.</param>
|
||||
/// <param name="body">The body of the future request. If <c>null</c> do nothing.</param>
|
||||
/// <param name="gzipEnabled">
|
||||
/// Indicates if the content will be wrapped in a GZip stream, or a regular string stream will be used.
|
||||
/// </param>
|
||||
internal static void SetRequestSerailizedContent(this HttpRequestMessage request,
|
||||
IClientService service, object body, bool gzipEnabled)
|
||||
{
|
||||
if (body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
HttpContent content = null;
|
||||
|
||||
var mediaType = "application/" + service.Serializer.Format;
|
||||
var serializedObject = service.SerializeObject(body);
|
||||
if (gzipEnabled)
|
||||
{
|
||||
content = CreateZipContent(serializedObject);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue(mediaType)
|
||||
{
|
||||
CharSet = Encoding.UTF8.WebName
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
content = new StringContent(serializedObject, Encoding.UTF8, mediaType);
|
||||
}
|
||||
|
||||
request.Content = content;
|
||||
}
|
||||
|
||||
/// <summary>Creates a GZip content based on the given content.</summary>
|
||||
/// <param name="content">Content to GZip.</param>
|
||||
/// <returns>GZiped HTTP content.</returns>
|
||||
internal static HttpContent CreateZipContent(string content)
|
||||
{
|
||||
var stream = CreateGZipStream(content);
|
||||
var sc = new StreamContent(stream);
|
||||
sc.Headers.ContentEncoding.Add("gzip");
|
||||
return sc;
|
||||
}
|
||||
|
||||
/// <summary>Creates a GZip stream by the given serialized object.</summary>
|
||||
private static Stream CreateGZipStream(string serializedObject)
|
||||
{
|
||||
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(serializedObject);
|
||||
using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
|
||||
{
|
||||
using (GZipStream gzip = new GZipStream(ms, CompressionMode.Compress, true))
|
||||
{
|
||||
gzip.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
// Reset the stream to the beginning. It doesn't work otherwise!
|
||||
ms.Position = 0;
|
||||
byte[] compressed = new byte[ms.Length];
|
||||
ms.Read(compressed, 0, compressed.Length);
|
||||
return new MemoryStream(compressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
Copyright 2011 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.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Discovery;
|
||||
using Google.Apis.Services;
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>A client service request which supports both sync and async execution to get the stream.</summary>
|
||||
public interface IClientServiceRequest
|
||||
{
|
||||
/// <summary>Gets the name of the method to which this request belongs.</summary>
|
||||
string MethodName { get; }
|
||||
|
||||
/// <summary>Gets the rest path of this request.</summary>
|
||||
string RestPath { get; }
|
||||
|
||||
/// <summary>Gets the HTTP method of this request.</summary>
|
||||
string HttpMethod { get; }
|
||||
|
||||
/// <summary>Gets the parameters information for this specific request.</summary>
|
||||
IDictionary<string, IParameter> RequestParameters { get; }
|
||||
|
||||
/// <summary>Gets the service which is related to this request.</summary>
|
||||
IClientService Service { get; }
|
||||
|
||||
/// <summary>Creates a HTTP request message with all path and query parameters, ETag, etc.</summary>
|
||||
/// <param name="overrideGZipEnabled">
|
||||
/// If <c>null</c> use the service default GZip behavior. Otherwise indicates if GZip is enabled or disabled.
|
||||
/// </param>
|
||||
HttpRequestMessage CreateRequest(Nullable<bool> overrideGZipEnabled = null);
|
||||
|
||||
/// <summary>Executes the request asynchronously and returns the result stream.</summary>
|
||||
Task<Stream> ExecuteAsStreamAsync();
|
||||
|
||||
/// <summary>Executes the request asynchronously and returns the result stream.</summary>
|
||||
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
|
||||
Task<Stream> ExecuteAsStreamAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Executes the request and returns the result stream.</summary>
|
||||
Stream ExecuteAsStream();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A client service request which inherits from <see cref="IClientServiceRequest"/> and represents a specific
|
||||
/// service request with the given response type. It supports both sync and async execution to get the response.
|
||||
/// </summary>
|
||||
public interface IClientServiceRequest<TResponse> : IClientServiceRequest
|
||||
{
|
||||
/// <summary>Executes the request asynchronously and returns the result object.</summary>
|
||||
Task<TResponse> ExecuteAsync();
|
||||
|
||||
/// <summary>Executes the request asynchronously and returns the result object.</summary>
|
||||
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
|
||||
Task<TResponse> ExecuteAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Executes the request and returns the result object.</summary>
|
||||
TResponse Execute();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2011 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.
|
||||
*/
|
||||
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface containing additional response-properties which will be added to every schema type which is
|
||||
/// a direct response to a request.
|
||||
/// </summary>
|
||||
public interface IDirectResponseSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// The e-tag of this response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will be set by the service deserialization method,
|
||||
/// or the by json response parser if implemented on service.
|
||||
/// </remarks>
|
||||
string ETag { get; set; }
|
||||
}
|
||||
}
|
||||
159
Yavsc/GoogleApiSupport/Google.Apis/Requests/PageStreamer.cs
Normal file
159
Yavsc/GoogleApiSupport/Google.Apis/Requests/PageStreamer.cs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
Copyright 2016 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.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Requests
|
||||
{
|
||||
// TODO(jskeet): Make sure one of our samples uses this.
|
||||
|
||||
/// <summary>
|
||||
/// A page streamer is a helper to provide both synchronous and asynchronous page streaming
|
||||
/// of a listable or queryable resource.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The expected usage pattern is to create a single paginator for a resource collection,
|
||||
/// and then use the instance methods to obtain paginated results.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// To construct a page streamer to return snippets from the YouTube v3 Data API, you might use code
|
||||
/// such as the following. The pattern for other APIs would be very similar, with the <c>request.PageToken</c>,
|
||||
/// <c>response.NextPageToken</c> and <c>response.Items</c> properties potentially having different names. Constructing
|
||||
/// the page streamer doesn't require any service references or authentication, so it's completely safe to perform this
|
||||
/// in a type initializer.
|
||||
/// <code><![CDATA[
|
||||
/// using Google.Apis.YouTube.v3;
|
||||
/// using Google.Apis.YouTube.v3.Data;
|
||||
/// ...
|
||||
/// private static readonly snippetPageStreamer = new PageStreamer<SearchResult, SearchResource.ListRequest, SearchListResponse, string>(
|
||||
/// (request, token) => request.PageToken = token,
|
||||
/// response => response.NextPageToken,
|
||||
/// response => response.Items);
|
||||
/// ]]></code>
|
||||
/// </example>
|
||||
/// <typeparam name="TResource">The type of resource being paginated</typeparam>
|
||||
/// <typeparam name="TRequest">The type of request used to fetch pages</typeparam>
|
||||
/// <typeparam name="TResponse">The type of response obtained when fetching pages</typeparam>
|
||||
/// <typeparam name="TToken">The type of the "next page token", which must be a reference type;
|
||||
/// a null reference for a token indicates the end of a stream of pages.</typeparam>
|
||||
public sealed class PageStreamer<TResource, TRequest, TResponse, TToken>
|
||||
where TToken : class
|
||||
where TRequest : IClientServiceRequest<TResponse>
|
||||
{
|
||||
// Simple way of avoiding NullReferenceException if the response extractor returns null.
|
||||
private static readonly TResource[] emptyResources = new TResource[0];
|
||||
|
||||
private readonly Action<TRequest, TToken> requestModifier;
|
||||
private readonly Func<TResponse, TToken> tokenExtractor;
|
||||
private readonly Func<TResponse, IEnumerable<TResource>> resourceExtractor;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a paginator for later use.
|
||||
/// </summary>
|
||||
/// <param name="requestModifier">Action to modify a request to include the specified page token.
|
||||
/// Must not be null.</param>
|
||||
/// <param name="tokenExtractor">Function to extract the next page token from a response.
|
||||
/// Must not be null.</param>
|
||||
/// <param name="resourceExtractor">Function to extract a sequence of resources from a response.
|
||||
/// Must not be null, although it can return null if it is passed a response which contains no
|
||||
/// resources.</param>
|
||||
public PageStreamer(
|
||||
Action<TRequest, TToken> requestModifier,
|
||||
Func<TResponse, TToken> tokenExtractor,
|
||||
Func<TResponse, IEnumerable<TResource>> resourceExtractor)
|
||||
{
|
||||
if (requestModifier == null)
|
||||
{
|
||||
throw new ArgumentNullException("requestProvider");
|
||||
}
|
||||
if (tokenExtractor == null)
|
||||
{
|
||||
throw new ArgumentNullException("tokenExtractor");
|
||||
}
|
||||
if (resourceExtractor == null)
|
||||
{
|
||||
throw new ArgumentNullException("resourceExtractor");
|
||||
}
|
||||
this.requestModifier = requestModifier;
|
||||
this.tokenExtractor = tokenExtractor;
|
||||
this.resourceExtractor = resourceExtractor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lazily fetches resources a page at a time.
|
||||
/// </summary>
|
||||
/// <param name="request">The initial request to send. If this contains a page token,
|
||||
/// that token is maintained. This will be modified with new page tokens over time, and should not
|
||||
/// be changed by the caller. (The caller should clone the request if they want an independent object
|
||||
/// to use in other calls or to modify.) Must not be null.</param>
|
||||
/// <returns>A sequence of resources, which are fetched a page at a time. Must not be null.</returns>
|
||||
public IEnumerable<TResource> Fetch(TRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ArgumentNullException("request");
|
||||
}
|
||||
TToken token;
|
||||
do
|
||||
{
|
||||
TResponse response = request.Execute();
|
||||
token = tokenExtractor(response);
|
||||
requestModifier(request, token);
|
||||
foreach (var item in resourceExtractor(response) ?? emptyResources)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
} while (token != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously (but eagerly) fetches a complete set of resources, potentially making multiple requests.
|
||||
/// </summary>
|
||||
/// <param name="request">The initial request to send. If this contains a page token,
|
||||
/// that token is maintained. This will be modified with new page tokens over time, and should not
|
||||
/// be changed by the caller. (The caller should clone the request if they want an independent object
|
||||
/// to use in other calls or to modify.) Must not be null.</param>
|
||||
/// <returns>A sequence of resources, which are fetched asynchronously and a page at a time.</returns>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>A task whose result (when complete) is the complete set of results fetched starting with the given
|
||||
/// request, and continuing to make further requests until a response has no "next page" token.</returns>
|
||||
public async Task<IList<TResource>> FetchAllAsync(
|
||||
TRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ArgumentNullException("request");
|
||||
}
|
||||
var results = new List<TResource>();
|
||||
TToken token;
|
||||
do
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
TResponse response = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
token = tokenExtractor(response);
|
||||
requestModifier(request, token);
|
||||
results.AddRange(resourceExtractor(response) ?? emptyResources);
|
||||
} while (token != null);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
353
Yavsc/GoogleApiSupport/Google.Apis/Services/BaseClientService.cs
Normal file
353
Yavsc/GoogleApiSupport/Google.Apis/Services/BaseClientService.cs
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
/*
|
||||
Copyright 2013 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.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Google.Apis.Discovery;
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.Json;
|
||||
using Google.Apis.Logging;
|
||||
using Google.Apis.Requests;
|
||||
using Google.Apis.Util;
|
||||
using Google.Apis.Testing;
|
||||
|
||||
namespace Google.Apis.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// A base class for a client service which provides common mechanism for all services, like
|
||||
/// serialization and GZip support. It should be safe to use a single service instance to make server requests
|
||||
/// concurrently from multiple threads.
|
||||
/// This class adds a special <see cref="Google.Apis.Http.IHttpExecuteInterceptor"/> to the
|
||||
/// <see cref="Google.Apis.Http.ConfigurableMessageHandler"/> execute interceptor list, which uses the given
|
||||
/// Authenticator. It calls to its applying authentication method, and injects the "Authorization" header in the
|
||||
/// request.
|
||||
/// If the given Authenticator implements <see cref="Google.Apis.Http.IHttpUnsuccessfulResponseHandler"/>, this
|
||||
/// class adds the Authenticator to the <see cref="Google.Apis.Http.ConfigurableMessageHandler"/>'s unsuccessful
|
||||
/// response handler list.
|
||||
/// </summary>
|
||||
public abstract class BaseClientService : IClientService
|
||||
{
|
||||
/// <summary>The class logger.</summary>
|
||||
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<BaseClientService>();
|
||||
|
||||
/// <summary>The default maximum allowed length of a URL string for GET requests.</summary>
|
||||
[VisibleForTestOnly]
|
||||
public const uint DefaultMaxUrlLength = 2048;
|
||||
|
||||
#region Initializer
|
||||
|
||||
/// <summary>An initializer class for the client service.</summary>
|
||||
public class Initializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the factory for creating <see cref="System.Net.Http.HttpClient"/> instance. If this
|
||||
/// property is not set the service uses a new <see cref="Google.Apis.Http.HttpClientFactory"/> instance.
|
||||
/// </summary>
|
||||
public IHttpClientFactory HttpClientFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a HTTP client initializer which is able to customize properties on
|
||||
/// <see cref="Google.Apis.Http.ConfigurableHttpClient"/> and
|
||||
/// <see cref="Google.Apis.Http.ConfigurableMessageHandler"/>.
|
||||
/// </summary>
|
||||
public IConfigurableHttpClientInitializer HttpClientInitializer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get or sets the exponential back-off policy used by the service. Default value is
|
||||
/// <c>UnsuccessfulResponse503</c>, which means that exponential back-off is used on 503 abnormal HTTP
|
||||
/// response.
|
||||
/// If the value is set to <c>None</c>, no exponential back-off policy is used, and it's up to the user to
|
||||
/// configure the <see cref="Google.Apis.Http.ConfigurableMessageHandler"/> in an
|
||||
/// <see cref="Google.Apis.Http.IConfigurableHttpClientInitializer"/> to set a specific back-off
|
||||
/// implementation (using <see cref="Google.Apis.Http.BackOffHandler"/>).
|
||||
/// </summary>
|
||||
public ExponentialBackOffPolicy DefaultExponentialBackOffPolicy { get; set; }
|
||||
|
||||
/// <summary>Gets or sets whether this service supports GZip. Default value is <c>true</c>.</summary>
|
||||
public bool GZipEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the serializer. Default value is <see cref="Google.Apis.Json.NewtonsoftJsonSerializer"/>.
|
||||
/// </summary>
|
||||
public ISerializer Serializer { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the API Key. Default value is <c>null</c>.</summary>
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Application name to be used in the User-Agent header. Default value is <c>null</c>.
|
||||
/// </summary>
|
||||
public string ApplicationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed length of a URL string for GET requests. Default value is <c>2048</c>. If the value is
|
||||
/// set to <c>0</c>, requests will never be modified due to URL string length.
|
||||
/// </summary>
|
||||
public uint MaxUrlLength { get; set; }
|
||||
|
||||
/// <summary>Constructs a new initializer with default values.</summary>
|
||||
public Initializer()
|
||||
{
|
||||
GZipEnabled = true;
|
||||
Serializer = new NewtonsoftJsonSerializer();
|
||||
DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.UnsuccessfulResponse503;
|
||||
MaxUrlLength = DefaultMaxUrlLength;
|
||||
}
|
||||
|
||||
internal void Validate()
|
||||
{
|
||||
// TODO: Validate ApplicationName
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Constructs a new base client with the specified initializer.</summary>
|
||||
protected BaseClientService(Initializer initializer)
|
||||
{
|
||||
initializer.Validate();
|
||||
// Set the right properties by the initializer's properties.
|
||||
GZipEnabled = initializer.GZipEnabled;
|
||||
Serializer = initializer.Serializer;
|
||||
ApiKey = initializer.ApiKey;
|
||||
ApplicationName = initializer.ApplicationName;
|
||||
if (ApplicationName == null)
|
||||
{
|
||||
Logger.Warning("Application name is not set. Please set Initializer.ApplicationName property");
|
||||
}
|
||||
HttpClientInitializer = initializer.HttpClientInitializer;
|
||||
|
||||
// Create a HTTP client for this service.
|
||||
HttpClient = CreateHttpClient(initializer);
|
||||
}
|
||||
|
||||
/// <summary>Returns <c>true</c> if this service contains the specified feature.</summary>
|
||||
private bool HasFeature(Features feature)
|
||||
{
|
||||
return Features.Contains(Utilities.GetEnumStringValue(feature));
|
||||
}
|
||||
|
||||
private ConfigurableHttpClient CreateHttpClient(Initializer initializer)
|
||||
{
|
||||
// If factory wasn't set use the default HTTP client factory.
|
||||
var factory = initializer.HttpClientFactory ?? new HttpClientFactory();
|
||||
var args = new CreateHttpClientArgs
|
||||
{
|
||||
GZipEnabled = GZipEnabled,
|
||||
ApplicationName = ApplicationName,
|
||||
};
|
||||
|
||||
// Add the user's input initializer.
|
||||
if (HttpClientInitializer != null)
|
||||
{
|
||||
args.Initializers.Add(HttpClientInitializer);
|
||||
}
|
||||
|
||||
// Add exponential back-off initializer if necessary.
|
||||
if (initializer.DefaultExponentialBackOffPolicy != ExponentialBackOffPolicy.None)
|
||||
{
|
||||
args.Initializers.Add(new ExponentialBackOffInitializer(initializer.DefaultExponentialBackOffPolicy,
|
||||
CreateBackOffHandler));
|
||||
}
|
||||
|
||||
var httpClient = factory.CreateHttpClient(args);
|
||||
if (initializer.MaxUrlLength > 0)
|
||||
{
|
||||
httpClient.MessageHandler.AddExecuteInterceptor(new MaxUrlLengthInterceptor(initializer.MaxUrlLength));
|
||||
}
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the back-off handler with <see cref="Google.Apis.Util.ExponentialBackOff"/>.
|
||||
/// Overrides this method to change the default behavior of back-off handler (e.g. you can change the maximum
|
||||
/// waited request's time span, or create a back-off handler with you own implementation of
|
||||
/// <see cref="Google.Apis.Util.IBackOff"/>).
|
||||
/// </summary>
|
||||
protected virtual BackOffHandler CreateBackOffHandler()
|
||||
{
|
||||
// TODO(peleyal): consider return here interface and not the concrete class
|
||||
return new BackOffHandler(new ExponentialBackOff());
|
||||
}
|
||||
|
||||
#region IClientService Members
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConfigurableHttpClient HttpClient { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IConfigurableHttpClientInitializer HttpClientInitializer { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool GZipEnabled { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ApiKey { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ApplicationName { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetRequestSerailizedContent(HttpRequestMessage request, object body)
|
||||
{
|
||||
request.SetRequestSerailizedContent(this, body, GZipEnabled);
|
||||
}
|
||||
|
||||
#region Serialization
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISerializer Serializer { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string SerializeObject(object obj)
|
||||
{
|
||||
if (HasFeature(Discovery.Features.LegacyDataResponse))
|
||||
{
|
||||
// Legacy path
|
||||
var request = new StandardResponse<object> { Data = obj };
|
||||
return Serializer.Serialize(request);
|
||||
}
|
||||
return Serializer.Serialize(obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<T> DeserializeResponse<T>(HttpResponseMessage response)
|
||||
{
|
||||
var text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
// If a string is request, don't parse the response.
|
||||
if (Type.Equals(typeof(T), typeof(string)))
|
||||
{
|
||||
return (T)(object)text;
|
||||
}
|
||||
|
||||
// Check if there was an error returned. The error node is returned in both paths
|
||||
// Deserialize the stream based upon the format of the stream.
|
||||
if (HasFeature(Discovery.Features.LegacyDataResponse))
|
||||
{
|
||||
// Legacy path (deprecated!)
|
||||
StandardResponse<T> sr = null;
|
||||
try
|
||||
{
|
||||
sr = Serializer.Deserialize<StandardResponse<T>>(text);
|
||||
}
|
||||
catch (JsonReaderException ex)
|
||||
{
|
||||
throw new GoogleApiException(Name,
|
||||
"Failed to parse response from server as json [" + text + "]", ex);
|
||||
}
|
||||
|
||||
if (sr.Error != null)
|
||||
{
|
||||
throw new GoogleApiException(Name, "Server error - " + sr.Error)
|
||||
{
|
||||
Error = sr.Error
|
||||
};
|
||||
}
|
||||
|
||||
if (sr.Data == null)
|
||||
{
|
||||
throw new GoogleApiException(Name, "The response could not be deserialized.");
|
||||
}
|
||||
return sr.Data;
|
||||
}
|
||||
|
||||
// New path: Deserialize the object directly.
|
||||
T result = default(T);
|
||||
try
|
||||
{
|
||||
result = Serializer.Deserialize<T>(text);
|
||||
}
|
||||
catch (JsonReaderException ex)
|
||||
{
|
||||
throw new GoogleApiException(Name, "Failed to parse response from server as json [" + text + "]", ex);
|
||||
}
|
||||
|
||||
// TODO(peleyal): is this the right place to check ETag? it isn't part of deserialization!
|
||||
// If this schema/object provides an error container, check it.
|
||||
var eTag = response.Headers.ETag != null ? response.Headers.ETag.Tag : null;
|
||||
if (result is IDirectResponseSchema && eTag != null)
|
||||
{
|
||||
(result as IDirectResponseSchema).ETag = eTag;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<RequestError> DeserializeError(HttpResponseMessage response)
|
||||
{
|
||||
StandardResponse<object> errorResponse = null;
|
||||
try
|
||||
{
|
||||
var str = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
errorResponse = Serializer.Deserialize<StandardResponse<object>>(str);
|
||||
if (errorResponse.Error == null)
|
||||
{
|
||||
throw new GoogleApiException(Name, "error response is null");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// exception will be thrown in case the response content is empty or it can't be deserialized to
|
||||
// Standard response (which contains data and error properties)
|
||||
throw new GoogleApiException(Name,
|
||||
"An Error occurred, but the error response could not be deserialized", ex);
|
||||
}
|
||||
|
||||
return errorResponse.Error;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Abstract Members
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract string Name { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract string BaseUri { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract string BasePath { get; }
|
||||
|
||||
/// <summary>The URI used for batch operations.</summary>
|
||||
public virtual string BatchUri { get { return null; } }
|
||||
|
||||
/// <summary>The path used for batch operations.</summary>
|
||||
public virtual string BatchPath { get { return null; } }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract IList<string> Features { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
if (HttpClient != null)
|
||||
{
|
||||
HttpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
Copyright 2010 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.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.Requests;
|
||||
|
||||
namespace Google.Apis.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Client service contains all the necessary information a Google service requires.
|
||||
/// Each concrete <see cref="Google.Apis.Requests.IClientServiceRequest"/> has a reference to a service for
|
||||
/// important properties like API key, application name, base Uri, etc.
|
||||
/// This service interface also contains serialization methods to serialize an object to stream and deserialize a
|
||||
/// stream into an object.
|
||||
/// </summary>
|
||||
public interface IClientService : IDisposable
|
||||
{
|
||||
/// <summary>Gets the HTTP client which is used to create requests.</summary>
|
||||
ConfigurableHttpClient HttpClient { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a HTTP client initializer which is able to custom properties on
|
||||
/// <see cref="Google.Apis.Http.ConfigurableHttpClient"/> and
|
||||
/// <see cref="Google.Apis.Http.ConfigurableMessageHandler"/>.
|
||||
/// </summary>
|
||||
IConfigurableHttpClientInitializer HttpClientInitializer { get; }
|
||||
|
||||
/// <summary>Gets the service name.</summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>Gets the BaseUri of the service. All request paths should be relative to this URI.</summary>
|
||||
string BaseUri { get; }
|
||||
|
||||
/// <summary>Gets the BasePath of the service.</summary>
|
||||
string BasePath { get; }
|
||||
|
||||
/// <summary>Gets the supported features by this service.</summary>
|
||||
IList<string> Features { get; }
|
||||
|
||||
/// <summary>Gets or sets whether this service supports GZip.</summary>
|
||||
bool GZipEnabled { get; }
|
||||
|
||||
/// <summary>Gets the API-Key (DeveloperKey) which this service uses for all requests.</summary>
|
||||
string ApiKey { get; }
|
||||
|
||||
/// <summary>Gets the application name to be used in the User-Agent header.</summary>
|
||||
string ApplicationName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the content of the request by the given body and the this service's configuration.
|
||||
/// First the body object is serialized by the Serializer and then, if GZip is enabled, the content will be
|
||||
/// wrapped in a GZip stream, otherwise a regular string stream will be used.
|
||||
/// </summary>
|
||||
void SetRequestSerailizedContent(HttpRequestMessage request, object body);
|
||||
|
||||
#region Serialization Methods
|
||||
|
||||
/// <summary>Gets the Serializer used by this service.</summary>
|
||||
ISerializer Serializer { get; }
|
||||
|
||||
/// <summary>Serializes an object into a string representation.</summary>
|
||||
string SerializeObject(object data);
|
||||
|
||||
/// <summary>Deserializes a response into the specified object.</summary>
|
||||
Task<T> DeserializeResponse<T>(HttpResponseMessage response);
|
||||
|
||||
/// <summary>Deserializes an error response into a <see cref="RequestError"/> object.</summary>
|
||||
/// <exception cref="GoogleApiException">If no error is found in the response.</exception>
|
||||
Task<RequestError> DeserializeError(HttpResponseMessage response);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
72
Yavsc/GoogleApiSupport/Google.Apis/Upload/IUploadProgress.cs
Normal file
72
Yavsc/GoogleApiSupport/Google.Apis/Upload/IUploadProgress.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
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;
|
||||
|
||||
namespace Google.Apis.Upload
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum to communicate the status of an upload for progress reporting.
|
||||
/// </summary>
|
||||
public enum UploadStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The upload has not started.
|
||||
/// </summary>
|
||||
NotStarted,
|
||||
|
||||
/// <summary>
|
||||
/// The upload is initializing.
|
||||
/// </summary>
|
||||
Starting,
|
||||
|
||||
/// <summary>
|
||||
/// Data is being uploaded.
|
||||
/// </summary>
|
||||
Uploading,
|
||||
|
||||
/// <summary>
|
||||
/// The upload completed successfully.
|
||||
/// </summary>
|
||||
Completed,
|
||||
|
||||
/// <summary>
|
||||
/// The upload failed.
|
||||
/// </summary>
|
||||
Failed
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Interface reporting upload progress.
|
||||
/// </summary>
|
||||
public interface IUploadProgress
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current status of the upload
|
||||
/// </summary>
|
||||
UploadStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the approximate number of bytes sent to the server.
|
||||
/// </summary>
|
||||
long BytesSent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an exception if one occurred.
|
||||
/// </summary>
|
||||
Exception Exception { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
Copyright 2016 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.
|
||||
*/
|
||||
|
||||
namespace Google.Apis.Upload
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IUploadSessionData: Provides UploadUri for client to persist. Allows resuming an upload after a program restart for seekable ContentStreams.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Defines the data passed from the ResumeableUpload class upon initiation of an upload.
|
||||
/// When the client application adds an event handler for the UploadSessionData event, the data
|
||||
/// defined in this interface (currently the UploadURI) is passed as a parameter to the event handler procedure.
|
||||
/// An event handler for the UploadSessionData event is only required if the application will support resuming the
|
||||
/// upload after a program restart.
|
||||
/// </remarks>
|
||||
public interface IUploadSessionData
|
||||
{
|
||||
/// <summary>
|
||||
/// The resumable session URI (UploadUri)
|
||||
/// </summary>
|
||||
System.Uri UploadUri { get; }
|
||||
}
|
||||
|
||||
}
|
||||
1051
Yavsc/GoogleApiSupport/Google.Apis/Upload/ResumableUpload.cs
Normal file
1051
Yavsc/GoogleApiSupport/Google.Apis/Upload/ResumableUpload.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// 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 Google.Apis.Http;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Google.Apis.Upload
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for <see cref="ResumableUpload"/> operations.
|
||||
/// </summary>
|
||||
public sealed class ResumableUploadOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP client to use when starting the upload sessions and uploading data.
|
||||
/// </summary>
|
||||
public HttpClient HttpClient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback for modifying the session initiation request.
|
||||
/// See https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload#start-resumable for more information.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note: If these options are used with a <see cref="ResumableUpload"/> created using <see cref="ResumableUpload.CreateFromUploadUri"/>,
|
||||
/// this property will be ignored as the session has already been initiated.
|
||||
/// </remarks>
|
||||
public Action<HttpRequestMessage> ModifySessionInitiationRequest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the serializer to use when parsing error responses.
|
||||
/// </summary>
|
||||
public ISerializer Serializer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the service performing the upload.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will be used to set the <see cref="GoogleApiException.ServiceName"/> in the event of an error.
|
||||
/// </remarks>
|
||||
public string ServiceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="HttpClient"/> as a <see cref="Google.Apis.Http.ConfigurableHttpClient"/> if it is an instance of one.
|
||||
/// </summary>
|
||||
internal ConfigurableHttpClient ConfigurableHttpClient => HttpClient as ConfigurableHttpClient;
|
||||
}
|
||||
}
|
||||
183
Yavsc/GoogleApiSupport/Google.Apis/Util/Store/FileDataStore.cs
Normal file
183
Yavsc/GoogleApiSupport/Google.Apis/Util/Store/FileDataStore.cs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
Copyright 2017 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.
|
||||
*/
|
||||
|
||||
// TODO: This does not support UWP Storage.
|
||||
|
||||
using Google.Apis.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Util.Store
|
||||
{
|
||||
/// <summary>
|
||||
/// File data store that implements <see cref="IDataStore"/>. This store creates a different file for each
|
||||
/// combination of type and key. This file data store stores a JSON format of the specified object.
|
||||
/// </summary>
|
||||
public class FileDataStore : IDataStore
|
||||
{
|
||||
private const string XdgDataHomeSubdirectory = "google-filedatastore";
|
||||
private static readonly Task CompletedTask = Task.FromResult(0);
|
||||
|
||||
readonly string folderPath;
|
||||
/// <summary>Gets the full folder path.</summary>
|
||||
public string FolderPath { get { return folderPath; } }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new file data store. If <c>fullPath</c> is <c>false</c> the path will be used as relative to
|
||||
/// <c>Environment.SpecialFolder.ApplicationData"</c> on Windows, or <c>$HOME</c> on Linux and MacOS,
|
||||
/// otherwise the input folder will be treated as absolute.
|
||||
/// The folder is created if it doesn't exist yet.
|
||||
/// </summary>
|
||||
/// <param name="folder">Folder path.</param>
|
||||
/// <param name="fullPath">
|
||||
/// Defines whether the folder parameter is absolute or relative to
|
||||
/// <c>Environment.SpecialFolder.ApplicationData</c> on Windows, or<c>$HOME</c> on Linux and MacOS.
|
||||
/// </param>
|
||||
public FileDataStore(string folder, bool fullPath = false)
|
||||
{
|
||||
folderPath = fullPath
|
||||
? folder
|
||||
: Path.Combine(GetHomeDirectory(), folder);
|
||||
if (!Directory.Exists(folderPath))
|
||||
{
|
||||
Directory.CreateDirectory(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetHomeDirectory()
|
||||
{
|
||||
string appData = Environment.GetEnvironmentVariable("APPDATA");
|
||||
if (!string.IsNullOrEmpty(appData))
|
||||
{
|
||||
// This is almost certainly windows.
|
||||
// This path must be the same between the desktop FileDataStore and this netstandard FileDataStore.
|
||||
return appData;
|
||||
}
|
||||
string home = Environment.GetEnvironmentVariable("HOME");
|
||||
if (!string.IsNullOrEmpty(home))
|
||||
{
|
||||
// This is almost certainly Linux or MacOS.
|
||||
// Follow the XDG Base Directory Specification: https://specifications.freedesktop.org/basedir-spec/latest/index.html
|
||||
// Store data in subdirectory of $XDG_DATA_HOME if it exists, defaulting to $HOME/.local/share if not set.
|
||||
string xdgDataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
|
||||
if (string.IsNullOrEmpty(xdgDataHome))
|
||||
{
|
||||
xdgDataHome = Path.Combine(home, ".local", "share");
|
||||
}
|
||||
return Path.Combine(xdgDataHome, XdgDataHomeSubdirectory);
|
||||
}
|
||||
throw new PlatformNotSupportedException("Relative FileDataStore paths not supported on this platform.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the given value for the given key. It creates a new file (named <see cref="GenerateStoredKey"/>) in
|
||||
/// <see cref="FolderPath"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to store in the data store.</typeparam>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="value">The value to store in the data store.</param>
|
||||
public Task StoreAsync<T>(string key, T value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
throw new ArgumentException("Key MUST have a value");
|
||||
}
|
||||
|
||||
var serialized = NewtonsoftJsonSerializer.Instance.Serialize(value);
|
||||
var filePath = Path.Combine(folderPath, GenerateStoredKey(key, typeof(T)));
|
||||
File.WriteAllText(filePath, serialized);
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the given key. It deletes the <see cref="GenerateStoredKey"/> named file in
|
||||
/// <see cref="FolderPath"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to delete from the data store.</param>
|
||||
public Task DeleteAsync<T>(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
throw new ArgumentException("Key MUST have a value");
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(folderPath, GenerateStoredKey(key, typeof(T)));
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the stored value for the given key or <c>null</c> if the matching file (<see cref="GenerateStoredKey"/>
|
||||
/// in <see cref="FolderPath"/> doesn't exist.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to retrieve.</typeparam>
|
||||
/// <param name="key">The key to retrieve from the data store.</param>
|
||||
/// <returns>The stored object.</returns>
|
||||
public Task<T> GetAsync<T>(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
throw new ArgumentException("Key MUST have a value");
|
||||
}
|
||||
|
||||
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
|
||||
var filePath = Path.Combine(folderPath, GenerateStoredKey(key, typeof(T)));
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var obj = File.ReadAllText(filePath);
|
||||
tcs.SetResult(NewtonsoftJsonSerializer.Instance.Deserialize<T>(obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.SetResult(default(T));
|
||||
}
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all values in the data store. This method deletes all files in <see cref="FolderPath"/>.
|
||||
/// </summary>
|
||||
public Task ClearAsync()
|
||||
{
|
||||
if (Directory.Exists(folderPath))
|
||||
{
|
||||
Directory.Delete(folderPath, true);
|
||||
Directory.CreateDirectory(folderPath);
|
||||
}
|
||||
|
||||
return CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Creates a unique stored key based on the key and the class type.</summary>
|
||||
/// <param name="key">The object key.</param>
|
||||
/// <param name="t">The type to store or retrieve.</param>
|
||||
public static string GenerateStoredKey(string key, Type t)
|
||||
{
|
||||
return string.Format("{0}-{1}", t.FullName, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
Copyright 2017 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.Threading.Tasks;
|
||||
|
||||
namespace Google.Apis.Util.Store
|
||||
{
|
||||
/// <summary>
|
||||
/// A null datastore. Nothing is stored, nothing is retrievable.
|
||||
/// </summary>
|
||||
public class NullDataStore : IDataStore
|
||||
{
|
||||
private static readonly Task s_completedTask = CompletedTask<int>();
|
||||
|
||||
private static Task<T> CompletedTask<T>()
|
||||
{
|
||||
var tcs = new TaskCompletionSource<T>();
|
||||
tcs.SetResult(default(T));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new null datastore, that stores nothing.
|
||||
/// </summary>
|
||||
public NullDataStore()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task ClearAsync() => s_completedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task DeleteAsync<T>(string key) => s_completedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously returns the stored value for the given key or <c>null</c> if not found.
|
||||
/// This implementation of <see cref="IDataStore"/> will always return a completed task
|
||||
/// with a result of <c>null</c>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to retrieve from the data store.</typeparam>
|
||||
/// <param name="key">The key to retrieve its value.</param>
|
||||
/// <returns>Always <c>null</c>.</returns>
|
||||
public Task<T> GetAsync<T>(string key) => CompletedTask<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously stores the given value for the given key (replacing any existing value).
|
||||
/// This implementation of <see cref="IDataStore"/> does not store the value,
|
||||
/// and will not return it in future calls to <see cref="GetAsync{T}(string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to store in the data store.</typeparam>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>A task that completes immediately.</returns>
|
||||
public Task StoreAsync<T>(string key, T value) => s_completedTask;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue