Repository: jclouds Updated Branches: refs/heads/master a697396e8 -> 2bd055011
Add support for Swift conditional copy Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/2bd05501 Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/2bd05501 Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/2bd05501 Branch: refs/heads/master Commit: 2bd0550110acca8e5b2e9026f0eb6c9c4d4f34b4 Parents: a697396 Author: Andrew Gaul <[email protected]> Authored: Mon Feb 8 20:53:28 2016 -0800 Committer: Andrew Gaul <[email protected]> Committed: Tue Feb 9 16:34:48 2016 -0800 ---------------------------------------------------------------------- .../openstack/swift/v1/features/ObjectApi.java | 96 ++++++++++++++++++++ .../openstack/swift/v1/options/CopyOptions.java | 48 ++++++++++ .../swift/v1/features/ObjectApiLiveTest.java | 60 ++++++++++++ .../swift/v1/features/ObjectApiMockTest.java | 26 ++++++ 4 files changed, 230 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/jclouds/blob/2bd05501/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java ---------------------------------------------------------------------- diff --git a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java index 335dfa6..f56a8f5 100644 --- a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java +++ b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java @@ -51,6 +51,7 @@ import org.jclouds.openstack.swift.v1.domain.SwiftObject; import org.jclouds.openstack.swift.v1.functions.ETagHeader; import org.jclouds.openstack.swift.v1.functions.ParseObjectFromResponse; import org.jclouds.openstack.swift.v1.functions.ParseObjectListFromResponse; +import org.jclouds.openstack.swift.v1.options.CopyOptions; import org.jclouds.openstack.swift.v1.options.ListContainerOptions; import org.jclouds.openstack.swift.v1.options.PutOptions; import org.jclouds.rest.annotations.BinderParam; @@ -264,8 +265,10 @@ public interface ObjectApi { * @param sourceObject * the source object name. * + * @deprecated call copy(String, String, String, CopyOptions) instead * @throws KeyNotFoundException if the source or destination container do not exist. */ + @Deprecated @Named("object:copy") @PUT @Path("/{destinationObject}") @@ -275,6 +278,32 @@ public interface ObjectApi { @PathParam("sourceObject") String sourceObject); /** + * Copies an object from one container to another. + * + * <h3>NOTE</h3> + * This is a server side copy. + * + * @param destinationObject + * the destination object name. + * @param sourceContainer + * the source container name. + * @param sourceObject + * the source object name. + * @param options + * conditional copy + * + * @throws KeyNotFoundException if the source or destination container do not exist. + */ + @Named("object:copy") + @PUT + @Path("/{destinationObject}") + @Headers(keys = OBJECT_COPY_FROM, values = "/{sourceContainer}/{sourceObject}") + void copy(@PathParam("destinationObject") String destinationObject, + @PathParam("sourceContainer") String sourceContainer, + @PathParam("sourceObject") String sourceObject, + CopyOptions options); + + /** * Copies an object from one container to another, replacing metadata. * * <h3>NOTE</h3> @@ -291,8 +320,10 @@ public interface ObjectApi { * @param objectMetadata * Unprefixed/unescaped metadata, such as Content-Disposition * + * @deprecated call copy(String, String, String, Map, Map, CopyOptions) instead * @throws KeyNotFoundException if the source or destination container do not exist. */ + @Deprecated @Named("object:copy") @PUT @Path("/{destinationObject}") @@ -303,6 +334,37 @@ public interface ObjectApi { @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> userMetadata, @BinderParam(BindToHeaders.class) Map<String, String> objectMetadata); + /** + * Copies an object from one container to another, replacing metadata. + * + * <h3>NOTE</h3> + * This is a server side copy. + * + * @param destinationObject + * the destination object name. + * @param sourceContainer + * the source container name. + * @param sourceObject + * the source object name. + * @param userMetadata + * Freeform metadata for the object, automatically prefixed/escaped + * @param objectMetadata + * Unprefixed/unescaped metadata, such as Content-Disposition + * @param options + * conditional copy + * + * @throws KeyNotFoundException if the source or destination container do not exist. + */ + @Named("object:copy") + @PUT + @Path("/{destinationObject}") + @Headers(keys = {OBJECT_COPY_FROM, OBJECT_COPY_FRESH_METADATA}, values = {"/{sourceContainer}/{sourceObject}", "True"}) + void copy(@PathParam("destinationObject") String destinationObject, + @PathParam("sourceContainer") String sourceContainer, + @PathParam("sourceObject") String sourceObject, + @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> userMetadata, + @BinderParam(BindToHeaders.class) Map<String, String> objectMetadata, + CopyOptions options); /** * Copies an object from one container to another, appending metadata. @@ -321,8 +383,10 @@ public interface ObjectApi { * @param objectMetadata * Unprefixed/unescaped metadata, such as Content-Disposition * + * @deprecated call copyAppendMetadata(String, String, String, Map, Map, CopyOptions) instead * @throws KeyNotFoundException if the source or destination container do not exist. */ + @Deprecated @Named("object:copy") @PUT @Path("/{destinationObject}") @@ -332,4 +396,36 @@ public interface ObjectApi { @PathParam("sourceObject") String sourceObject, @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> userMetadata, @BinderParam(BindToHeaders.class) Map<String, String> objectMetadata); + + /** + * Copies an object from one container to another, appending metadata. + * + * <h3>NOTE</h3> + * This is a server side copy. + * + * @param destinationObject + * the destination object name. + * @param sourceContainer + * the source container name. + * @param sourceObject + * the source object name. + * @param userMetadata + * Freeform metadata for the object, automatically prefixed/escaped + * @param objectMetadata + * Unprefixed/unescaped metadata, such as Content-Disposition + * @param options + * conditional copy + * + * @throws KeyNotFoundException if the source or destination container do not exist. + */ + @Named("object:copy") + @PUT + @Path("/{destinationObject}") + @Headers(keys = OBJECT_COPY_FROM, values = "/{sourceContainer}/{sourceObject}") + void copyAppendMetadata(@PathParam("destinationObject") String destinationObject, + @PathParam("sourceContainer") String sourceContainer, + @PathParam("sourceObject") String sourceObject, + @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> userMetadata, + @BinderParam(BindToHeaders.class) Map<String, String> objectMetadata, + CopyOptions options); } http://git-wip-us.apache.org/repos/asf/jclouds/blob/2bd05501/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/options/CopyOptions.java ---------------------------------------------------------------------- diff --git a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/options/CopyOptions.java b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/options/CopyOptions.java new file mode 100644 index 0000000..f7c35b78 --- /dev/null +++ b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/options/CopyOptions.java @@ -0,0 +1,48 @@ +/* + * 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.openstack.swift.v1.options; + +import java.util.Date; + +import org.jclouds.date.DateService; +import org.jclouds.date.internal.SimpleDateFormatDateService; +import org.jclouds.http.options.BaseHttpRequestOptions; + +import com.google.common.net.HttpHeaders; + +public final class CopyOptions extends BaseHttpRequestOptions { + public static final CopyOptions NONE = new CopyOptions(); + + private static final DateService dateService = new SimpleDateFormatDateService(); + + public CopyOptions ifMatch(String ifMatch) { + this.headers.put(HttpHeaders.IF_MATCH, ifMatch); + return this; + } + + // Swift only supports If-None-Match: * which is not useful for copy + + public CopyOptions ifModifiedSince(Date ifModifiedSince) { + this.headers.put(HttpHeaders.IF_MODIFIED_SINCE, dateService.rfc822DateFormat(ifModifiedSince)); + return this; + } + + public CopyOptions ifUnmodifiedSince(Date ifUnmodifiedSince) { + this.headers.put(HttpHeaders.IF_UNMODIFIED_SINCE, dateService.rfc822DateFormat(ifUnmodifiedSince)); + return this; + } +} http://git-wip-us.apache.org/repos/asf/jclouds/blob/2bd05501/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java ---------------------------------------------------------------------- diff --git a/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java b/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java index dc5b4c1..33d9ac7 100644 --- a/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java +++ b/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java @@ -33,13 +33,16 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; +import org.assertj.core.api.Fail; import org.jclouds.blobstore.KeyNotFoundException; +import org.jclouds.http.HttpResponseException; import org.jclouds.http.options.GetOptions; import org.jclouds.io.Payload; import org.jclouds.openstack.swift.v1.SwiftApi; import org.jclouds.openstack.swift.v1.domain.ObjectList; import org.jclouds.openstack.swift.v1.domain.SwiftObject; import org.jclouds.openstack.swift.v1.internal.BaseSwiftApiLiveTest; +import org.jclouds.openstack.swift.v1.options.CopyOptions; import org.jclouds.openstack.swift.v1.options.ListContainerOptions; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -245,6 +248,63 @@ public class ObjectApiLiveTest extends BaseSwiftApiLiveTest<SwiftApi> { } } + public void testCopyObjectConditional() throws Exception { + for (String regionId : regions) { + // source + String sourceContainer = "src" + containerName; + String sourceObjectName = "original.txt"; + String badSource = "badSource"; + + // destination + String destinationContainer = "dest" + containerName; + String destinationObject = "copy.txt"; + String destinationPath = "/" + destinationContainer + "/" + destinationObject; + + ContainerApi containerApi = api.getContainerApi(regionId); + + // create source and destination dirs + containerApi.create(sourceContainer); + containerApi.create(destinationContainer); + + // get the api for this region and container + ObjectApi srcApi = api.getObjectApi(regionId, sourceContainer); + ObjectApi destApi = api.getObjectApi(regionId, destinationContainer); + + // Create source object + assertNotNull(srcApi.put(sourceObjectName, PAYLOAD)); + SwiftObject sourceObject = srcApi.get(sourceObjectName); + checkObject(sourceObject); + + destApi.copy(destinationObject, sourceContainer, sourceObjectName, new CopyOptions().ifMatch(sourceObject.getETag())); + try { + destApi.copy(destinationObject, sourceContainer, sourceObjectName, new CopyOptions().ifMatch("fake-etag")); + Fail.failBecauseExceptionWasNotThrown(HttpResponseException.class); + } catch (HttpResponseException hre) { + assertThat(hre.getResponse().getStatusCode()).isEqualTo(412); + } + + long now = System.currentTimeMillis(); + Date before = new Date(now - 1000 * 1000); + Date after = new Date(now + 1000 * 1000); + + destApi.copy(destinationObject, sourceContainer, sourceObjectName, new CopyOptions().ifModifiedSince(before)); + try { + destApi.copy(destinationObject, sourceContainer, sourceObjectName, new CopyOptions().ifModifiedSince(after)); + Fail.failBecauseExceptionWasNotThrown(HttpResponseException.class); + } catch (HttpResponseException hre) { + assertThat(hre.getResponse().getStatusCode()).isEqualTo(304); + } + + try { + destApi.copy(destinationObject, sourceContainer, sourceObjectName, new CopyOptions().ifUnmodifiedSince(before)); + Fail.failBecauseExceptionWasNotThrown(HttpResponseException.class); + } catch (HttpResponseException hre) { + assertThat(hre.getResponse().getStatusCode()).isEqualTo(412); + } + destApi.copy(destinationObject, sourceContainer, sourceObjectName, new CopyOptions().ifUnmodifiedSince(after)); + } + } + public void testList() throws Exception { for (String regionId : regions) { ObjectApi objectApi = api.getObjectApi(regionId, containerName); http://git-wip-us.apache.org/repos/asf/jclouds/blob/2bd05501/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java ---------------------------------------------------------------------- diff --git a/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java b/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java index 77305eb..096a0e1 100644 --- a/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java +++ b/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java @@ -19,6 +19,7 @@ package org.jclouds.openstack.swift.v1.features; import static com.google.common.base.Charsets.US_ASCII; import static com.google.common.io.BaseEncoding.base16; import static com.google.common.net.HttpHeaders.EXPIRES; +import static org.assertj.core.api.Assertions.assertThat; import static org.jclouds.Constants.PROPERTY_MAX_RETRIES; import static org.jclouds.Constants.PROPERTY_RETRY_DELAY_START; import static org.jclouds.Constants.PROPERTY_SO_TIMEOUT; @@ -52,6 +53,7 @@ import org.jclouds.io.payloads.ByteSourcePayload; import org.jclouds.openstack.swift.v1.SwiftApi; import org.jclouds.openstack.swift.v1.domain.ObjectList; import org.jclouds.openstack.swift.v1.domain.SwiftObject; +import org.jclouds.openstack.swift.v1.options.CopyOptions; import org.jclouds.openstack.swift.v1.options.ListContainerOptions; import org.jclouds.openstack.swift.v1.reference.SwiftHeaders; import org.jclouds.openstack.v2_0.internal.BaseOpenStackMockTest; @@ -484,6 +486,30 @@ public class ObjectApiMockTest extends BaseOpenStackMockTest<SwiftApi> { } } + public void testCopyObjectConditional() throws Exception { + MockWebServer server = mockOpenStackServer(); + server.enqueue(addCommonHeaders(new MockResponse().setBody(stringFromResource("/access.json")))); + server.enqueue(addCommonHeaders(new MockResponse().setResponseCode(201) + .addHeader(SwiftHeaders.OBJECT_COPY_FROM, "/bar/foo.txt"))); + try { + SwiftApi api = api(server.getUrl("/").toString(), "openstack-swift"); + api.getObjectApi("DFW", "foo").copy("bar.txt", "bar", "foo.txt", new CopyOptions().ifMatch("fakeetag")); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + + RecordedRequest copyRequest = server.takeRequest(); + assertEquals(copyRequest.getRequestLine(), + "PUT /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/foo/bar.txt HTTP/1.1"); + + List<String> requestHeaders = copyRequest.getHeaders(); + assertThat(requestHeaders).contains("If-Match: fakeetag"); + assertThat(requestHeaders).contains(SwiftHeaders.OBJECT_COPY_FROM + ": /bar/foo.txt"); + } finally { + server.shutdown(); + } + } + @Test(expectedExceptions = KeyNotFoundException.class) public void testCopyObjectFail() throws InterruptedException, IOException { MockWebServer server = mockOpenStackServer();
