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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue