mirror of https://github.com/apache/jclouds.git
JCLOUDS-458: Resumable Upload with live tests
This commit is contained in:
parent
72e42dbfba
commit
f679dc707a
|
@ -25,6 +25,7 @@ import org.jclouds.googlecloudstorage.features.BucketApi;
|
||||||
import org.jclouds.googlecloudstorage.features.DefaultObjectAccessControlsApi;
|
import org.jclouds.googlecloudstorage.features.DefaultObjectAccessControlsApi;
|
||||||
import org.jclouds.googlecloudstorage.features.ObjectAccessControlsApi;
|
import org.jclouds.googlecloudstorage.features.ObjectAccessControlsApi;
|
||||||
import org.jclouds.googlecloudstorage.features.ObjectApi;
|
import org.jclouds.googlecloudstorage.features.ObjectApi;
|
||||||
|
import org.jclouds.googlecloudstorage.features.ResumableUploadApi;
|
||||||
import org.jclouds.rest.annotations.Delegate;
|
import org.jclouds.rest.annotations.Delegate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,4 +69,11 @@ public interface GoogleCloudStorageApi extends Closeable {
|
||||||
@Delegate
|
@Delegate
|
||||||
@Path("")
|
@Path("")
|
||||||
ObjectApi getObjectApi();
|
ObjectApi getObjectApi();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to Google Cloud Storage ResumableUpload features
|
||||||
|
*/
|
||||||
|
@Delegate
|
||||||
|
@Path("")
|
||||||
|
ResumableUploadApi getResumableUploadApi();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You 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 org.jclouds.googlecloudstorage.binders;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import org.jclouds.googlecloudstorage.domain.templates.ObjectTemplate;
|
||||||
|
import org.jclouds.http.HttpRequest;
|
||||||
|
import org.jclouds.rest.MapBinder;
|
||||||
|
import org.jclouds.rest.binders.BindToJsonPayload;
|
||||||
|
|
||||||
|
public class ResumableUploadBinder implements MapBinder {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private BindToJsonPayload jsonBinder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <R extends HttpRequest> R bindToRequest(R request, Map<String, Object> postParams)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
ObjectTemplate template = (ObjectTemplate) postParams.get("template");
|
||||||
|
return bindToRequest(request, template);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <R extends HttpRequest> R bindToRequest(R request, Object input) {
|
||||||
|
return jsonBinder.bindToRequest(request, input);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,4 +31,8 @@ public final class DomainUtils {
|
||||||
List<Byte> reversedList = Lists.reverse(hashByte);
|
List<Byte> reversedList = Lists.reverse(hashByte);
|
||||||
return Bytes.toArray(reversedList);
|
return Bytes.toArray(reversedList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String generateContentRange(Long lowerLimit, Long upperLimit, Long totalSize) {
|
||||||
|
return "bytes " + lowerLimit + "-" + upperLimit + "/" + totalSize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You 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 org.jclouds.googlecloudstorage.domain;
|
||||||
|
|
||||||
|
import static com.google.common.base.MoreObjects.toStringHelper;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
|
||||||
|
import org.jclouds.javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.common.base.MoreObjects.ToStringHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents results of resumable upload response.
|
||||||
|
*/
|
||||||
|
public class ResumableUpload {
|
||||||
|
|
||||||
|
protected final Integer statusCode;
|
||||||
|
protected final String uploadId;
|
||||||
|
protected final String contentLength;
|
||||||
|
protected final Long rangeUpperValue;
|
||||||
|
protected final Long rangeLowerValue;
|
||||||
|
|
||||||
|
private ResumableUpload(Integer statusCode, @Nullable String uploadId, @Nullable String contentLength,
|
||||||
|
@Nullable Long rangeLowerValue, @Nullable Long rangeUpperValue) {
|
||||||
|
if (rangeLowerValue != null && rangeUpperValue != null) {
|
||||||
|
checkArgument(rangeLowerValue < rangeUpperValue, "lower range must less than upper range, was: %s - %s",
|
||||||
|
rangeLowerValue, rangeUpperValue);
|
||||||
|
}
|
||||||
|
this.statusCode = checkNotNull(statusCode, "statusCode");
|
||||||
|
this.uploadId = uploadId;
|
||||||
|
this.contentLength = contentLength;
|
||||||
|
this.rangeUpperValue = rangeUpperValue;
|
||||||
|
this.rangeLowerValue = rangeLowerValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUploadId() {
|
||||||
|
return uploadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getStatusCode() {
|
||||||
|
return statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentLength() {
|
||||||
|
return contentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getRangeUpperValue() {
|
||||||
|
return rangeUpperValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getRangeLowerValue() {
|
||||||
|
return rangeLowerValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ToStringHelper string() {
|
||||||
|
return toStringHelper(this).add("statusCode", statusCode).add("uploadId", uploadId)
|
||||||
|
.add("contentLength", contentLength).add("rangeUpperValue", rangeUpperValue)
|
||||||
|
.add("rangeLowerValue", rangeLowerValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return string().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder toBuilder() {
|
||||||
|
return new Builder().fromResumableUpload(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
|
||||||
|
protected String uploadId;
|
||||||
|
protected Integer statusCode;
|
||||||
|
protected String contentLength;
|
||||||
|
protected Long rangeUpperValue;
|
||||||
|
protected Long rangeLowerValue;
|
||||||
|
|
||||||
|
public Builder uploadId(String uploadId) {
|
||||||
|
this.uploadId = uploadId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder statusCode(Integer statusCode) {
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder contentLength(String contentLength) {
|
||||||
|
this.contentLength = contentLength;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder rangeUpperValue(Long rangeUpperValue) {
|
||||||
|
this.rangeUpperValue = rangeUpperValue;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder rangeLowerValue(Long rangeLowerValue) {
|
||||||
|
this.rangeLowerValue = rangeLowerValue;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResumableUpload build() {
|
||||||
|
return new ResumableUpload(statusCode, uploadId, contentLength, rangeLowerValue, rangeUpperValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder fromResumableUpload(ResumableUpload in) {
|
||||||
|
return this.statusCode(in.getStatusCode()).uploadId(in.getUploadId()).contentLength(in.getContentLength())
|
||||||
|
.rangeUpperValue(in.getRangeUpperValue()).rangeLowerValue(in.getRangeLowerValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You 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 org.jclouds.googlecloudstorage.features;
|
||||||
|
|
||||||
|
import static org.jclouds.googlecloudstorage.reference.GoogleCloudStorageConstants.STORAGE_FULLCONTROL_SCOPE;
|
||||||
|
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.DefaultValue;
|
||||||
|
import javax.ws.rs.HeaderParam;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import org.jclouds.googlecloudstorage.binders.ResumableUploadBinder;
|
||||||
|
import org.jclouds.googlecloudstorage.binders.UploadBinder;
|
||||||
|
import org.jclouds.googlecloudstorage.domain.ResumableUpload;
|
||||||
|
import org.jclouds.googlecloudstorage.domain.templates.ObjectTemplate;
|
||||||
|
import org.jclouds.googlecloudstorage.options.InsertObjectOptions;
|
||||||
|
import org.jclouds.googlecloudstorage.parser.ParseToResumableUpload;
|
||||||
|
import org.jclouds.io.Payload;
|
||||||
|
import org.jclouds.oauth.v2.config.OAuthScopes;
|
||||||
|
import org.jclouds.oauth.v2.filters.OAuthAuthenticator;
|
||||||
|
import org.jclouds.rest.annotations.MapBinder;
|
||||||
|
import org.jclouds.rest.annotations.PayloadParam;
|
||||||
|
import org.jclouds.rest.annotations.QueryParams;
|
||||||
|
import org.jclouds.rest.annotations.RequestFilters;
|
||||||
|
import org.jclouds.rest.annotations.ResponseParser;
|
||||||
|
import org.jclouds.rest.annotations.SkipEncoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides Resumable Upload support via Rest API
|
||||||
|
*
|
||||||
|
* @see <a href="https://developers.google.com/storage/docs/json_api/v1/objects"/>
|
||||||
|
* @see <a href="https://developers.google.com/storage/docs/json_api/v1/how-tos/upload#resumable"/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
@SkipEncoding({ '/', '=' })
|
||||||
|
@RequestFilters(OAuthAuthenticator.class)
|
||||||
|
public interface ResumableUploadApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initiate a Resumable Upload Session
|
||||||
|
*
|
||||||
|
* @see https://developers.google.com/storage/docs/json_api/v1/how-tos/upload#resumable
|
||||||
|
*
|
||||||
|
* @param bucketName
|
||||||
|
* Name of the bucket in which the object to be stored
|
||||||
|
* @param objectName
|
||||||
|
* Name of the object to upload
|
||||||
|
* @param contentType
|
||||||
|
* Content type of the uploaded data
|
||||||
|
* @param contentLength
|
||||||
|
* ContentLength of the uploaded object (Media part)
|
||||||
|
*
|
||||||
|
* @return a {@link ResumableUpload}
|
||||||
|
*/
|
||||||
|
@Named("Object:initResumableUpload")
|
||||||
|
@POST
|
||||||
|
@QueryParams(keys = "uploadType", values = "resumable")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/upload/storage/v1/b/{bucket}/o")
|
||||||
|
@OAuthScopes(STORAGE_FULLCONTROL_SCOPE)
|
||||||
|
@ResponseParser(ParseToResumableUpload.class)
|
||||||
|
ResumableUpload initResumableUpload(@PathParam("bucket") String bucketName, @QueryParam("name") String objectName,
|
||||||
|
@HeaderParam("X-Upload-Content-Type") String contentType,
|
||||||
|
@HeaderParam("X-Upload-Content-Length") String contentLength);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initiate a Resumable Upload Session
|
||||||
|
*
|
||||||
|
* @see https://developers.google.com/storage/docs/json_api/v1/how-tos/upload#simple
|
||||||
|
*
|
||||||
|
* @param bucketName
|
||||||
|
* Name of the bucket in which the object to be stored
|
||||||
|
* @param contentType
|
||||||
|
* Content type of the uploaded data (Media part)
|
||||||
|
* @param contentLength
|
||||||
|
* Content length of the uploaded data (Media part)
|
||||||
|
* @param metada
|
||||||
|
* Supply an {@link ObjectTemplate}
|
||||||
|
*
|
||||||
|
* @return a {@link ResumableUpload}
|
||||||
|
*/
|
||||||
|
@Named("Object:resumableUpload")
|
||||||
|
@POST
|
||||||
|
@QueryParams(keys = "uploadType", values = "resumable")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/upload/storage/v1/b/{bucket}/o")
|
||||||
|
@OAuthScopes(STORAGE_FULLCONTROL_SCOPE)
|
||||||
|
@MapBinder(ResumableUploadBinder.class)
|
||||||
|
@ResponseParser(ParseToResumableUpload.class)
|
||||||
|
ResumableUpload initResumableUpload(@PathParam("bucket") String bucketName,
|
||||||
|
@HeaderParam("X-Upload-Content-Type") String contentType,
|
||||||
|
@HeaderParam("X-Upload-Content-Length") Long contentLength,
|
||||||
|
@PayloadParam("template") ObjectTemplate metadata);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a new object
|
||||||
|
*
|
||||||
|
* @see https://developers.google.com/storage/docs/json_api/v1/how-tos/upload#resumable
|
||||||
|
*
|
||||||
|
* @param bucketName
|
||||||
|
* Name of the bucket in which the object to be stored
|
||||||
|
* @param options
|
||||||
|
* Supply {@link InsertObjectOptions} with optional query parameters. 'name' is mandatory.
|
||||||
|
*
|
||||||
|
* @return If successful, this method returns a {@link GCSObject} resource.
|
||||||
|
*/
|
||||||
|
@Named("Object:resumableUpload")
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@QueryParams(keys = "uploadType", values = "resumable")
|
||||||
|
@Path("/upload/storage/v1/b/{bucket}/o")
|
||||||
|
@OAuthScopes(STORAGE_FULLCONTROL_SCOPE)
|
||||||
|
@MapBinder(UploadBinder.class)
|
||||||
|
@ResponseParser(ParseToResumableUpload.class)
|
||||||
|
ResumableUpload upload(@PathParam("bucket") String bucketName, @QueryParam("upload_id") String uploadId,
|
||||||
|
@HeaderParam("Content-Type") String contentType, @HeaderParam("Content-Length") String contentLength,
|
||||||
|
@PayloadParam("payload") Payload payload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facilitate to use resumable upload operation to upload files in chunks
|
||||||
|
*
|
||||||
|
* @see https://developers.google.com/storage/docs/json_api/v1/how-tos/upload#resumable
|
||||||
|
*
|
||||||
|
* @param bucketName
|
||||||
|
* Name of the bucket in which the object to be stored
|
||||||
|
* @param uploadId
|
||||||
|
* uploadId returned from initResumableUpload operation
|
||||||
|
* @param contentType
|
||||||
|
* Content type of the uploaded data
|
||||||
|
* @param contentLength
|
||||||
|
* Content length of the uploaded data
|
||||||
|
* @param contentRange
|
||||||
|
* Range in {bytes StartingByte - Endingbyte/Totalsize } format ex: bytes 0 - 1213/2000
|
||||||
|
* @param payload
|
||||||
|
* a {@link Payload} with actual data to upload
|
||||||
|
*
|
||||||
|
* @return a {@link ResumableUpload}
|
||||||
|
*/
|
||||||
|
@Named("Object:Upload")
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@QueryParams(keys = "uploadType", values = "resumable")
|
||||||
|
@Path("/upload/storage/v1/b/{bucket}/o")
|
||||||
|
@OAuthScopes(STORAGE_FULLCONTROL_SCOPE)
|
||||||
|
@MapBinder(UploadBinder.class)
|
||||||
|
@ResponseParser(ParseToResumableUpload.class)
|
||||||
|
ResumableUpload chunkUpload(@PathParam("bucket") String bucketName, @QueryParam("upload_id") String uploadId,
|
||||||
|
@HeaderParam("Content-Type") String contentType, @HeaderParam("Content-Length") Long contentLength,
|
||||||
|
@HeaderParam("Content-Range") String contentRange, @PayloadParam("payload") Payload payload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the status of a resumable upload
|
||||||
|
*
|
||||||
|
* @see https://developers.google.com/storage/docs/json_api/v1/how-tos/upload#resumable
|
||||||
|
*
|
||||||
|
* @param bucketName
|
||||||
|
* Name of the bucket in which the object to be stored
|
||||||
|
* @param uploadId
|
||||||
|
* uploadId returned from initResumableUpload operation
|
||||||
|
* @param contentRange
|
||||||
|
* Range in {bytes StartingByte - Endingbyte/Totalsize } format ex: bytes 0 - 1213/2000
|
||||||
|
*
|
||||||
|
* @return a {@link ResumableUpload}
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Named("Object:Upload")
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@DefaultValue("0")
|
||||||
|
@QueryParams(keys = "uploadType", values = "resumable")
|
||||||
|
@Path("/upload/storage/v1/b/{bucket}/o")
|
||||||
|
@OAuthScopes(STORAGE_FULLCONTROL_SCOPE)
|
||||||
|
@ResponseParser(ParseToResumableUpload.class)
|
||||||
|
ResumableUpload checkStatus(@PathParam("bucket") String bucketName, @QueryParam("upload_id") String uploadId,
|
||||||
|
@HeaderParam("Content-Range") String contentRange);
|
||||||
|
|
||||||
|
}
|
|
@ -46,6 +46,8 @@ public class GoogleCloudStorageErrorHandler implements HttpErrorHandler {
|
||||||
String message412 = "PreconditionFailed: At least one of the pre-conditions you specified did not hold.\n";
|
String message412 = "PreconditionFailed: At least one of the pre-conditions you specified did not hold.\n";
|
||||||
|
|
||||||
switch (response.getStatusCode()) {
|
switch (response.getStatusCode()) {
|
||||||
|
case 308:
|
||||||
|
return;
|
||||||
case 401:
|
case 401:
|
||||||
case 403:
|
case 403:
|
||||||
exception = new AuthorizationException(message, exception);
|
exception = new AuthorizationException(message, exception);
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You 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 org.jclouds.googlecloudstorage.handlers;
|
||||||
|
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import org.jclouds.http.HttpCommand;
|
||||||
|
import org.jclouds.http.HttpResponse;
|
||||||
|
import org.jclouds.http.handlers.BackoffLimitedRetryHandler;
|
||||||
|
import org.jclouds.http.handlers.RedirectionRetryHandler;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will parse and set an appropriate exception on the command object.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class GoogleCloudStorageRedirectRetryHandler extends RedirectionRetryHandler {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
protected GoogleCloudStorageRedirectRetryHandler(BackoffLimitedRetryHandler backoffHandler) {
|
||||||
|
super(backoffHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldRetryRequest(HttpCommand command, HttpResponse response) {
|
||||||
|
if (response.getStatusCode() == 308) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return super.shouldRetryRequest(command, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You 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 org.jclouds.googlecloudstorage.parser;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.jclouds.googlecloudstorage.domain.ResumableUpload;
|
||||||
|
import org.jclouds.http.HttpResponse;
|
||||||
|
|
||||||
|
import com.google.common.base.Function;
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
|
|
||||||
|
public class ParseToResumableUpload implements Function<HttpResponse, ResumableUpload> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResumableUpload apply(HttpResponse response) {
|
||||||
|
|
||||||
|
String contentLength = response.getFirstHeaderOrNull("Content-Length");
|
||||||
|
String sessionUri = response.getFirstHeaderOrNull("Location");
|
||||||
|
String uploadId = null;
|
||||||
|
if (sessionUri != null) {
|
||||||
|
uploadId = getUploadId(sessionUri);
|
||||||
|
}
|
||||||
|
String range = response.getFirstHeaderOrNull("Range");
|
||||||
|
Long upperLimit = null;
|
||||||
|
Long lowerLimit = null;
|
||||||
|
if (range != null) {
|
||||||
|
upperLimit = getUpperLimitFromRange(range);
|
||||||
|
lowerLimit = getLowerLimitFromRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResumableUpload.builder().statusCode(response.getStatusCode()).contentLength(contentLength)
|
||||||
|
.uploadId(uploadId).rangeUpperValue(upperLimit).rangeLowerValue(lowerLimit).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the Id of the Upload
|
||||||
|
private String getUploadId(String sessionUri) {
|
||||||
|
return Splitter.on(Pattern.compile("\\&")).trimResults().omitEmptyStrings().withKeyValueSeparator("=")
|
||||||
|
.split(sessionUri).get("upload_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getUpperLimitFromRange(String range) {
|
||||||
|
String upperLimit = range.split("-")[1];
|
||||||
|
return Long.parseLong(upperLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getLowerLimitFromRange(String range) {
|
||||||
|
String removeByte = range.split("=")[1];
|
||||||
|
String lowerLimit = removeByte.split("-")[0];
|
||||||
|
return Long.parseLong(lowerLimit);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You 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 org.jclouds.googlecloudstorage.features;
|
||||||
|
|
||||||
|
import static javax.ws.rs.core.Response.Status.OK;
|
||||||
|
import static javax.ws.rs.core.Response.Status.CREATED;
|
||||||
|
|
||||||
|
import static org.testng.Assert.assertEquals;
|
||||||
|
import static org.testng.Assert.assertNotNull;
|
||||||
|
import static org.testng.Assert.assertNotEquals;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.jclouds.googlecloudstorage.domain.DomainResourceRefferences.ObjectRole;
|
||||||
|
import org.jclouds.googlecloudstorage.domain.Bucket;
|
||||||
|
import org.jclouds.googlecloudstorage.domain.DomainUtils;
|
||||||
|
import org.jclouds.googlecloudstorage.domain.ResumableUpload;
|
||||||
|
import org.jclouds.googlecloudstorage.domain.templates.BucketTemplate;
|
||||||
|
import org.jclouds.googlecloudstorage.domain.templates.ObjectTemplate;
|
||||||
|
import org.jclouds.googlecloudstorage.domain.ObjectAccessControls;
|
||||||
|
import org.jclouds.googlecloudstorage.internal.BaseGoogleCloudStorageApiLiveTest;
|
||||||
|
import org.jclouds.io.Payloads;
|
||||||
|
import org.jclouds.io.payloads.ByteSourcePayload;
|
||||||
|
import org.jclouds.utils.TestUtils;
|
||||||
|
import org.testng.annotations.AfterClass;
|
||||||
|
import org.testng.annotations.BeforeClass;
|
||||||
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
|
import com.google.common.io.ByteSource;
|
||||||
|
|
||||||
|
public class ResumableUploadApiLiveTest extends BaseGoogleCloudStorageApiLiveTest {
|
||||||
|
|
||||||
|
private static final String BUCKET_NAME = "resumableuploadbucket" + UUID.randomUUID();
|
||||||
|
private static final String UPLOAD_OBJECT_NAME = "jcloudslogo.jpg";
|
||||||
|
private static final String CHUNKED_OBJECT_NAME = "jclouds.pdf";
|
||||||
|
private static final int INCOMPLETE = 308;
|
||||||
|
private static final long MIN_CHUNK_SIZE = 256 * 1024; // Min allowed size for a chunk
|
||||||
|
|
||||||
|
private ResumableUploadApi api() {
|
||||||
|
return api.getResumableUploadApi();
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
private void createBucket() {
|
||||||
|
BucketTemplate template = new BucketTemplate().name(BUCKET_NAME);
|
||||||
|
Bucket bucket = api.getBucketApi().createBucket(PROJECT_NUMBER, template);
|
||||||
|
assertNotNull(bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(groups = "live")
|
||||||
|
public void testResumableJpegUpload() throws IOException {
|
||||||
|
|
||||||
|
// Read Object
|
||||||
|
long contentLength = MIN_CHUNK_SIZE * 4;
|
||||||
|
ByteSource byteSource = TestUtils.randomByteSource().slice(0, contentLength);
|
||||||
|
|
||||||
|
// Initialize resumableUpload with metadata. ObjectTemaplete must provide the name
|
||||||
|
ObjectAccessControls oacl = ObjectAccessControls.builder().bucket(BUCKET_NAME).entity("allUsers")
|
||||||
|
.role(ObjectRole.OWNER).build();
|
||||||
|
|
||||||
|
ObjectTemplate template = new ObjectTemplate();
|
||||||
|
template.contentType("image/jpeg").addAcl(oacl).size(contentLength).name(UPLOAD_OBJECT_NAME)
|
||||||
|
.contentLanguage("en").contentDisposition("attachment");
|
||||||
|
|
||||||
|
ResumableUpload initResponse = api().initResumableUpload(BUCKET_NAME, "image/jpeg", contentLength, template);
|
||||||
|
|
||||||
|
assertNotNull(initResponse);
|
||||||
|
assertEquals(initResponse.getStatusCode().intValue(), OK.getStatusCode());
|
||||||
|
assertNotNull(initResponse.getUploadId());
|
||||||
|
|
||||||
|
String uploadId = initResponse.getUploadId();
|
||||||
|
|
||||||
|
// Upload the payload
|
||||||
|
ByteSourcePayload payload = Payloads.newByteSourcePayload(byteSource);
|
||||||
|
ResumableUpload uploadResponse = api().upload(BUCKET_NAME, uploadId, "image/jpeg", byteSource.read().length + "",
|
||||||
|
payload);
|
||||||
|
|
||||||
|
assertEquals(uploadResponse.getStatusCode().intValue(), OK.getStatusCode());
|
||||||
|
|
||||||
|
// CheckStatus
|
||||||
|
ResumableUpload status = api().checkStatus(BUCKET_NAME, uploadId, "bytes */*");
|
||||||
|
|
||||||
|
int code = status.getStatusCode();
|
||||||
|
assertNotEquals(code, INCOMPLETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(groups = "live")
|
||||||
|
public void testResumableChunkedUpload() throws IOException, InterruptedException {
|
||||||
|
|
||||||
|
// Read Object
|
||||||
|
long contentLength = MIN_CHUNK_SIZE * 3;
|
||||||
|
ByteSource byteSource = TestUtils.randomByteSource().slice(0, contentLength);
|
||||||
|
|
||||||
|
// Initialize resumableUpload with metadata. ObjectTemaplete must provide the name
|
||||||
|
ObjectAccessControls oacl = ObjectAccessControls.builder().bucket(BUCKET_NAME).entity("allUsers")
|
||||||
|
.role(ObjectRole.OWNER).build();
|
||||||
|
|
||||||
|
ObjectTemplate template = new ObjectTemplate();
|
||||||
|
template.contentType("application/pdf").addAcl(oacl).size(contentLength).name(CHUNKED_OBJECT_NAME)
|
||||||
|
.contentLanguage("en").contentDisposition("attachment");
|
||||||
|
|
||||||
|
ResumableUpload initResponse = api().initResumableUpload(BUCKET_NAME, "application/pdf", contentLength, template);
|
||||||
|
|
||||||
|
assertNotNull(initResponse);
|
||||||
|
assertEquals(initResponse.getStatusCode().intValue(), OK.getStatusCode());
|
||||||
|
assertNotNull(initResponse.getUploadId());
|
||||||
|
|
||||||
|
// Get the upload_id for the session
|
||||||
|
String uploadId = initResponse.getUploadId();
|
||||||
|
|
||||||
|
// Check the status first
|
||||||
|
ResumableUpload status = api().checkStatus(BUCKET_NAME, uploadId, "bytes */*");
|
||||||
|
int code = status.getStatusCode();
|
||||||
|
assertEquals(code, INCOMPLETE);
|
||||||
|
|
||||||
|
// Uploads in 2 chunks.
|
||||||
|
long totalSize = byteSource.read().length;
|
||||||
|
long offset = 0;
|
||||||
|
// Size of the first chunk
|
||||||
|
long chunkSize = MIN_CHUNK_SIZE * 2;
|
||||||
|
|
||||||
|
// Uploading First chunk
|
||||||
|
ByteSourcePayload payload = Payloads.newByteSourcePayload(byteSource.slice(offset, chunkSize));
|
||||||
|
long length = byteSource.slice(offset, chunkSize).size();
|
||||||
|
String Content_Range = DomainUtils.generateContentRange(0L, length, totalSize);
|
||||||
|
ResumableUpload uploadResponse = api().chunkUpload(BUCKET_NAME, uploadId, "application/pdf", length,
|
||||||
|
Content_Range, payload);
|
||||||
|
|
||||||
|
int code2 = uploadResponse.getStatusCode();
|
||||||
|
assertEquals(code2, INCOMPLETE);
|
||||||
|
|
||||||
|
// Read uploaded length
|
||||||
|
long lowerValue = uploadResponse.getRangeLowerValue();
|
||||||
|
long uploaded = uploadResponse.getRangeUpperValue();
|
||||||
|
|
||||||
|
assertThat(lowerValue).isEqualTo(0);
|
||||||
|
assertThat(uploaded).isEqualTo(chunkSize - 1); // confirms chunk is totally uploaded
|
||||||
|
|
||||||
|
long resumeLength = totalSize - (uploaded + 1);
|
||||||
|
|
||||||
|
// 2nd chunk
|
||||||
|
ByteSourcePayload payload2 = Payloads.newByteSourcePayload(byteSource.slice(uploaded + 1,
|
||||||
|
byteSource.read().length - uploaded - 1));
|
||||||
|
// Upload the 2nd chunk
|
||||||
|
String Content_Range2 = DomainUtils.generateContentRange(uploaded + 1, totalSize - 1, totalSize);
|
||||||
|
ResumableUpload resumeResponse = api().chunkUpload(BUCKET_NAME, uploadId, "application/pdf", resumeLength,
|
||||||
|
Content_Range2, payload2);
|
||||||
|
|
||||||
|
int code3 = resumeResponse.getStatusCode();
|
||||||
|
assertThat(code3).isIn(OK.getStatusCode(), CREATED.getStatusCode()); // 200 or 201 if upload succeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
private void deleteObjectsandBucket() {
|
||||||
|
api.getObjectApi().deleteObject(BUCKET_NAME, UPLOAD_OBJECT_NAME);
|
||||||
|
api.getObjectApi().deleteObject(BUCKET_NAME, CHUNKED_OBJECT_NAME);
|
||||||
|
api.getBucketApi().deleteBucket(BUCKET_NAME);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue