mirror of https://github.com/apache/jclouds.git
Issue 773. Support S3 Multi-Object Delete (API)
This commit is contained in:
parent
65c08cbb54
commit
bd76d17642
|
@ -80,7 +80,7 @@ public class RequestAuthorizeSignature implements HttpRequestFilter, RequestSign
|
|||
private static final Set<String> SIGNED_PARAMETERS = ImmutableSet.of("acl", "torrent", "logging", "location", "policy",
|
||||
"requestPayment", "versioning", "versions", "versionId", "notification", "uploadId", "uploads",
|
||||
"partNumber", "website", "response-content-type", "response-content-language", "response-expires",
|
||||
"response-cache-control", "response-content-disposition", "response-content-encoding");
|
||||
"response-cache-control", "response-content-disposition", "response-content-encoding", "delete");
|
||||
|
||||
private final SignatureWire signatureWire;
|
||||
private final String accessKey;
|
||||
|
|
|
@ -29,8 +29,11 @@ import javax.ws.rs.Path;
|
|||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
|
||||
import org.jclouds.aws.s3.binders.BindIterableAsPayloadToDeleteRequest;
|
||||
import org.jclouds.aws.s3.binders.BindObjectMetadataToRequest;
|
||||
import org.jclouds.aws.s3.binders.BindPartIdsAndETagsToRequest;
|
||||
import org.jclouds.aws.s3.domain.DeleteResult;
|
||||
import org.jclouds.aws.s3.xml.DeleteResultHandler;
|
||||
import org.jclouds.aws.s3.functions.ETagFromHttpResponseViaRegex;
|
||||
import org.jclouds.aws.s3.functions.ObjectMetadataKey;
|
||||
import org.jclouds.aws.s3.functions.UploadIdFromHttpResponseViaRegex;
|
||||
|
@ -46,6 +49,7 @@ 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.annotations.XMLResponseParser;
|
||||
import org.jclouds.rest.functions.ReturnVoidOnNotFoundOr404;
|
||||
import org.jclouds.s3.Bucket;
|
||||
import org.jclouds.s3.S3AsyncClient;
|
||||
|
@ -112,4 +116,15 @@ public interface AWSS3AsyncClient extends S3AsyncClient {
|
|||
@PathParam("key") String key, @QueryParam("uploadId") String uploadId,
|
||||
@BinderParam(BindPartIdsAndETagsToRequest.class) Map<Integer, String> parts);
|
||||
|
||||
/**
|
||||
* @see AWSS3Client#deleteObjects
|
||||
*/
|
||||
@POST
|
||||
@Path("/")
|
||||
@QueryParams(keys = "delete")
|
||||
@XMLResponseParser(DeleteResultHandler.class)
|
||||
ListenableFuture<DeleteResult> deleteObjects(
|
||||
@Bucket @EndpointParam(parser = AssignCorrectHostnameForBucket.class) @BinderParam(BindAsHostPrefixIfConfigured.class) @ParamValidators(BucketNameValidator.class) String bucketName,
|
||||
@BinderParam(BindIterableAsPayloadToDeleteRequest.class) Iterable<String> keys);
|
||||
|
||||
}
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
package org.jclouds.aws.s3;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.jclouds.aws.s3.domain.DeleteResult;
|
||||
import org.jclouds.concurrent.Timeout;
|
||||
import org.jclouds.io.Payload;
|
||||
import org.jclouds.s3.S3Client;
|
||||
|
@ -143,4 +145,26 @@ public interface AWSS3Client extends S3Client {
|
|||
* @return ETag of the content uploaded
|
||||
*/
|
||||
String completeMultipartUpload(String bucketName, String key, String uploadId, Map<Integer, String> parts);
|
||||
|
||||
/**
|
||||
* The Multi-Object Delete operation enables you to delete multiple objects from a bucket using a
|
||||
* single HTTP request. If you know the object keys that you want to delete, then this operation
|
||||
* provides a suitable alternative to sending individual delete requests (see DELETE Object),
|
||||
* reducing per-request overhead.
|
||||
*
|
||||
* The Multi-Object Delete request contains a set of up to 1000 keys that you want to delete.
|
||||
*
|
||||
* If a key does not exist is considered to be deleted.
|
||||
*
|
||||
* The Multi-Object Delete operation supports two modes for the response; verbose and quiet.
|
||||
* By default, the operation uses verbose mode in which the response includes the result of
|
||||
* deletion of each key in your request.
|
||||
*
|
||||
* @see <a href="http://docs.amazonwebservices.com/AmazonS3/latest/API/multiobjectdeleteapi.html" />
|
||||
* @param bucketName
|
||||
* namespace of the objects you are deleting
|
||||
* @param keys
|
||||
* set of unique keys identifying objects
|
||||
*/
|
||||
DeleteResult deleteObjects(String bucketName, Iterable<String> keys);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Licensed to jclouds, Inc. (jclouds) under one or more
|
||||
* contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. jclouds 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.aws.s3.binders;
|
||||
|
||||
import org.jclouds.crypto.CryptoStreams;
|
||||
import org.jclouds.http.HttpRequest;
|
||||
import org.jclouds.io.Payload;
|
||||
import org.jclouds.io.Payloads;
|
||||
import org.jclouds.rest.Binder;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* @author Andrei Savu
|
||||
*/
|
||||
public class BindIterableAsPayloadToDeleteRequest implements Binder {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <R extends HttpRequest> R bindToRequest(R request, Object input) {
|
||||
checkArgument(checkNotNull(input, "input is null") instanceof Iterable,
|
||||
"this binder is only valid for an Iterable");
|
||||
checkNotNull(request, "request is null");
|
||||
|
||||
Iterable<String> keys = (Iterable<String>) input;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String key : keys) {
|
||||
builder.append(String.format("<Object><Key>%s</Key></Object>", key));
|
||||
}
|
||||
|
||||
final String objects = builder.toString();
|
||||
checkArgument(!objects.isEmpty(), "The list of keys should not be empty.");
|
||||
|
||||
final String content = String.format("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<Delete>%s</Delete>", objects);
|
||||
|
||||
Payload payload = Payloads.newStringPayload(content);
|
||||
payload.getContentMetadata().setContentType(MediaType.TEXT_XML);
|
||||
payload.getContentMetadata().setContentMD5(CryptoStreams.md5(content.getBytes()));
|
||||
|
||||
request.setPayload(payload);
|
||||
return request;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/**
|
||||
* Licensed to jclouds, Inc. (jclouds) under one or more
|
||||
* contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. jclouds 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.aws.s3.domain;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.ForwardingSet;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* Multi-object delete API response
|
||||
* <p/>
|
||||
* Contains a list of the keys that were deleted
|
||||
*
|
||||
* @author Andrei Savu
|
||||
* @see <a href="http://docs.amazonwebservices.com/AmazonS3/latest/API/multiobjectdeleteapi.html" />
|
||||
*/
|
||||
public class DeleteResult extends ForwardingSet<String> {
|
||||
|
||||
public static class Error {
|
||||
|
||||
private final String code;
|
||||
private final String message;
|
||||
|
||||
public Error(String code, String message) {
|
||||
this.code = checkNotNull(code, "code is null");
|
||||
this.message = checkNotNull(message, "message is null");
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Error)) return false;
|
||||
|
||||
Error that = (Error) o;
|
||||
|
||||
return Objects.equal(code, that.code)
|
||||
&& Objects.equal(message, that.message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(code, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Objects.toStringHelper(this).omitNullValues()
|
||||
.add("code", code).add("message", message).toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public Builder toBuilder() {
|
||||
return builder().fromDeleteResult(this);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private ImmutableSet.Builder<String> deleted = ImmutableSet.builder();
|
||||
private ImmutableMap.Builder<String, Error> errors = ImmutableMap.builder();
|
||||
|
||||
/**
|
||||
* @see DeleteResult#getErrors
|
||||
*/
|
||||
public Builder putError(String key, Error error) {
|
||||
this.errors.put(key, error);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DeleteResult#getErrors
|
||||
*/
|
||||
public Builder errors(Map<String, Error> errors) {
|
||||
this.errors = ImmutableMap.<String, Error>builder().putAll(errors);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DeleteResult#getDeleted
|
||||
*/
|
||||
public Builder deleted(Iterable<String> deleted) {
|
||||
this.deleted = ImmutableSet.<String>builder().addAll(deleted);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DeleteResult#getDeleted
|
||||
*/
|
||||
public Builder add(String key) {
|
||||
this.deleted.add(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DeleteResult#getDeleted
|
||||
*/
|
||||
public Builder addAll(Iterable<String> key) {
|
||||
this.deleted.addAll(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DeleteResult build() {
|
||||
return new DeleteResult(deleted.build(), errors.build());
|
||||
}
|
||||
|
||||
public Builder fromDeleteResult(DeleteResult result) {
|
||||
return addAll(result.getDeleted()).errors(result.getErrors());
|
||||
}
|
||||
}
|
||||
|
||||
private final Set<String> deleted;
|
||||
private final Map<String, Error> errors;
|
||||
|
||||
public DeleteResult(Set<String> deleted, Map<String, Error> errors) {
|
||||
this.deleted = ImmutableSet.copyOf(deleted);
|
||||
this.errors = ImmutableMap.copyOf(errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of successfully deleted keys
|
||||
*/
|
||||
public Set<String> getDeleted() {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map with details about failed delete operations indexed by object name
|
||||
*/
|
||||
public Map<String, Error> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<String> delegate() {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof DeleteResult)) return false;
|
||||
|
||||
DeleteResult that = (DeleteResult) o;
|
||||
|
||||
return Objects.equal(errors, that.errors)
|
||||
&& Objects.equal(deleted, that.deleted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(deleted, errors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Objects.toStringHelper(this).omitNullValues()
|
||||
.add("deleted", deleted).add("errors", errors).toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* Licensed to jclouds, Inc. (jclouds) under one or more
|
||||
* contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. jclouds 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.aws.s3.xml;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.jclouds.aws.s3.domain.DeleteResult;
|
||||
import org.jclouds.http.functions.ParseSax;
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import static org.jclouds.util.SaxUtils.equalsOrSuffix;
|
||||
|
||||
/**
|
||||
* @author Andrei Savu
|
||||
*/
|
||||
public class DeleteResultHandler extends ParseSax.HandlerForGeneratedRequestWithResult<DeleteResult> {
|
||||
|
||||
public static final String DELETED_TAG = "Deleted";
|
||||
public static final String ERROR_TAG = "Error";
|
||||
|
||||
private final ErrorEntryHandler errorEntryHandler = new ErrorEntryHandler();
|
||||
|
||||
private StringBuilder deletedEntryAccumulator = new StringBuilder();
|
||||
|
||||
/**
|
||||
* Accumulator for the set of successfully deleted files
|
||||
*/
|
||||
private final ImmutableSet.Builder<String> deleted = ImmutableSet.builder();
|
||||
|
||||
/**
|
||||
* Accumulator for the set of errors
|
||||
*/
|
||||
private final ImmutableMap.Builder<String, DeleteResult.Error> errors = ImmutableMap.builder();
|
||||
|
||||
private boolean parsingDeletedEntry = false;
|
||||
private boolean parsingErrorEntry = false;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void startElement(String uri, String name, String qName, Attributes attributes)
|
||||
throws SAXException {
|
||||
if (equalsOrSuffix(qName, DELETED_TAG)) {
|
||||
parsingDeletedEntry = true;
|
||||
} else if (equalsOrSuffix(qName, ERROR_TAG)) {
|
||||
parsingErrorEntry = true;
|
||||
}
|
||||
|
||||
if (parsingDeletedEntry) {
|
||||
deletedEntryAccumulator = new StringBuilder();
|
||||
} else if (parsingErrorEntry) {
|
||||
errorEntryHandler.startElement(uri, name, qName, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void characters(char[] chars, int start, int length) throws SAXException {
|
||||
if (parsingDeletedEntry) {
|
||||
deletedEntryAccumulator.append(chars, start, length);
|
||||
} else if (parsingErrorEntry) {
|
||||
errorEntryHandler.characters(chars, start, length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void endElement(String uri, String name, String qName) throws SAXException {
|
||||
if (equalsOrSuffix(qName, DELETED_TAG)) {
|
||||
parsingDeletedEntry = false;
|
||||
deleted.add(deletedEntryAccumulator.toString().trim());
|
||||
} else if (equalsOrSuffix(qName, ERROR_TAG)) {
|
||||
parsingErrorEntry = false;
|
||||
errors.put(errorEntryHandler.getResult());
|
||||
}
|
||||
|
||||
if (parsingErrorEntry) {
|
||||
errorEntryHandler.endElement(uri, name, qName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public DeleteResult getResult() {
|
||||
return new DeleteResult(deleted.build(), errors.build());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package org.jclouds.aws.s3.xml;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import org.jclouds.aws.s3.domain.DeleteResult;
|
||||
import org.jclouds.http.functions.ParseSax;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jclouds.util.SaxUtils.equalsOrSuffix;
|
||||
|
||||
/**
|
||||
* @author Andrei Savu
|
||||
*/
|
||||
public class ErrorEntryHandler extends ParseSax.HandlerForGeneratedRequestWithResult<Map.Entry<String, DeleteResult.Error>> {
|
||||
|
||||
private StringBuilder accumulator = new StringBuilder();
|
||||
|
||||
private String key;
|
||||
private String code;
|
||||
private String message;
|
||||
|
||||
@Override
|
||||
public void characters(char[] chars, int start, int length) throws SAXException {
|
||||
accumulator.append(chars, start, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endElement(String uri, String name, String qName) throws SAXException {
|
||||
if (equalsOrSuffix(qName, "Key")) {
|
||||
key = accumulator.toString().trim();
|
||||
} else if (equalsOrSuffix(qName, "Code")) {
|
||||
code = accumulator.toString().trim();
|
||||
} else if (equalsOrSuffix(qName, "Message")) {
|
||||
message = accumulator.toString().trim();
|
||||
}
|
||||
accumulator = new StringBuilder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map.Entry<String, DeleteResult.Error> getResult() {
|
||||
return Maps.immutableEntry(key, new DeleteResult.Error(code, message));
|
||||
}
|
||||
}
|
|
@ -19,12 +19,20 @@
|
|||
package org.jclouds.aws.s3;
|
||||
|
||||
import static org.jclouds.aws.s3.blobstore.options.AWSS3PutObjectOptions.Builder.storageClass;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
import static org.testng.Assert.assertNotNull;
|
||||
import static org.testng.Assert.assertTrue;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.jclouds.aws.s3.domain.DeleteResult;
|
||||
import org.jclouds.aws.s3.internal.BaseAWSS3ClientExpectTest;
|
||||
import org.jclouds.blobstore.domain.Blob;
|
||||
import org.jclouds.blobstore.domain.BlobBuilder;
|
||||
import org.jclouds.crypto.CryptoStreams;
|
||||
import org.jclouds.http.HttpRequest;
|
||||
import org.jclouds.http.HttpResponse;
|
||||
import org.jclouds.io.Payload;
|
||||
import org.jclouds.io.Payloads;
|
||||
import org.jclouds.s3.blobstore.functions.BlobToObject;
|
||||
import org.jclouds.s3.domain.ObjectMetadata.StorageClass;
|
||||
import org.testng.annotations.Test;
|
||||
|
@ -38,6 +46,7 @@ import com.google.inject.Injector;
|
|||
*/
|
||||
@Test
|
||||
public class AWSS3ClientExpectTest extends BaseAWSS3ClientExpectTest {
|
||||
|
||||
HttpRequest bucketLocationRequest = HttpRequest.builder()
|
||||
.method("GET")
|
||||
.endpoint("https://test.s3.amazonaws.com/?location")
|
||||
|
@ -52,6 +61,7 @@ public class AWSS3ClientExpectTest extends BaseAWSS3ClientExpectTest {
|
|||
.addHeader("x-amz-request-id", "51BF4F45D49B1B34")
|
||||
.addHeader("Date", CONSTANT_DATE)
|
||||
.addHeader("Server", "AmazonS3").build();
|
||||
|
||||
@Test
|
||||
public void testPutWithReducedRedundancy() {
|
||||
Injector injector = createInjector(Functions.forMap(ImmutableMap.<HttpRequest, HttpResponse>of()), createModule(), setupProperties());
|
||||
|
@ -80,4 +90,62 @@ public class AWSS3ClientExpectTest extends BaseAWSS3ClientExpectTest {
|
|||
client.putObject("test", blobToObject.apply(blob),
|
||||
storageClass(StorageClass.REDUCED_REDUNDANCY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteMultipleObjects() {
|
||||
final String request = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<Delete>" +
|
||||
"<Object><Key>key1</Key></Object>" +
|
||||
"<Object><Key>key2</Key></Object>" +
|
||||
"</Delete>";
|
||||
|
||||
final Payload requestPayload = Payloads.newStringPayload(request);
|
||||
requestPayload.getContentMetadata().setContentType("text/xml");
|
||||
requestPayload.getContentMetadata().setContentMD5(CryptoStreams.md5(request.getBytes()));
|
||||
|
||||
final String response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<DeleteResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\n" +
|
||||
" <Deleted>\n" +
|
||||
" <Key>key1</Key>\n" +
|
||||
" </Deleted>\n" +
|
||||
" <Deleted>\n" +
|
||||
" <Key>key1.1</Key>\n" +
|
||||
" </Deleted>\n" +
|
||||
" <Error>\n" +
|
||||
" <Key>key2</Key>\n" +
|
||||
" <Code>AccessDenied</Code>\n" +
|
||||
" <Message>Access Denied</Message>\n" +
|
||||
" </Error>\n" +
|
||||
"</DeleteResult>";
|
||||
|
||||
final Payload responsePayload = Payloads.newStringPayload(response);
|
||||
responsePayload.getContentMetadata().setContentType("text/xml");
|
||||
|
||||
AWSS3Client client = requestsSendResponses(bucketLocationRequest, bucketLocationResponse,
|
||||
HttpRequest.builder()
|
||||
.method("POST")
|
||||
.endpoint("https://test.s3-eu-west-1.amazonaws.com/?delete")
|
||||
.addHeader("Host", "test.s3-eu-west-1.amazonaws.com")
|
||||
.addHeader("Date", CONSTANT_DATE)
|
||||
.addHeader("Authorization", "AWS identity:/k3HQNVVyAQMsr9qhx6hajocVu4=")
|
||||
.payload(requestPayload)
|
||||
.build(),
|
||||
HttpResponse.builder()
|
||||
.statusCode(200)
|
||||
.addHeader("x-amz-request-id", "7A84C3CD4437A4C0")
|
||||
.addHeader("Date", CONSTANT_DATE)
|
||||
.addHeader("ETag", "437b930db84b8079c2dd804a71936b5f")
|
||||
.addHeader("Server", "AmazonS3")
|
||||
.payload(responsePayload)
|
||||
.build()
|
||||
);
|
||||
|
||||
DeleteResult result = client.deleteObjects("test", ImmutableSet.of("key1", "key2"));
|
||||
assertNotNull(result, "result is null");
|
||||
|
||||
assertEquals(result.getDeleted(), ImmutableSet.of("key1", "key1.1"));
|
||||
assertEquals(result.getErrors().size(), 1);
|
||||
|
||||
assertEquals(result.getErrors().get("key2"), new DeleteResult.Error("AccessDenied", "Access Denied"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import static org.jclouds.io.Payloads.newByteArrayPayload;
|
|||
import static org.jclouds.s3.options.ListBucketOptions.Builder.withPrefix;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
import static org.testng.Assert.assertNotNull;
|
||||
import static org.testng.Assert.assertTrue;
|
||||
import static org.testng.Assert.fail;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
@ -34,10 +35,13 @@ import java.io.File;
|
|||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import org.jclouds.aws.AWSResponseException;
|
||||
import org.jclouds.aws.domain.Region;
|
||||
import org.jclouds.aws.s3.domain.DeleteResult;
|
||||
import org.jclouds.blobstore.AsyncBlobStore;
|
||||
import org.jclouds.blobstore.BlobStore;
|
||||
import org.jclouds.blobstore.KeyNotFoundException;
|
||||
|
@ -260,4 +264,32 @@ public class AWSS3ClientLiveTest extends S3ClientLiveTest {
|
|||
}
|
||||
}
|
||||
|
||||
public void testDeleteMultipleObjects() throws InterruptedException {
|
||||
String container = getContainerName();
|
||||
try {
|
||||
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
Blob blob = view.getBlobStore().blobBuilder(key).payload("").build();
|
||||
view.getBlobStore().putBlob(container, blob);
|
||||
|
||||
builder.add(key);
|
||||
}
|
||||
|
||||
Set<String> keys = builder.build();
|
||||
DeleteResult result = getApi().deleteObjects(container, keys);
|
||||
|
||||
assertTrue(result.getDeleted().containsAll(keys));
|
||||
assertEquals(result.getErrors().size(), 0);
|
||||
|
||||
for (String key : keys) {
|
||||
assertConsistencyAwareBlobDoesntExist(container, key);
|
||||
}
|
||||
|
||||
} finally {
|
||||
returnContainer(container);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Licensed to jclouds, Inc. (jclouds) under one or more
|
||||
* contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. jclouds 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.aws.s3.binders;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.jclouds.http.HttpRequest;
|
||||
import org.jclouds.io.Payload;
|
||||
import org.jclouds.io.Payloads;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import static org.testng.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* @author Andrei Savu
|
||||
*/
|
||||
public class BindIterableAsPayloadToDeleteRequestTest {
|
||||
|
||||
private final BindIterableAsPayloadToDeleteRequest binder = new BindIterableAsPayloadToDeleteRequest();
|
||||
private final HttpRequest request = HttpRequest.builder().method("POST").endpoint("http://localhost/").build();
|
||||
|
||||
@Test
|
||||
public void testWithASmallSet() {
|
||||
HttpRequest result = binder.bindToRequest(request, ImmutableSet.of("key1", "key2"));
|
||||
|
||||
Payload payload = Payloads
|
||||
.newStringPayload("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Delete>" +
|
||||
"<Object><Key>key1</Key></Object><Object><Key>key2</Key></Object></Delete>");
|
||||
payload.getContentMetadata().setContentType(MediaType.TEXT_XML);
|
||||
|
||||
assertEquals(result.getPayload(), payload);
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = IllegalArgumentException.class)
|
||||
public void testEmptySetThrowsException() {
|
||||
binder.bindToRequest(request, ImmutableSet.of());
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = NullPointerException.class)
|
||||
public void testFailsOnNullSet() {
|
||||
binder.bindToRequest(request, null);
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = IllegalArgumentException.class)
|
||||
public void testExpectedASetInstance() {
|
||||
binder.bindToRequest(request, ImmutableList.of());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Licensed to jclouds, Inc. (jclouds) under one or more
|
||||
* contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. jclouds 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.aws.s3.xml;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.jclouds.aws.s3.domain.DeleteResult;
|
||||
import org.jclouds.http.functions.BaseHandlerTest;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import static org.testng.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* @author Andrei Savu
|
||||
*/
|
||||
// NOTE:without testName, this will not call @Before* and fail w/NPE during surefire
|
||||
@Test(groups = "unit", testName = "DeleteResultHandlerTest")
|
||||
public class DeleteResultHandlerTest extends BaseHandlerTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
InputStream is = getClass().getResourceAsStream("/delete-result.xml");
|
||||
|
||||
DeleteResult expected = expected();
|
||||
|
||||
DeleteResultHandler handler = injector.getInstance(DeleteResultHandler.class);
|
||||
DeleteResult result = factory.create(handler).parse(is);
|
||||
|
||||
assertEquals(result.toString(), expected.toString());
|
||||
}
|
||||
|
||||
private DeleteResult expected() {
|
||||
return DeleteResult.builder()
|
||||
.add("key1")
|
||||
.add("key1.1")
|
||||
.putError("key2", new DeleteResult.Error("AccessDenied", "Access Denied"))
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||
<Deleted>
|
||||
<Key>key1</Key>
|
||||
</Deleted>
|
||||
<Deleted>
|
||||
<Key>key1.1</Key>
|
||||
</Deleted>
|
||||
<Error>
|
||||
<Key>key2</Key>
|
||||
<Code>AccessDenied</Code>
|
||||
<Message>Access Denied</Message>
|
||||
</Error>
|
||||
</DeleteResult>
|
Loading…
Reference in New Issue