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