/* * Copyright (c) 2014 Loic Merckel * Copyright (c) 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. */ /* * * The original version of this file (i.e., the one that is copyrighted 2013 Google Inc.) * can be found here: * * http://code.google.com/p/google-api-java-client/source/checkout * package com.google.api.client.googleapis.media; * */ package io.uploader.drive.drive.media; import com.google.api.client.googleapis.MethodOverride; import com.google.api.client.http.AbstractInputStreamContent; import com.google.api.client.http.ByteArrayContent; import com.google.api.client.http.EmptyContent; import com.google.api.client.http.GZipEncoding; 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.HttpContent; import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpMethods; 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.http.InputStreamContent; import com.google.api.client.http.MultipartContent; import com.google.api.client.util.Beta; import com.google.api.client.util.ByteStreams; import com.google.api.client.util.Preconditions; import com.google.api.client.util.Sleeper; import org.apache.commons.io.input.BoundedInputStream; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Media HTTP Uploader, with support for both direct and resumable media * uploads. Documentation is available <a href= * 'http://code.google.com/p/google-api-java-client/wiki/MediaUpload'>here</a>. * * <p> * For resumable uploads, when the media content length is known, if the * provided {@link InputStream} has {@link InputStream#markSupported} as * {@code false} then it is wrapped in an {@link BufferedInputStream} to support * the {@link InputStream#mark} and {@link InputStream#reset} methods required * for handling server errors. If the media content length is unknown then each * chunk is stored temporarily in memory. This is required to determine when the * last chunk is reached. * </p> * * <p> * See {@link #setDisableGZipContent(boolean)} for information on when content * is gzipped and how to control that behavior. * </p> * * <p> * 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}. * </p> * * <p> * Upgrade warning: in prior version 1.14 exponential back-off was enabled by * default for an abnormal HTTP response and there was a regular retry (without * back-off) when I/O exception was thrown. Starting with version 1.15 back-off * is disabled and there is no retry on I/O exception by default. * </p> * * <p> * Upgrade warning: in prior version 1.16 {@link #serverErrorCallback} was * public but starting with version 1.17 it has been removed from the public * API, and changed to be package private. * </p> * * <p> * Implementation is not thread-safe. * </p> * * @since 1.9 * * @author rmistry@google.com (Ravi Mistry) * @author peleyal@google.com (Eyal Peled) */ public final class MediaHttpUploader { private static final Logger logger = LoggerFactory .getLogger(MediaHttpUploader.class); /** * Upload content type header. * * @since 1.13 */ public static final String CONTENT_LENGTH_HEADER = "X-Upload-Content-Length"; /** * Upload content length header. * * @since 1.13 */ public static final String CONTENT_TYPE_HEADER = "X-Upload-Content-Type"; /** * Upload state associated with the Media HTTP uploader. */ public enum UploadState { /** The upload process has not started yet. */ NOT_STARTED, /** Set before the initiation request is sent. */ INITIATION_STARTED, /** Set after the initiation request completes. */ INITIATION_COMPLETE, /** Set after a media file chunk is uploaded. */ MEDIA_IN_PROGRESS, /** Set after the complete media file is successfully uploaded. */ MEDIA_COMPLETE } /** The current state of the uploader. */ private UploadState uploadState = UploadState.NOT_STARTED; static final int MB = 0x100000; private static final int KB = 0x400; /** * Minimum number of bytes that can be uploaded to the server (set to * 256KB). */ public static final int MINIMUM_CHUNK_SIZE = 256 * KB; /** * Default maximum number of bytes that will be uploaded to the server in * any single HTTP request (set to 10 MB). */ public static final int DEFAULT_CHUNK_SIZE = 10 * MB; /** The HTTP content of the media to be uploaded. */ private final AbstractInputStreamContent mediaContent; /** The request factory for connections to the server. */ private final HttpRequestFactory requestFactory; /** The transport to use for requests. */ private final HttpTransport transport; /** * HTTP content metadata of the media to be uploaded or {@code null} for * none. */ private HttpContent metadata; /** * The length of the HTTP media content. * * <p> * {@code 0} before it is lazily initialized in * {@link #getMediaContentLength()} after which it could still be {@code 0} * for empty media content. Will be {@code < 0} if the media content length * has not been specified. * </p> */ private long mediaContentLength; /** * Determines if media content length has been calculated yet in * {@link #getMediaContentLength()}. */ private boolean isMediaContentLengthCalculated; /** * The HTTP method used for the initiation request. * * <p> * Can only be {@link HttpMethods#POST} (for media upload) or * {@link HttpMethods#PUT} (for media update). The default value is * {@link HttpMethods#POST}. * </p> */ private String initiationRequestMethod = HttpMethods.POST; /** The HTTP headers used in the initiation request. */ private HttpHeaders initiationHeaders = new HttpHeaders(); /** * The HTTP request object that is currently used to send upload requests or * {@code null} before {@link #upload}. */ private HttpRequest currentRequest; /** * An Input stream of the HTTP media content or {@code null} before * {@link #upload}. */ private InputStream contentInputStream; /** * Determines whether direct media upload is enabled or disabled. If value * is set to {@code true} then a direct upload will be done where the whole * media content is uploaded in a single request If value is set to * {@code false} then the upload uses the resumable media upload protocol to * upload in data chunks. Defaults to {@code false}. */ private boolean directUploadEnabled; /** * Progress listener to send progress notifications to or {@code null} for * none. */ private MediaHttpUploaderProgressListener progressListener; /** * The media content length is used in the "Content-Range" header. If we * reached the end of the stream, this variable will be set with the length * of the stream. This value is used only in resumable media upload. */ String mediaContentLengthStr = "*"; /** * The number of bytes the server received so far. This value will not be * calculated for direct uploads when the content length is not known in * advance. */ // TODO(rmistry): Figure out a way to compute the content length using // CountingInputStream. private long totalBytesServerReceived; /** * Maximum size of individual chunks that will get uploaded by single HTTP * requests. The default value is {@link #DEFAULT_CHUNK_SIZE}. */ private int chunkSize = DEFAULT_CHUNK_SIZE; /** * Used to cache a single byte when the media content length is unknown or * {@code null} for none. */ private Byte cachedByte; /** * The number of bytes the client had sent to the server so far or {@code 0} * for none. It is used for resumable media upload when the media content * length is not specified. */ private long totalBytesClientSent; /** * The number of bytes of the current chunk which was sent to the server or * {@code 0} for none. This value equals to chunk size for each chunk the * client send to the server, except for the ending chunk. */ private int currentChunkLength; /** * The content buffer of the current request or {@code null} for none. It is * used for resumable media upload when the media content length is not * specified. It is instantiated for every request in * {@link #setContentAndHeadersOnCurrentRequest} and is set to {@code null} * when the request is completed in {@link #upload}. */ private byte currentRequestContentBuffer[]; /** * Whether to disable GZip compression of HTTP content. * * <p> * The default value is {@code false}. * </p> */ private boolean disableGZipContent; /** Sleeper. **/ Sleeper sleeper = Sleeper.DEFAULT; /** * Construct the {@link MediaHttpUploader}. * * <p> * The input stream received by calling * {@link AbstractInputStreamContent#getInputStream} is closed when the * upload process is successfully completed. For resumable uploads, when the * media content length is known, if the input stream has * {@link InputStream#markSupported} as {@code false} then it is wrapped in * an {@link BufferedInputStream} to support the {@link InputStream#mark} * and {@link InputStream#reset} methods required for handling server * errors. If the media content length is unknown then each chunk is stored * temporarily in memory. This is required to determine when the last chunk * is reached. * </p> * * @param mediaContent * The Input stream content of the media to be uploaded * @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 MediaHttpUploader(AbstractInputStreamContent mediaContent, HttpTransport transport, HttpRequestInitializer httpRequestInitializer) { this.mediaContent = Preconditions.checkNotNull(mediaContent); this.transport = Preconditions.checkNotNull(transport); this.requestFactory = httpRequestInitializer == null ? transport .createRequestFactory() : transport .createRequestFactory(httpRequestInitializer); } /** * Executes a direct media upload or resumable media upload conforming to * the specifications listed <a href= * 'http://code.google.com/apis/gdata/docs/resumable_upload.html'>here.</a> * * <p> * This method is not reentrant. A new instance of {@link MediaHttpUploader} * must be instantiated before upload called be called again. * </p> * * <p> * If an error is encountered during the request execution the caller is * responsible for parsing the response correctly. For example for JSON * errors: * </p> * * <pre> * if (!response.isSuccessStatusCode()) { * throw GoogleJsonResponseException.from(jsonFactory, response); * } * </pre> * * <p> * Callers should call {@link HttpResponse#disconnect} when the returned * HTTP response object is no longer needed. However, * {@link HttpResponse#disconnect} does not have to be called if the * response stream is properly closed. Example usage: * </p> * * <pre> * HttpResponse response = batch.upload(initiationRequestUrl); * try { * // process the HTTP response object * } finally { * response.disconnect(); * } * </pre> * * @param initiationRequestUrl * The request URL where the initiation request will be sent * @return HTTP response */ public HttpResponse upload(GenericUrl initiationRequestUrl) throws IOException { Preconditions.checkArgument(uploadState == UploadState.NOT_STARTED); if (directUploadEnabled) { return directUpload(initiationRequestUrl); } return resumableUpload(initiationRequestUrl); } /** * Direct Uploads the media. * * @param initiationRequestUrl * The request URL where the initiation request will be sent * @return HTTP response */ private HttpResponse directUpload(GenericUrl initiationRequestUrl) throws IOException { updateStateAndNotifyListener(UploadState.MEDIA_IN_PROGRESS); HttpContent content = mediaContent; if (metadata != null) { content = new MultipartContent().setContentParts(Arrays.asList( metadata, mediaContent)); initiationRequestUrl.put("uploadType", "multipart"); } else { initiationRequestUrl.put("uploadType", "media"); } HttpRequest request = requestFactory.buildRequest( initiationRequestMethod, initiationRequestUrl, content); request.getHeaders().putAll(initiationHeaders); // We do not have to do anything special here if media content length is // unspecified because // direct media upload works even when the media content length == -1. HttpResponse response = executeCurrentRequest(request); boolean responseProcessed = false; try { if (isMediaLengthKnown()) { totalBytesServerReceived = getMediaContentLength(); } updateStateAndNotifyListener(UploadState.MEDIA_COMPLETE); responseProcessed = true; } finally { if (!responseProcessed) { response.disconnect(); } } return response; } /** * Uploads the media in a resumable manner. * * @param initiationRequestUrl * The request URL where the initiation request will be sent * @return HTTP response */ // https://developers.google.com/drive/web/manage-uploads#resumable private HttpResponse resumableUpload(GenericUrl initiationRequestUrl) throws IOException { // Make initial request to get the unique upload URL. HttpResponse initialResponse = executeUploadInitiation(initiationRequestUrl); if (!initialResponse.isSuccessStatusCode()) { // If the initiation request is not successful return it // immediately. logger.info("Unsuccessful: " + initialResponse.getStatusMessage()); return initialResponse; } GenericUrl uploadUrl; try { uploadUrl = new GenericUrl(initialResponse.getHeaders().getLocation()); } finally { initialResponse.disconnect(); } // Convert media content into a byte stream to upload in chunks. contentInputStream = mediaContent.getInputStream(); if (!contentInputStream.markSupported() && isMediaLengthKnown()) { // If we know the media content length then wrap the stream into a // Buffered input stream to // support the {@link InputStream#mark} and {@link // InputStream#reset} methods required for // handling server errors. contentInputStream = new BufferedInputStream(contentInputStream); } HttpResponse response = null ; // Upload the media content in chunks. while (true) { currentRequest = requestFactory.buildPutRequest(uploadUrl, null); setContentAndHeadersOnCurrentRequest(); // set mediaErrorHandler as I/O exception handler and as // unsuccessful response handler for // calling to serverErrorCallback on an I/O exception or an abnormal // HTTP response AtomicInteger httpErrorCounter = new AtomicInteger (0) ; new MediaUploadErrorHandler(this, currentRequest, httpErrorCounter); AtomicInteger tryCounter = new AtomicInteger (0) ; while (true) { try { if (isMediaLengthKnown()) { // TODO(rmistry): Support gzipping content for the case where // media content length is // known // (https://code.google.com/p/google-api-java-client/issues/detail?id=691). response = executeCurrentRequestWithoutGZip(currentRequest); break ; } else { response = executeCurrentRequest(currentRequest); break ; } } catch (Throwable e) { logger.error("Error occurred while uploading", e); if (tryCounter.getAndIncrement() >= 5) { logger.error("Could not be recovered..."); throw e ; } logger.error("Retry (" + tryCounter.get() + " times)", e); } } boolean returningResponse = false; try { if (response.isSuccessStatusCode()) { totalBytesServerReceived = getMediaContentLength(); if (mediaContent.getCloseInputStream()) { contentInputStream.close(); } updateStateAndNotifyListener(UploadState.MEDIA_COMPLETE); returningResponse = true; return response; } int statusCode = response.getStatusCode() ; if (statusCode != 308) { // https://developers.google.com/drive/web/manage-uploads#resume-upload returningResponse = true; return response; } else { httpErrorCounter.set(0); } // Check to see if the upload URL has changed on the server. String updatedUploadUrl = response.getHeaders().getLocation(); if (updatedUploadUrl != null) { uploadUrl = new GenericUrl(updatedUploadUrl); } // we check the amount of bytes the server received so far, // because the server may process // fewer bytes than the amount of bytes the client had sent long newBytesServerReceived = getNextByteIndex(response .getHeaders().getRange()); // the server can receive any amount of bytes from 0 to current // chunk length long currentBytesServerReceived = newBytesServerReceived - totalBytesServerReceived; Preconditions.checkState(currentBytesServerReceived >= 0 && currentBytesServerReceived <= currentChunkLength); long copyBytes = currentChunkLength - currentBytesServerReceived; if (isMediaLengthKnown()) { if (copyBytes > 0) { // If the server didn't receive all the bytes the client // sent the current position of // the input stream is incorrect. So we should reset the // stream and skip those bytes // that the server had already received. // Otherwise (the server got all bytes the client sent), // the stream is in its right // position, and we can continue from there contentInputStream.reset(); long actualSkipValue = contentInputStream .skip(currentBytesServerReceived); Preconditions .checkState(currentBytesServerReceived == actualSkipValue); } } else if (copyBytes == 0) { // server got all the bytes, so we don't need to use this // buffer. Otherwise, we have to // keep the buffer and copy part (or all) of its bytes to // the stream we are sending to the // server currentRequestContentBuffer = null; } totalBytesServerReceived = newBytesServerReceived; updateStateAndNotifyListener(UploadState.MEDIA_IN_PROGRESS); } finally { if (!returningResponse) { response.disconnect(); } } } } /** * @return {@code true} if the media length is known, otherwise * {@code false} */ private boolean isMediaLengthKnown() throws IOException { return getMediaContentLength() >= 0; } /** * Uses lazy initialization to compute the media content length. * * <p> * This is done to avoid throwing an {@link IOException} in the constructor. * </p> */ private long getMediaContentLength() throws IOException { if (!isMediaContentLengthCalculated) { mediaContentLength = mediaContent.getLength(); isMediaContentLengthCalculated = true; } return mediaContentLength; } /** * This method sends a POST request with empty content to get the unique * upload URL. * * @param initiationRequestUrl * The request URL where the initiation request will be sent */ private HttpResponse executeUploadInitiation(GenericUrl initiationRequestUrl) throws IOException { updateStateAndNotifyListener(UploadState.INITIATION_STARTED); initiationRequestUrl.put("uploadType", "resumable"); HttpContent content = metadata == null ? new EmptyContent() : metadata; HttpRequest request = requestFactory.buildRequest( initiationRequestMethod, initiationRequestUrl, content); initiationHeaders.set(CONTENT_TYPE_HEADER, mediaContent.getType()); if (isMediaLengthKnown()) { initiationHeaders.set(CONTENT_LENGTH_HEADER, getMediaContentLength()); } request.getHeaders().putAll(initiationHeaders); HttpResponse response = executeCurrentRequest(request); boolean notificationCompleted = false; try { updateStateAndNotifyListener(UploadState.INITIATION_COMPLETE); notificationCompleted = true; } finally { if (!notificationCompleted) { response.disconnect(); } } return response; } /** * Executes the current request with some minimal common code. * * @param request * current request * @return HTTP response */ private HttpResponse executeCurrentRequestWithoutGZip(HttpRequest request) throws IOException { // method override for non-POST verbs new MethodOverride().intercept(request); // don't throw an exception so we can let a custom Google exception be // thrown request.setThrowExceptionOnExecuteError(false); // execute the request HttpResponse response = request.execute(); return response; } /** * Executes the current request with some common code that includes * exponential backoff and GZip encoding. * * @param request * current request * @return HTTP response */ private HttpResponse executeCurrentRequest(HttpRequest request) throws IOException { // enable GZip encoding if necessary if (!disableGZipContent && !(request.getContent() instanceof EmptyContent)) { request.setEncoding(new GZipEncoding()); } // execute request HttpResponse response = executeCurrentRequestWithoutGZip(request); return response; } /** * Sets the HTTP media content chunk and the required headers that should be * used in the upload request. */ private void setContentAndHeadersOnCurrentRequest() throws IOException { int blockSize; if (isMediaLengthKnown()) { // We know exactly what the blockSize will be because we know the // media content length. blockSize = (int) Math.min(chunkSize, getMediaContentLength() - totalBytesServerReceived); } else { // Use the chunkSize as the blockSize because we do know what what // it is yet. blockSize = chunkSize; } AbstractInputStreamContent contentChunk; int actualBlockSize = blockSize; if (isMediaLengthKnown()) { // Mark the current position in case we need to retry the request. contentInputStream.mark(blockSize); //InputStream limitInputStream = ByteStreams.limit( // contentInputStream, blockSize); InputStream limitInputStream = new BoundedInputStream(contentInputStream, blockSize); ((BoundedInputStream) limitInputStream).setPropagateClose(false) ; contentChunk = new InputStreamContent(mediaContent.getType(), limitInputStream).setRetrySupported(true) .setLength(blockSize).setCloseInputStream(false); mediaContentLengthStr = String.valueOf(getMediaContentLength()); } else { // If the media content length is not known we implement a custom // buffered input stream that // enables us to detect the length of the media content when the // last chunk is sent. We // accomplish this by always trying to read an extra byte further // than the end of the current // chunk. int actualBytesRead; int bytesAllowedToRead; // amount of bytes which need to be copied from last chunk buffer int copyBytes = 0; if (currentRequestContentBuffer == null) { bytesAllowedToRead = cachedByte == null ? blockSize + 1 : blockSize; currentRequestContentBuffer = new byte[blockSize + 1]; if (cachedByte != null) { currentRequestContentBuffer[0] = cachedByte; } } else { // currentRequestContentBuffer is not null that means one of the // following: // 1. This is a request to recover from a server error (e.g. // 503) // or // 2. The server received less bytes than the amount of bytes // the client had sent. For // example, the client sends bytes 100-199, but the server // returns back status code 308, // and its "Range" header is "bytes=0-150". // In that case, the new request will be constructed from the // previous request's byte buffer // plus new bytes from the stream. copyBytes = (int) (totalBytesClientSent - totalBytesServerReceived); // shift copyBytes bytes to the beginning - those are the bytes // which weren't received by // the server in the last chunk. System.arraycopy(currentRequestContentBuffer, currentChunkLength - copyBytes, currentRequestContentBuffer, 0, copyBytes); if (cachedByte != null) { // add the last cached byte to the buffer currentRequestContentBuffer[copyBytes] = cachedByte; } bytesAllowedToRead = blockSize - copyBytes; } actualBytesRead = ByteStreams.read(contentInputStream, currentRequestContentBuffer, blockSize + 1 - bytesAllowedToRead, bytesAllowedToRead); if (actualBytesRead < bytesAllowedToRead) { actualBlockSize = copyBytes + Math.max(0, actualBytesRead); if (cachedByte != null) { actualBlockSize++; cachedByte = null; } if (mediaContentLengthStr.equals("*")) { // At this point we know we reached the media content length // because we either read less // than the specified chunk size or there is no more data // left to be read. mediaContentLengthStr = String .valueOf(totalBytesServerReceived + actualBlockSize); } } else { cachedByte = currentRequestContentBuffer[blockSize]; } contentChunk = new ByteArrayContent(mediaContent.getType(), currentRequestContentBuffer, 0, actualBlockSize); totalBytesClientSent = totalBytesServerReceived + actualBlockSize; } currentChunkLength = actualBlockSize; currentRequest.setContent(contentChunk); if (actualBlockSize == 0) { // special case of zero content media being uploaded currentRequest.getHeaders().setContentRange("bytes */0"); } else { currentRequest.getHeaders().setContentRange( "bytes " + totalBytesServerReceived + "-" + (totalBytesServerReceived + actualBlockSize - 1) + "/" + mediaContentLengthStr); } } /** * {@link Beta} <br/> * The call back method that will be invoked on a server error or an I/O * exception during resumable upload inside {@link #upload}. * * <p> * This method changes the current request to query the current status of * the upload to find how many bytes were successfully uploaded before the * server error occurred. * </p> */ @Beta void serverErrorCallback() throws IOException { Preconditions.checkNotNull(currentRequest, "The current request should not be null"); // Query the current status of the upload by issuing an empty PUT // request on the upload URI. currentRequest.setContent(new EmptyContent()); currentRequest .getHeaders() .setContentRange( "bytes */" + (isMediaLengthKnown() ? getMediaContentLength() : "*")); } /** * Returns the next byte index identifying data that the server has not yet * received, obtained from the HTTP Range header (E.g a header of * "Range: 0-55" would cause 56 to be returned). <code>null</code> or * malformed headers cause 0 to be returned. * * @param rangeHeader * in the HTTP response * @return the byte index beginning where the server has yet to receive data */ private long getNextByteIndex(String rangeHeader) { if (rangeHeader == null) { return 0L; } return Long .parseLong(rangeHeader.substring(rangeHeader.indexOf('-') + 1)) + 1; } /** * Returns HTTP content metadata for the media request or {@code null} for * none. */ public HttpContent getMetadata() { return metadata; } /** * Sets HTTP content metadata for the media request or {@code null} for * none. */ public MediaHttpUploader setMetadata(HttpContent metadata) { this.metadata = metadata; return this; } /** Returns the HTTP content of the media to be uploaded. */ public HttpContent getMediaContent() { return mediaContent; } /** Returns the transport to use for requests. */ public HttpTransport getTransport() { return transport; } /** * Sets whether direct media upload is enabled or disabled. * * <p> * If value is set to {@code true} then a direct upload will be done where * the whole media content is uploaded in a single request. If value is set * to {@code false} then the upload uses the resumable media upload protocol * to upload in data chunks. * </p> * * <p> * Direct upload is recommended if the content size falls below a certain * minimum limit. This is because there's minimum block write size for some * Google APIs, so if the resumable request fails in the space of that first * block, the client will have to restart from the beginning anyway. * </p> * * <p> * Defaults to {@code false}. * </p> * * @since 1.9 */ public MediaHttpUploader setDirectUploadEnabled(boolean directUploadEnabled) { this.directUploadEnabled = directUploadEnabled; return this; } /** * Returns whether direct media upload is enabled or disabled. If value is * set to {@code true} then a direct upload will be done where the whole * media content is uploaded in a single request. If value is set to * {@code false} then the upload uses the resumable media upload protocol to * upload in data chunks. Defaults to {@code false}. * * @since 1.9 */ public boolean isDirectUploadEnabled() { return directUploadEnabled; } /** * Sets the progress listener to send progress notifications to or * {@code null} for none. */ public MediaHttpUploader setProgressListener( MediaHttpUploaderProgressListener progressListener) { this.progressListener = progressListener; return this; } /** * Returns the progress listener to send progress notifications to or * {@code null} for none. */ public MediaHttpUploaderProgressListener getProgressListener() { return progressListener; } /** * Sets the maximum size of individual chunks that will get uploaded by * single HTTP requests. The default value is {@link #DEFAULT_CHUNK_SIZE}. * * <p> * The minimum allowable value is {@link #MINIMUM_CHUNK_SIZE} and the * specified chunk size must be a multiple of {@link #MINIMUM_CHUNK_SIZE}. * </p> */ public MediaHttpUploader setChunkSize(int chunkSize) { Preconditions.checkArgument(chunkSize > 0 && chunkSize % MINIMUM_CHUNK_SIZE == 0); this.chunkSize = chunkSize; return this; } /** * Returns the maximum size of individual chunks that will get uploaded by * single HTTP requests. The default value is {@link #DEFAULT_CHUNK_SIZE}. */ public int getChunkSize() { return chunkSize; } /** * Returns whether to disable GZip compression of HTTP content. * * @since 1.13 */ public boolean getDisableGZipContent() { return disableGZipContent; } /** * Sets whether to disable GZip compression of HTTP content. * * <p> * By default it is {@code false}. * </p> * * <p> * If {@link #setDisableGZipContent(boolean)} is set to false (the default * value) then content is gzipped for direct media upload and resumable * media uploads when content length is not known. Due to a current * limitation, content is not gzipped for resumable media uploads when * content length is known; this limitation will be removed in the future. * </p> * * @since 1.13 */ public MediaHttpUploader setDisableGZipContent(boolean disableGZipContent) { this.disableGZipContent = disableGZipContent; return this; } /** * Returns the sleeper. * * @since 1.15 */ public Sleeper getSleeper() { return sleeper; } /** * Sets the sleeper. The default value is {@link Sleeper#DEFAULT}. * * @since 1.15 */ public MediaHttpUploader setSleeper(Sleeper sleeper) { this.sleeper = sleeper; return this; } /** * Returns the HTTP method used for the initiation request. * * <p> * The default value is {@link HttpMethods#POST}. * </p> * * @since 1.12 */ public String getInitiationRequestMethod() { return initiationRequestMethod; } /** * Sets the HTTP method used for the initiation request. * * <p> * Can only be {@link HttpMethods#POST} (for media upload) or * {@link HttpMethods#PUT} (for media update). The default value is * {@link HttpMethods#POST}. * </p> * * @since 1.12 */ public MediaHttpUploader setInitiationRequestMethod( String initiationRequestMethod) { Preconditions.checkArgument(initiationRequestMethod .equals(HttpMethods.POST) || initiationRequestMethod.equals(HttpMethods.PUT)); this.initiationRequestMethod = initiationRequestMethod; return this; } /** Sets the HTTP headers used for the initiation request. */ public MediaHttpUploader setInitiationHeaders(HttpHeaders initiationHeaders) { this.initiationHeaders = initiationHeaders; return this; } /** Returns the HTTP headers used for the initiation request. */ public HttpHeaders getInitiationHeaders() { return initiationHeaders; } /** * Gets the total number of bytes the server received so far or {@code 0} * for direct uploads when the content length is not known. * * @return the number of bytes the server received so far */ public long getNumBytesUploaded() { return totalBytesServerReceived; } /** * Sets the upload state and notifies the progress listener. * * @param uploadState * value to set to */ private void updateStateAndNotifyListener(UploadState uploadState) throws IOException { this.uploadState = uploadState; if (progressListener != null) { progressListener.progressChanged(this); } } /** * Gets the current upload state of the uploader. * * @return the upload state */ public UploadState getUploadState() { return uploadState; } /** * Gets the upload progress denoting the percentage of bytes that have been * uploaded, represented between 0.0 (0%) and 1.0 (100%). * * <p> * Do not use if the specified {@link AbstractInputStreamContent} has no * content length specified. Instead, consider using * {@link #getNumBytesUploaded} to denote progress. * </p> * * @throws IllegalArgumentException * if the specified {@link AbstractInputStreamContent} has no * content length * @return the upload progress */ public double getProgress() throws IOException { Preconditions .checkArgument( isMediaLengthKnown(), "Cannot call getProgress() if " + "the specified AbstractInputStreamContent has no content length. Use " + " getNumBytesUploaded() to denote progress instead."); return getMediaContentLength() == 0 ? 0 : (double) totalBytesServerReceived / getMediaContentLength(); } }