All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.api.client.googleapis.media.MediaHttpDownloader Maven / Gradle / Ivy

There is a newer version: 2.7.0
Show newest version
/*
 * 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.
 */

package com.google.api.client.googleapis.media;

import static com.google.common.base.MoreObjects.firstNonNull;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpBackOffIOExceptionHandler;
import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.util.Preconditions;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.OutputStream;

/**
 * Media HTTP Downloader, with support for both direct and resumable media downloads. Documentation
 * is available here.
 *
 * 

Implementation is not thread-safe. * *

Back-off is disabled by default. To enable it for an abnormal HTTP response and an I/O * exception you should call {@link HttpRequest#setUnsuccessfulResponseHandler} with a new {@link * HttpBackOffUnsuccessfulResponseHandler} instance and {@link HttpRequest#setIOExceptionHandler} * with {@link HttpBackOffIOExceptionHandler}. * *

Upgrade warning: in prior version 1.14 exponential back-off was enabled by default for an * abnormal HTTP response. Starting with version 1.15 it's disabled by default. * * @since 1.9 * @author [email protected] (Ravi Mistry) */ @SuppressWarnings("deprecation") public final class MediaHttpDownloader { /** Download state associated with the Media HTTP downloader. */ public enum DownloadState { /** The download process has not started yet. */ NOT_STARTED, /** Set after a media file chunk is downloaded. */ MEDIA_IN_PROGRESS, /** Set after the complete media file is successfully downloaded. */ MEDIA_COMPLETE } /** * Default maximum number of bytes that will be downloaded from the server in any single HTTP * request. Set to 32MB because that is the maximum App Engine request size. */ public static final int MAXIMUM_CHUNK_SIZE = 32 * MediaHttpUploader.MB; /** The request factory for connections to the server. */ private final HttpRequestFactory requestFactory; /** The transport to use for requests. */ private final HttpTransport transport; /** * Determines whether direct media download is enabled or disabled. If value is set to {@code * true} then a direct download will be done where the whole media content is downloaded in a * single request. If value is set to {@code false} then the download uses the resumable media * download protocol to download in data chunks. Defaults to {@code false}. */ private boolean directDownloadEnabled = false; /** Progress listener to send progress notifications to or {@code null} for none. */ private MediaHttpDownloaderProgressListener progressListener; /** * Maximum size of individual chunks that will get downloaded by single HTTP requests. The default * value is {@link #MAXIMUM_CHUNK_SIZE}. */ private int chunkSize = MAXIMUM_CHUNK_SIZE; /** * The length of the HTTP media content or {@code 0} before it is initialized in {@link * #setMediaContentLength}. */ private long mediaContentLength; /** The current state of the downloader. */ private DownloadState downloadState = DownloadState.NOT_STARTED; /** The total number of bytes downloaded by this downloader. */ private long bytesDownloaded; /** * The last byte position of the media file we want to download, default value is {@code -1}. * *

If its value is {@code -1} it means there is no upper limit on the byte position. */ private long lastBytePos = -1; /** * Construct the {@link MediaHttpDownloader}. * * @param transport The transport to use for requests * @param httpRequestInitializer The initializer to use when creating an {@link HttpRequest} or * {@code null} for none */ public MediaHttpDownloader( HttpTransport transport, HttpRequestInitializer httpRequestInitializer) { this.transport = Preconditions.checkNotNull(transport); this.requestFactory = httpRequestInitializer == null ? transport.createRequestFactory() : transport.createRequestFactory(httpRequestInitializer); } /** * Executes a direct media download or a resumable media download. * *

This method does not close the given output stream. * *

This method is not reentrant. A new instance of {@link MediaHttpDownloader} must be * instantiated before download called be called again. * * @param requestUrl The request URL where the download requests will be sent * @param outputStream destination output stream */ public void download(GenericUrl requestUrl, OutputStream outputStream) throws IOException { download(requestUrl, null, outputStream); } /** * Executes a direct media download or a resumable media download. * *

This method does not close the given output stream. * *

This method is not reentrant. A new instance of {@link MediaHttpDownloader} must be * instantiated before download called be called again. * * @param requestUrl request URL where the download requests will be sent * @param requestHeaders request headers or {@code null} to ignore * @param outputStream destination output stream * @since 1.12 */ public void download(GenericUrl requestUrl, HttpHeaders requestHeaders, OutputStream outputStream) throws IOException { Preconditions.checkArgument(downloadState == DownloadState.NOT_STARTED); requestUrl.put("alt", "media"); if (directDownloadEnabled) { updateStateAndNotifyListener(DownloadState.MEDIA_IN_PROGRESS); HttpResponse response = executeCurrentRequest(lastBytePos, requestUrl, requestHeaders, outputStream); // All required bytes have been downloaded from the server. mediaContentLength = firstNonNull(response.getHeaders().getContentLength(), mediaContentLength); bytesDownloaded = mediaContentLength; updateStateAndNotifyListener(DownloadState.MEDIA_COMPLETE); return; } // Download the media content in chunks. while (true) { long currentRequestLastBytePos = bytesDownloaded + chunkSize - 1; if (lastBytePos != -1) { // If last byte position has been specified, use it iff it is smaller than the chunk size. currentRequestLastBytePos = Math.min(lastBytePos, currentRequestLastBytePos); } HttpResponse response = executeCurrentRequest( currentRequestLastBytePos, requestUrl, requestHeaders, outputStream); String contentRange = response.getHeaders().getContentRange(); long nextByteIndex = getNextByteIndex(contentRange); setMediaContentLength(contentRange); // If the last byte position is specified, complete the download when it is less than // nextByteIndex. if (lastBytePos != -1 && lastBytePos <= nextByteIndex) { // All required bytes from the range have been downloaded from the server. bytesDownloaded = lastBytePos; updateStateAndNotifyListener(DownloadState.MEDIA_COMPLETE); return; } if (mediaContentLength <= nextByteIndex) { // All required bytes have been downloaded from the server. bytesDownloaded = mediaContentLength; updateStateAndNotifyListener(DownloadState.MEDIA_COMPLETE); return; } bytesDownloaded = nextByteIndex; updateStateAndNotifyListener(DownloadState.MEDIA_IN_PROGRESS); } } /** * Executes the current request. * * @param currentRequestLastBytePos last byte position for current request * @param requestUrl request URL where the download requests will be sent * @param requestHeaders request headers or {@code null} to ignore * @param outputStream destination output stream * @return HTTP response */ private HttpResponse executeCurrentRequest( long currentRequestLastBytePos, GenericUrl requestUrl, HttpHeaders requestHeaders, OutputStream outputStream) throws IOException { // prepare the GET request HttpRequest request = requestFactory.buildGetRequest(requestUrl); // add request headers if (requestHeaders != null) { request.getHeaders().putAll(requestHeaders); } // set Range header (if necessary) if (bytesDownloaded != 0 || currentRequestLastBytePos != -1) { StringBuilder rangeHeader = new StringBuilder(); rangeHeader.append("bytes=").append(bytesDownloaded).append("-"); if (currentRequestLastBytePos != -1) { rangeHeader.append(currentRequestLastBytePos); } request.getHeaders().setRange(rangeHeader.toString()); } // execute the request and copy into the output stream HttpResponse response = request.execute(); try { ByteStreams.copy(response.getContent(), outputStream); } finally { response.disconnect(); } return response; } /** * Returns the next byte index identifying data that the server has not yet sent out, obtained * from the HTTP Content-Range header (E.g a header of "Content-Range: 0-55/1000" would cause 56 * to be returned). null headers cause 0 to be returned. * * @param rangeHeader in the HTTP response * @return the byte index beginning where the server has yet to send out data */ private long getNextByteIndex(String rangeHeader) { if (rangeHeader == null) { return 0L; } return Long.parseLong( rangeHeader.substring(rangeHeader.indexOf('-') + 1, rangeHeader.indexOf('/'))) + 1; } /** * Sets the total number of bytes that have been downloaded of the media resource. * *

If a download was aborted mid-way due to a connection failure then users can resume the * download from the point where it left off. * *

Use {@link #setContentRange} if you need to specify both the bytes downloaded and the last * byte position. * * @param bytesDownloaded The total number of bytes downloaded */ public MediaHttpDownloader setBytesDownloaded(long bytesDownloaded) { Preconditions.checkArgument(bytesDownloaded >= 0); this.bytesDownloaded = bytesDownloaded; return this; } /** * Sets the content range of the next download request. Eg: bytes=firstBytePos-lastBytePos. * *

If a download was aborted mid-way due to a connection failure then users can resume the * download from the point where it left off. * *

Use {@link #setBytesDownloaded} if you only need to specify the first byte position. * * @param firstBytePos The first byte position in the content range string * @param lastBytePos The last byte position in the content range string. * @since 1.24 */ public MediaHttpDownloader setContentRange(long firstBytePos, long lastBytePos) { Preconditions.checkArgument(lastBytePos >= firstBytePos); setBytesDownloaded(firstBytePos); this.lastBytePos = lastBytePos; return this; } /** @deprecated Use {@link #setContentRange(long, long)} instead. */ @Deprecated public MediaHttpDownloader setContentRange(long firstBytePos, int lastBytePos) { return setContentRange(firstBytePos, (long) lastBytePos); } /** * Sets the media content length from the HTTP Content-Range header (E.g a header of * "Content-Range: 0-55/1000" would cause 1000 to be set. null headers do not set * anything. * * @param rangeHeader in the HTTP response */ private void setMediaContentLength(String rangeHeader) { if (rangeHeader == null) { return; } if (mediaContentLength == 0) { mediaContentLength = Long.parseLong(rangeHeader.substring(rangeHeader.indexOf('/') + 1)); } } /** * Returns whether direct media download is enabled or disabled. If value is set to {@code true} * then a direct download will be done where the whole media content is downloaded in a single * request. If value is set to {@code false} then the download uses the resumable media download * protocol to download in data chunks. Defaults to {@code false}. */ public boolean isDirectDownloadEnabled() { return directDownloadEnabled; } /** * Returns whether direct media download is enabled or disabled. If value is set to {@code true} * then a direct download will be done where the whole media content is downloaded in a single * request. If value is set to {@code false} then the download uses the resumable media download * protocol to download in data chunks. Defaults to {@code false}. */ public MediaHttpDownloader setDirectDownloadEnabled(boolean directDownloadEnabled) { this.directDownloadEnabled = directDownloadEnabled; return this; } /** Sets the progress listener to send progress notifications to or {@code null} for none. */ public MediaHttpDownloader setProgressListener( MediaHttpDownloaderProgressListener progressListener) { this.progressListener = progressListener; return this; } /** Returns the progress listener to send progress notifications to or {@code null} for none. */ public MediaHttpDownloaderProgressListener getProgressListener() { return progressListener; } /** Returns the transport to use for requests. */ public HttpTransport getTransport() { return transport; } /** * Sets the maximum size of individual chunks that will get downloaded by single HTTP requests. * The default value is {@link #MAXIMUM_CHUNK_SIZE}. * *

The maximum allowable value is {@link #MAXIMUM_CHUNK_SIZE}. */ public MediaHttpDownloader setChunkSize(int chunkSize) { Preconditions.checkArgument(chunkSize > 0 && chunkSize <= MAXIMUM_CHUNK_SIZE); this.chunkSize = chunkSize; return this; } /** * Returns the maximum size of individual chunks that will get downloaded by single HTTP requests. * The default value is {@link #MAXIMUM_CHUNK_SIZE}. */ public int getChunkSize() { return chunkSize; } /** * Gets the total number of bytes downloaded by this downloader. * * @return the number of bytes downloaded */ public long getNumBytesDownloaded() { return bytesDownloaded; } /** * Gets the last byte position of the media file we want to download or {@code -1} if there is no * upper limit on the byte position. * * @return the last byte position * @since 1.13 */ public long getLastBytePosition() { return lastBytePos; } /** * Sets the download state and notifies the progress listener. * * @param downloadState value to set to */ private void updateStateAndNotifyListener(DownloadState downloadState) throws IOException { this.downloadState = downloadState; if (progressListener != null) { progressListener.progressChanged(this); } } /** * Gets the current download state of the downloader. * * @return the download state */ public DownloadState getDownloadState() { return downloadState; } /** * Gets the download progress denoting the percentage of bytes that have been downloaded, * represented between 0.0 (0%) and 1.0 (100%). * * @return the download progress */ public double getProgress() { return mediaContentLength == 0 ? 0 : (double) bytesDownloaded / mediaContentLength; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy