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.
This commit is contained in:
Timur Alperovich 2015-09-26 15:16:13 -07:00 committed by Ignasi Barrera
parent 06125793d2
commit 2385ba901e
6 changed files with 114 additions and 77 deletions

View File

@ -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<MultipartPart> parts) {
ImmutableList.Builder<GoogleCloudStorageObject> 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?
}

View File

@ -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 <a href = " https://developers.google.com/storage/docs/json_api/v1/objectAccessControls "/>
*/
@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<ObjectAccessControls> 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<ObjectAccessControls> 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);
}

View File

@ -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 <a href="https://developers.google.com/storage/docs/json_api/v1/objects"/>
*/
@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,7 +272,8 @@ 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,
GoogleCloudStorageObject updateObject(@PathParam("bucket") String bucketName,
@PathParam("object") @Encoded String objectName,
@BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate);
/**
@ -294,7 +296,8 @@ 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,
GoogleCloudStorageObject updateObject(@PathParam("bucket") String bucketName,
@PathParam("object") @Encoded String objectName,
@BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate, UpdateObjectOptions options);
/**
@ -315,7 +318,8 @@ 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,
GoogleCloudStorageObject patchObject(@PathParam("bucket") String bucketName,
@PathParam("object") @Encoded String objectName,
@BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate);
/**
@ -338,7 +342,8 @@ 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,
GoogleCloudStorageObject patchObject(@PathParam("bucket") String bucketName,
@PathParam("object") @Encoded String objectName,
@BinderParam(BindToJsonPayload.class) ObjectTemplate objectTemplate, UpdateObjectOptions options);
/**
@ -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("destinationObject") @Encoded String destinationObject,
@PathParam("sourceBucket") String sourceBucket,
@PathParam("sourceObject") String sourceObject,
@PathParam("sourceObject") @Encoded String sourceObject,
RewriteObjectOptions options);
}

View File

@ -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 } };
}

View File

@ -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"));

View File

@ -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"
}