Google Api

This commit is contained in:
Paul Schneider 2017-06-20 02:47:52 +02:00
commit 83f9fc1bd8
123 changed files with 12961 additions and 60 deletions

View file

@ -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; }
}
}

View file

@ -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);
}
}

View 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
}
}
}

View 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,
}
}

View file

@ -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
};
}
}
}

View 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();
}
}
}

View file

@ -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
}
}

View file

@ -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);
}
}
}
}

View file

@ -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();
}
}

View file

@ -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; }
}
}

View 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;
}
}
}

View 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();
}
}
}
}

View file

@ -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
}
}

View 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; }
}
}

View file

@ -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; }
}
}

File diff suppressed because it is too large Load diff

View file

@ -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;
}
}

View 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);
}
}
}

View file

@ -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;
}
}