From 2385ba901e98d89e6cabd2b32028e1b3cac3992e Mon Sep 17 00:00:00 2001 From: Timur Alperovich Date: Sat, 26 Sep 2015 15:16:13 -0700 Subject: [PATCH] JCLOUDS-1008: Use @Encoded with GCS. Google cloud storage should use the @Encoded annotation with the object names to make sure that the object is percent-encoded prior to being submitted in the path of the request. This was previously broken because the default path encoding ignores "/" and encodes the entire string. The @Encoded annotation instructs jclouds annotation processor to not encode the parameters to which it is attached and not to encode the entire request path. Parameters that are not annotated with @Encoded are URL encoded prior to being add to the path. --- .../GoogleCloudStorageBlobStore.java | 47 +++++------- .../features/ObjectAccessControlsApi.java | 32 ++++---- .../features/ObjectApi.java | 74 ++++++++++--------- ...leCloudStorageBlobIntegrationLiveTest.java | 1 + .../features/ObjectApiMockTest.java | 16 ++++ .../test/resources/object_encoded_get.json | 21 ++++++ 6 files changed, 114 insertions(+), 77 deletions(-) create mode 100644 providers/google-cloud-storage/src/test/resources/object_encoded_get.json diff --git a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobStore.java b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobStore.java index e852704d23..5dbb7cd610 100644 --- a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobStore.java +++ b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobStore.java @@ -20,8 +20,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.io.BaseEncoding.base64; import static org.jclouds.googlecloudstorage.domain.DomainResourceReferences.ObjectRole.READER; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.util.List; import java.util.Set; @@ -41,11 +39,11 @@ import org.jclouds.blobstore.domain.internal.BlobImpl; import org.jclouds.blobstore.domain.internal.PageSetImpl; import org.jclouds.blobstore.functions.BlobToHttpGetOptions; import org.jclouds.blobstore.internal.BaseBlobStore; +import org.jclouds.blobstore.options.CopyOptions; import org.jclouds.blobstore.options.CreateContainerOptions; import org.jclouds.blobstore.options.GetOptions; import org.jclouds.blobstore.options.ListContainerOptions; import org.jclouds.blobstore.options.PutOptions; -import org.jclouds.blobstore.options.CopyOptions; import org.jclouds.blobstore.strategy.internal.FetchBlobMetadata; import org.jclouds.blobstore.util.BlobUtils; import org.jclouds.collect.Memoized; @@ -73,11 +71,10 @@ import org.jclouds.http.HttpResponseException; import org.jclouds.io.ContentMetadata; import org.jclouds.io.Payload; import org.jclouds.io.PayloadSlicer; +import org.jclouds.util.Strings2; -import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.base.Supplier; -import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.hash.HashCode; @@ -204,12 +201,7 @@ public final class GoogleCloudStorageBlobStore extends BaseBlobStore { */ @Override public boolean blobExists(String container, String name) { - try { - String urlName = name.contains("/") ? URLEncoder.encode(name, Charsets.UTF_8.toString()) : name; - return api.getObjectApi().objectExists(container, urlName); - } catch (UnsupportedEncodingException e) { - throw Throwables.propagate(e); - } + return api.getObjectApi().objectExists(container, Strings2.urlEncode(name)); } /** @@ -239,12 +231,12 @@ public final class GoogleCloudStorageBlobStore extends BaseBlobStore { @Override public BlobMetadata blobMetadata(String container, String name) { - return objectToBlobMetadata.apply(api.getObjectApi().getObject(container, name)); + return objectToBlobMetadata.apply(api.getObjectApi().getObject(container, Strings2.urlEncode(name))); } @Override public Blob getBlob(String container, String name, GetOptions options) { - GoogleCloudStorageObject gcsObject = api.getObjectApi().getObject(container, name); + GoogleCloudStorageObject gcsObject = api.getObjectApi().getObject(container, Strings2.urlEncode(name)); if (gcsObject == null) { return null; } @@ -252,7 +244,7 @@ public final class GoogleCloudStorageBlobStore extends BaseBlobStore { MutableBlobMetadata metadata = objectToBlobMetadata.apply(gcsObject); Blob blob = new BlobImpl(metadata); // TODO: Does getObject not get the payload?! - Payload payload = api.getObjectApi().download(container, name, httpOptions).getPayload(); + Payload payload = api.getObjectApi().download(container, Strings2.urlEncode(name), httpOptions).getPayload(); payload.setContentMetadata(metadata.getContentMetadata()); // Doing this first retains it on setPayload. blob.setPayload(payload); return blob; @@ -260,18 +252,13 @@ public final class GoogleCloudStorageBlobStore extends BaseBlobStore { @Override public void removeBlob(String container, String name) { - String urlName; - try { - urlName = name.contains("/") ? URLEncoder.encode(name, Charsets.UTF_8.toString()) : name; - } catch (UnsupportedEncodingException uee) { - throw Throwables.propagate(uee); - } - api.getObjectApi().deleteObject(container, urlName); + api.getObjectApi().deleteObject(container, Strings2.urlEncode(name)); } @Override public BlobAccess getBlobAccess(String container, String name) { - ObjectAccessControls controls = api.getObjectAccessControlsApi().getObjectAccessControls(container, name, "allUsers"); + ObjectAccessControls controls = api.getObjectAccessControlsApi().getObjectAccessControls(container, + Strings2.urlEncode(name), "allUsers"); if (controls != null && controls.role() == DomainResourceReferences.ObjectRole.READER) { return BlobAccess.PUBLIC_READ; } else { @@ -287,9 +274,9 @@ public final class GoogleCloudStorageBlobStore extends BaseBlobStore { .bucket(container) .role(READER) .build(); - api.getObjectApi().patchObject(container, name, new ObjectTemplate().addAcl(controls)); + api.getObjectApi().patchObject(container, Strings2.urlEncode(name), new ObjectTemplate().addAcl(controls)); } else { - api.getObjectAccessControlsApi().deleteObjectAccessControls(container, name, "allUsers"); + api.getObjectAccessControlsApi().deleteObjectAccessControls(container, Strings2.urlEncode(name), "allUsers"); } } @@ -312,7 +299,8 @@ public final class GoogleCloudStorageBlobStore extends BaseBlobStore { public String copyBlob(String fromContainer, String fromName, String toContainer, String toName, CopyOptions options) { if (!options.getContentMetadata().isPresent() && !options.getUserMetadata().isPresent()) { - return api.getObjectApi().copyObject(toContainer, toName, fromContainer, fromName).etag(); + return api.getObjectApi().copyObject(toContainer, Strings2.urlEncode(toName), fromContainer, + Strings2.urlEncode(fromName)).etag(); } ObjectTemplate template = new ObjectTemplate(); @@ -349,7 +337,8 @@ public final class GoogleCloudStorageBlobStore extends BaseBlobStore { template.customMetadata(options.getUserMetadata().get()); } - return api.getObjectApi().copyObject(toContainer, toName, fromContainer, fromName, template).etag(); + return api.getObjectApi().copyObject(toContainer, Strings2.urlEncode(toName), fromContainer, + Strings2.urlEncode(fromName), template).etag(); } @Override @@ -372,12 +361,14 @@ public final class GoogleCloudStorageBlobStore extends BaseBlobStore { public String completeMultipartUpload(MultipartUpload mpu, List parts) { ImmutableList.Builder builder = ImmutableList.builder(); for (MultipartPart part : parts) { - builder.add(api.getObjectApi().getObject(mpu.containerName(), getMPUPartName(mpu, part.partNumber()))); + builder.add(api.getObjectApi().getObject(mpu.containerName(), + Strings2.urlEncode(getMPUPartName(mpu, part.partNumber())))); } ObjectTemplate destination = blobMetadataToObjectTemplate.apply(mpu.blobMetadata()); ComposeObjectTemplate template = ComposeObjectTemplate.builder().fromGoogleCloudStorageObject(builder.build()) .destination(destination).build(); - return api.getObjectApi().composeObjects(mpu.containerName(), mpu.blobName(), template).etag(); + return api.getObjectApi().composeObjects(mpu.containerName(), Strings2.urlEncode(mpu.blobName()), template) + .etag(); // TODO: delete components? } diff --git a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/features/ObjectAccessControlsApi.java b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/features/ObjectAccessControlsApi.java index ce3102938e..32b5e7c13a 100644 --- a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/features/ObjectAccessControlsApi.java +++ b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/features/ObjectAccessControlsApi.java @@ -23,6 +23,7 @@ import java.util.List; import javax.inject.Named; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; +import javax.ws.rs.Encoded; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; @@ -41,7 +42,6 @@ import org.jclouds.rest.annotations.Fallback; import org.jclouds.rest.annotations.PATCH; import org.jclouds.rest.annotations.RequestFilters; import org.jclouds.rest.annotations.SelectJson; -import org.jclouds.rest.annotations.SkipEncoding; import org.jclouds.rest.binders.BindToJsonPayload; /** @@ -49,7 +49,6 @@ import org.jclouds.rest.binders.BindToJsonPayload; * * @see */ -@SkipEncoding({ '/', '=' }) @RequestFilters(OAuthFilter.class) @Consumes(APPLICATION_JSON) public interface ObjectAccessControlsApi { @@ -74,7 +73,7 @@ public interface ObjectAccessControlsApi { @Fallback(NullOnNotFoundOr404.class) @Nullable ObjectAccessControls getObjectAccessControls(@PathParam("bucket") String bucketName, - @PathParam("object") String objectName, @PathParam("entity") String entity); + @PathParam("object") @Encoded String objectName, @PathParam("entity") String entity); /** * Returns the acl entry for the specified entity on the specified object. @@ -97,7 +96,7 @@ public interface ObjectAccessControlsApi { @Fallback(NullOnNotFoundOr404.class) @Nullable ObjectAccessControls getObjectAccessControls(@PathParam("bucket") String bucketName, - @PathParam("object") String objectName, @PathParam("entity") String entity, + @PathParam("object") @Encoded String objectName, @PathParam("entity") String entity, @QueryParam("generation") Long generation); /** @@ -116,7 +115,7 @@ public interface ObjectAccessControlsApi { @Produces(APPLICATION_JSON) @Path("/b/{bucket}/o/{object}/acl") ObjectAccessControls createObjectAccessControls(@PathParam("bucket") String bucketName, - @PathParam("object") String objectName, + @PathParam("object") @Encoded String objectName, @BinderParam(BindToJsonPayload.class) ObjectAccessControlsTemplate template); /** @@ -137,7 +136,7 @@ public interface ObjectAccessControlsApi { @Produces(APPLICATION_JSON) @Path("/b/{bucket}/o/{object}/acl") ObjectAccessControls createObjectAccessControls(@PathParam("bucket") String bucketName, - @PathParam("object") String objectName, + @PathParam("object") @Encoded String objectName, @BinderParam(BindToJsonPayload.class) ObjectAccessControlsTemplate template, @QueryParam("generation") Long generation); @@ -155,8 +154,8 @@ public interface ObjectAccessControlsApi { @Named("ObjectAccessControls:delete") @DELETE @Path("/b/{bucket}/o/{object}/acl/{entity}") - void deleteObjectAccessControls(@PathParam("bucket") String bucketName, @PathParam("object") String objectName, - @PathParam("entity") String entity); + void deleteObjectAccessControls(@PathParam("bucket") String bucketName, + @PathParam("object") @Encoded String objectName, @PathParam("entity") String entity); /** * Permanently deletes the acl entry for the specified entity on the specified bucket. @@ -174,8 +173,9 @@ public interface ObjectAccessControlsApi { @Named("ObjectAccessControls:delete") @DELETE @Path("/b/{bucket}/o/{object}/acl/{entity}") - void deleteObjectAccessControls(@PathParam("bucket") String bucketName, @PathParam("object") String objectName, - @PathParam("entity") String entity, @QueryParam("generation") Long generation); + void deleteObjectAccessControls(@PathParam("bucket") String bucketName, + @PathParam("object") @Encoded String objectName, @PathParam("entity") String entity, + @QueryParam("generation") Long generation); /** * Retrieves acl entries on a specified object @@ -193,7 +193,7 @@ public interface ObjectAccessControlsApi { @Fallback(NullOnNotFoundOr404.class) @Nullable List listObjectAccessControls(@PathParam("bucket") String bucketName, - @PathParam("object") String objectName); + @PathParam("object") @Encoded String objectName); /** * Retrieves acl entries on a specified object @@ -214,7 +214,7 @@ public interface ObjectAccessControlsApi { @Fallback(NullOnNotFoundOr404.class) @Nullable List listObjectAccessControls(@PathParam("bucket") String bucketName, - @PathParam("object") String objectName, @QueryParam("generation") Long generation); + @PathParam("object") @Encoded String objectName, @QueryParam("generation") Long generation); /** * Updates an acl entry on the specified object @@ -237,7 +237,7 @@ public interface ObjectAccessControlsApi { @Produces(APPLICATION_JSON) @Path("/b/{bucket}/o/{object}/acl/{entity}") ObjectAccessControls updateObjectAccessControls(@PathParam("bucket") String bucketName, - @PathParam("object") String objectName, @PathParam("entity") String entity, + @PathParam("object") @Encoded String objectName, @PathParam("entity") String entity, @BinderParam(BindToJsonPayload.class) ObjectAccessControlsTemplate template); /** @@ -262,7 +262,7 @@ public interface ObjectAccessControlsApi { @Produces(APPLICATION_JSON) @Path("/b/{bucket}/o/{object}/acl/{entity}") ObjectAccessControls updateObjectAccessControls(@PathParam("bucket") String bucketName, - @PathParam("object") String objectName, @PathParam("entity") String entity, + @PathParam("object") @Encoded String objectName, @PathParam("entity") String entity, @BinderParam(BindToJsonPayload.class) ObjectAccessControlsTemplate template, @QueryParam("generation") Long generation); @@ -286,7 +286,7 @@ public interface ObjectAccessControlsApi { @Produces(APPLICATION_JSON) @Path("/b/{bucket}/o/{object}/acl/{entity}") ObjectAccessControls patchObjectAccessControls(@PathParam("bucket") String bucketName, - @PathParam("object") String objectName, @PathParam("entity") String entity, + @PathParam("object") @Encoded String objectName, @PathParam("entity") String entity, @BinderParam(BindToJsonPayload.class) ObjectAccessControlsTemplate template); /** @@ -311,7 +311,7 @@ public interface ObjectAccessControlsApi { @Produces(APPLICATION_JSON) @Path("/b/{bucket}/o/{object}/acl/{entity}") ObjectAccessControls patchObjectAccessControls(@PathParam("bucket") String bucketName, - @PathParam("object") String objectName, @PathParam("entity") String entity, + @PathParam("object") @Encoded String objectName, @PathParam("entity") String entity, @BinderParam(BindToJsonPayload.class) ObjectAccessControlsTemplate template, @QueryParam("generation") Long generation); } diff --git a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/features/ObjectApi.java b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/features/ObjectApi.java index ff8dfdea94..4e4f5b93ff 100644 --- a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/features/ObjectApi.java +++ b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/features/ObjectApi.java @@ -21,6 +21,7 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import javax.inject.Named; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; +import javax.ws.rs.Encoded; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; @@ -60,7 +61,6 @@ 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; import org.jclouds.rest.binders.BindToJsonPayload; /** @@ -68,7 +68,6 @@ import org.jclouds.rest.binders.BindToJsonPayload; * * @see */ -@SkipEncoding({ '/', '=' }) @RequestFilters(OAuthFilter.class) public interface ObjectApi { @@ -87,7 +86,7 @@ public interface ObjectApi { @Path("storage/v1/b/{bucket}/o/{object}") @Fallback(FalseOnNotFoundOr404.class) @Nullable - boolean objectExists(@PathParam("bucket") String bucketName, @PathParam("object") String objectName); + boolean objectExists(@PathParam("bucket") String bucketName, @PathParam("object") @Encoded String objectName); /** * Retrieve an object metadata @@ -105,7 +104,8 @@ public interface ObjectApi { @Consumes(APPLICATION_JSON) @Fallback(NullOnNotFoundOr404.class) @Nullable - GoogleCloudStorageObject getObject(@PathParam("bucket") String bucketName, @PathParam("object") String objectName); + GoogleCloudStorageObject getObject(@PathParam("bucket") String bucketName, + @PathParam("object") @Encoded String objectName); /** * Retrieves objects metadata @@ -126,8 +126,8 @@ public interface ObjectApi { @Consumes(APPLICATION_JSON) @Fallback(NullOnNotFoundOr404.class) @Nullable - GoogleCloudStorageObject getObject(@PathParam("bucket") String bucketName, @PathParam("object") String objectName, - HttpRequestOptions options); + GoogleCloudStorageObject getObject(@PathParam("bucket") String bucketName, + @PathParam("object") @Encoded String objectName, HttpRequestOptions options); /** * Retrieve an object or their metadata @@ -146,7 +146,7 @@ public interface ObjectApi { @ResponseParser(ParseToPayloadEnclosing.class) @Fallback(NullOnNotFoundOr404.class) @Nullable - PayloadEnclosing download(@PathParam("bucket") String bucketName, @PathParam("object") String objectName); + PayloadEnclosing download(@PathParam("bucket") String bucketName, @PathParam("object") @Encoded String objectName); /** * Retrieves objects @@ -167,7 +167,8 @@ public interface ObjectApi { @Path("storage/v1/b/{bucket}/o/{object}") @ResponseParser(ParseToPayloadEnclosing.class) @Fallback(NullOnNotFoundOr404.class) - @Nullable PayloadEnclosing download(@PathParam("bucket") String bucketName, @PathParam("object") String objectName, + @Nullable + PayloadEnclosing download(@PathParam("bucket") String bucketName, @PathParam("object") @Encoded String objectName, HttpRequestOptions options); /** @@ -204,7 +205,7 @@ public interface ObjectApi { @DELETE @Path("storage/v1/b/{bucket}/o/{object}") @Fallback(FalseOnNotFoundOr404.class) - boolean deleteObject(@PathParam("bucket") String bucketName, @PathParam("object") String objectName); + boolean deleteObject(@PathParam("bucket") String bucketName, @PathParam("object") @Encoded String objectName); /** * Deletes an object and its metadata. Deletions are permanent if versioning is not enabled for the bucket, or if the @@ -221,7 +222,7 @@ public interface ObjectApi { @DELETE @Path("storage/v1/b/{bucket}/o/{object}") @Fallback(FalseOnNotFoundOr404.class) - boolean deleteObject(@PathParam("bucket") String bucketName, @PathParam("object") String objectName, + boolean deleteObject(@PathParam("bucket") String bucketName, @PathParam("object") @Encoded String objectName, DeleteObjectOptions options); /** @@ -271,8 +272,9 @@ public interface ObjectApi { @Produces(APPLICATION_JSON) @Path("storage/v1/b/{bucket}/o/{object}") @Fallback(NullOnNotFoundOr404.class) - GoogleCloudStorageObject updateObject(@PathParam("bucket") String bucketName, @PathParam("object") String objectName, - @BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate); + GoogleCloudStorageObject updateObject(@PathParam("bucket") String bucketName, + @PathParam("object") @Encoded String objectName, + @BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate); /** * Updates an object @@ -294,8 +296,9 @@ public interface ObjectApi { @Produces(APPLICATION_JSON) @Path("storage/v1/b/{bucket}/o/{object}") @Fallback(NullOnNotFoundOr404.class) - GoogleCloudStorageObject updateObject(@PathParam("bucket") String bucketName, @PathParam("object") String objectName, - @BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate, UpdateObjectOptions options); + GoogleCloudStorageObject updateObject(@PathParam("bucket") String bucketName, + @PathParam("object") @Encoded String objectName, + @BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate, UpdateObjectOptions options); /** * Updates an object according to patch semantics @@ -315,8 +318,9 @@ public interface ObjectApi { @Produces(APPLICATION_JSON) @Path("storage/v1/b/{bucket}/o/{object}") @Fallback(NullOnNotFoundOr404.class) - GoogleCloudStorageObject patchObject(@PathParam("bucket") String bucketName, @PathParam("object") String objectName, - @BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate); + GoogleCloudStorageObject patchObject(@PathParam("bucket") String bucketName, + @PathParam("object") @Encoded String objectName, + @BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate); /** * Updates an object according to patch semantics @@ -338,8 +342,9 @@ public interface ObjectApi { @Produces(APPLICATION_JSON) @Path("storage/v1/b/{bucket}/o/{object}") @Fallback(NullOnNotFoundOr404.class) - GoogleCloudStorageObject patchObject(@PathParam("bucket") String bucketName, @PathParam("object") String objectName, - @BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate, UpdateObjectOptions options); + GoogleCloudStorageObject patchObject(@PathParam("bucket") String bucketName, + @PathParam("object") @Encoded String objectName, + @BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate, UpdateObjectOptions options); /** * Concatenates a list of existing objects into a new object in the same bucket. @@ -358,7 +363,7 @@ public interface ObjectApi { @Consumes(APPLICATION_JSON) @Path("storage/v1/b/{destinationBucket}/o/{destinationObject}/compose") GoogleCloudStorageObject composeObjects(@PathParam("destinationBucket") String destinationBucket, - @PathParam("destinationObject") String destinationObject, + @PathParam("destinationObject") @Encoded String destinationObject, @BinderParam(BindToJsonPayload.class) ComposeObjectTemplate composeObjectTemplate); /** @@ -380,7 +385,7 @@ public interface ObjectApi { @Consumes(APPLICATION_JSON) @Path("storage/v1/b/{destinationBucket}/o/{destinationObject}/compose") GoogleCloudStorageObject composeObjects(@PathParam("destinationBucket") String destinationBucket, - @PathParam("destinationObject") String destinationObject, + @PathParam("destinationObject") @Encoded String destinationObject, @BinderParam(BindToJsonPayload.class) ComposeObjectTemplate composeObjectTemplate, ComposeObjectOptions options); @@ -403,8 +408,9 @@ public interface ObjectApi { @Consumes(APPLICATION_JSON) @Path("/storage/v1/b/{sourceBucket}/o/{sourceObject}/copyTo/b/{destinationBucket}/o/{destinationObject}") GoogleCloudStorageObject copyObject(@PathParam("destinationBucket") String destinationBucket, - @PathParam("destinationObject") String destinationObject, @PathParam("sourceBucket") String sourceBucket, - @PathParam("sourceObject") String sourceObject); + @PathParam("destinationObject") @Encoded String destinationObject, + @PathParam("sourceBucket") String sourceBucket, + @PathParam("sourceObject") @Encoded String sourceObject); /** * Copies an object to a specified location with updated metadata. @@ -427,8 +433,10 @@ public interface ObjectApi { @Consumes(APPLICATION_JSON) @Path("/storage/v1/b/{sourceBucket}/o/{sourceObject}/copyTo/b/{destinationBucket}/o/{destinationObject}") GoogleCloudStorageObject copyObject(@PathParam("destinationBucket") String destinationBucket, - @PathParam("destinationObject") String destinationObject, @PathParam("sourceBucket") String sourceBucket, - @PathParam("sourceObject") String sourceObject, @BinderParam(BindToJsonPayload.class) ObjectTemplate template); + @PathParam("destinationObject") @Encoded String destinationObject, + @PathParam("sourceBucket") String sourceBucket, + @PathParam("sourceObject") @Encoded String sourceObject, + @BinderParam(BindToJsonPayload.class) ObjectTemplate template); /** * Copies an object to a specified location. Optionally overrides metadata. @@ -451,8 +459,9 @@ public interface ObjectApi { @Consumes(APPLICATION_JSON) @Path("/storage/v1/b/{sourceBucket}/o/{sourceObject}/copyTo/b/{destinationBucket}/o/{destinationObject}") GoogleCloudStorageObject copyObject(@PathParam("destinationBucket") String destinationBucket, - @PathParam("destinationObject") String destinationObject, @PathParam("sourceBucket") String sourceBucket, - @PathParam("sourceObject") String sourceObject, CopyObjectOptions options); + @PathParam("destinationObject") @Encoded String destinationObject, + @PathParam("sourceBucket") String sourceBucket, + @PathParam("sourceObject") @Encoded String sourceObject, CopyObjectOptions options); /** * Stores a new object with metadata. @@ -495,9 +504,8 @@ public interface ObjectApi { @Consumes(APPLICATION_JSON) @Path("/storage/v1/b/{sourceBucket}/o/{sourceObject}/rewriteTo/b/{destinationBucket}/o/{destinationObject}") RewriteResponse rewriteObjects(@PathParam("destinationBucket") String destinationBucket, - @PathParam("destinationObject") String destinationObject, - @PathParam("sourceBucket") String sourceBucket, - @PathParam("sourceObject") String sourceObject); + @PathParam("destinationObject") @Encoded String destinationObject, + @PathParam("sourceBucket") String sourceBucket, @PathParam("sourceObject") @Encoded String sourceObject); /** * Rewrites a source object to a destination object. @@ -520,8 +528,8 @@ public interface ObjectApi { @Consumes(APPLICATION_JSON) @Path("/storage/v1/b/{sourceBucket}/o/{sourceObject}/rewriteTo/b/{destinationBucket}/o/{destinationObject}") RewriteResponse rewriteObjects(@PathParam("destinationBucket") String destinationBucket, - @PathParam("destinationObject") String destinationObject, - @PathParam("sourceBucket") String sourceBucket, - @PathParam("sourceObject") String sourceObject, - RewriteObjectOptions options); + @PathParam("destinationObject") @Encoded String destinationObject, + @PathParam("sourceBucket") String sourceBucket, + @PathParam("sourceObject") @Encoded String sourceObject, + RewriteObjectOptions options); } diff --git a/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/blobstore/integration/GoogleCloudStorageBlobIntegrationLiveTest.java b/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/blobstore/integration/GoogleCloudStorageBlobIntegrationLiveTest.java index 162c45a74d..db1bcd8da5 100644 --- a/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/blobstore/integration/GoogleCloudStorageBlobIntegrationLiveTest.java +++ b/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/blobstore/integration/GoogleCloudStorageBlobIntegrationLiveTest.java @@ -143,6 +143,7 @@ public class GoogleCloudStorageBlobIntegrationLiveTest extends BaseBlobIntegrati return new Object[][] { { "file.xml", "text/xml", file, realObject }, { "string.xml", "text/xml", realObject, realObject }, + { "stringwith/slash.xml", "text/xml", realObject, realObject }, { "bytes.xml", "application/octet-stream", realObject.getBytes(), realObject } }; } diff --git a/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/features/ObjectApiMockTest.java b/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/features/ObjectApiMockTest.java index 9906a90af9..35e98279f1 100644 --- a/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/features/ObjectApiMockTest.java +++ b/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/features/ObjectApiMockTest.java @@ -40,6 +40,7 @@ import org.jclouds.googlecloudstorage.parse.ParseGoogleCloudStorageObjectListTes import org.jclouds.googlecloudstorage.parse.ParseObjectRewriteResponse; import org.jclouds.http.internal.PayloadEnclosingImpl; import org.jclouds.io.PayloadEnclosing; +import org.jclouds.util.Strings2; import org.testng.annotations.Test; import com.google.common.net.MediaType; @@ -57,6 +58,13 @@ public class ObjectApiMockTest extends BaseGoogleCloudStorageApiMockTest { assertSent(server, "GET", "/storage/v1/b/test/o/file_name", null); } + public void existsEncoded() throws Exception { + server.enqueue(jsonResponse("/object_encoded_get.json")); + + assertTrue(objectApi().objectExists("test", Strings2.urlEncode("dir/file name"))); + assertSent(server, "GET", "/storage/v1/b/test/o/dir%2Ffile%20name", null); + } + public void exists_4xx() throws Exception { server.enqueue(response404()); @@ -120,6 +128,14 @@ public class ObjectApiMockTest extends BaseGoogleCloudStorageApiMockTest { assertSent(server, "DELETE", "/storage/v1/b/test/o/object_name", null); } + public void delete_encoded() throws Exception { + server.enqueue(new MockResponse()); + + // TODO: Should this be returning True on not found? + assertTrue(objectApi().deleteObject("test", Strings2.urlEncode("dir/object name"))); + assertSent(server, "DELETE", "/storage/v1/b/test/o/dir%2Fobject%20name", null); + } + public void list() throws Exception { server.enqueue(jsonResponse("/object_list.json")); diff --git a/providers/google-cloud-storage/src/test/resources/object_encoded_get.json b/providers/google-cloud-storage/src/test/resources/object_encoded_get.json new file mode 100644 index 0000000000..7fdb843f7d --- /dev/null +++ b/providers/google-cloud-storage/src/test/resources/object_encoded_get.json @@ -0,0 +1,21 @@ +{ + "kind": "storage#object", + "id": "test/dir%2Ffile%20name/1000", + "selfLink": "https://www.googleapis.com/storage/v1/b/test/o/dir%2Ffile%20name", + "name": "dir%2Ffile%20name", + "bucket": "test", + "generation": "1000", + "metageneration": "8", + "contentType": "application/x-tar", + "updated": "2014-09-27T00:01:44.819", + "storageClass": "STANDARD", + "size": "1000", + "md5Hash": "md5Hash", + "mediaLink": "https://www.googleapis.com/download/storage/v1/b/test/o/dir%2Ffile%20name?generation=1000&alt=media", + "owner": { + "entity": "entity", + "entityId": "entityId" + }, + "crc32c": "crc32c", + "etag": "etag" +}