JCLOUDS-651: Portable support for conditional copies
Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/8945258d Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/8945258d Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/8945258d Branch: refs/heads/master Commit: 8945258d79a5fc9b3a5aeb35703213ca06c74d0e Parents: 293d3f8 Author: Andrew Gaul <[email protected]> Authored: Thu Feb 11 21:11:24 2016 -0800 Committer: Andrew Gaul <[email protected]> Committed: Tue Feb 16 16:29:54 2016 -0800 ---------------------------------------------------------------------- .../blobstore/internal/BaseBlobStore.java | 39 +++ .../jclouds/blobstore/options/CopyOptions.java | 27 ++- .../internal/BaseBlobIntegrationTest.java | 236 +++++++++++++++++++ 3 files changed, 301 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/jclouds/blob/8945258d/blobstore/src/main/java/org/jclouds/blobstore/internal/BaseBlobStore.java ---------------------------------------------------------------------- diff --git a/blobstore/src/main/java/org/jclouds/blobstore/internal/BaseBlobStore.java b/blobstore/src/main/java/org/jclouds/blobstore/internal/BaseBlobStore.java index 820721e..c6d21ff 100644 --- a/blobstore/src/main/java/org/jclouds/blobstore/internal/BaseBlobStore.java +++ b/blobstore/src/main/java/org/jclouds/blobstore/internal/BaseBlobStore.java @@ -23,6 +23,7 @@ import static org.jclouds.util.Predicates2.retry; import java.io.InputStream; import java.io.IOException; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; @@ -46,6 +47,10 @@ import org.jclouds.blobstore.strategy.internal.MultipartUploadSlicingAlgorithm; import org.jclouds.blobstore.util.BlobUtils; import org.jclouds.collect.Memoized; import org.jclouds.domain.Location; +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.http.HttpResponseException; import org.jclouds.io.ContentMetadata; import org.jclouds.io.Payload; import org.jclouds.io.PayloadSlicer; @@ -258,6 +263,26 @@ public abstract class BaseBlobStore implements BlobStore { throw new KeyNotFoundException(fromContainer, fromName, "while copying"); } + String eTag = maybeQuoteETag(blob.getMetadata().getETag()); + if (eTag != null) { + if (options.ifMatch() != null && !options.ifMatch().equals(eTag)) { + throw returnResponseException(412); + } + if (options.ifNoneMatch() != null && options.ifNoneMatch().equals(eTag)) { + throw returnResponseException(412); + } + } + + Date lastModified = blob.getMetadata().getLastModified(); + if (lastModified != null) { + if (options.ifModifiedSince() != null && lastModified.compareTo(options.ifModifiedSince()) <= 0) { + throw returnResponseException(412); + } + if (options.ifUnmodifiedSince() != null && lastModified.compareTo(options.ifUnmodifiedSince()) >= 0) { + throw returnResponseException(412); + } + } + InputStream is = null; try { is = blob.getPayload().openStream(); @@ -311,4 +336,18 @@ public abstract class BaseBlobStore implements BlobStore { } return completeMultipartUpload(mpu, parts); } + + private static HttpResponseException returnResponseException(int code) { + HttpResponse response = HttpResponse.builder().statusCode(code).build(); + // TODO: bogus endpoint + return new HttpResponseException(new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://stub") + .build()), response); + } + + private static String maybeQuoteETag(String eTag) { + if (!eTag.startsWith("\"") && !eTag.endsWith("\"")) { + eTag = "\"" + eTag + "\""; + } + return eTag; + } } http://git-wip-us.apache.org/repos/asf/jclouds/blob/8945258d/blobstore/src/main/java/org/jclouds/blobstore/options/CopyOptions.java ---------------------------------------------------------------------- diff --git a/blobstore/src/main/java/org/jclouds/blobstore/options/CopyOptions.java b/blobstore/src/main/java/org/jclouds/blobstore/options/CopyOptions.java index bb7985c..4e11c3c 100644 --- a/blobstore/src/main/java/org/jclouds/blobstore/options/CopyOptions.java +++ b/blobstore/src/main/java/org/jclouds/blobstore/options/CopyOptions.java @@ -17,6 +17,7 @@ package org.jclouds.blobstore.options; +import java.util.Date; import java.util.Map; import org.jclouds.io.ContentMetadata; @@ -24,6 +25,7 @@ import org.jclouds.javax.annotation.Nullable; import com.google.auto.value.AutoValue; import com.google.common.annotations.Beta; +import com.google.common.collect.ImmutableMap; @AutoValue @Beta @@ -39,11 +41,34 @@ public abstract class CopyOptions { @Nullable public abstract Map<String, String> userMetadata(); + @Nullable + public abstract Date ifModifiedSince(); + @Nullable + public abstract Date ifUnmodifiedSince(); + @Nullable + public abstract String ifMatch(); + @Nullable + public abstract String ifNoneMatch(); + @AutoValue.Builder public abstract static class Builder { public abstract Builder contentMetadata(ContentMetadata contentMetadata); public abstract Builder userMetadata(Map<String, String> userMetadata); - public abstract CopyOptions build(); + public abstract Builder ifModifiedSince(Date ifModifiedSince); + public abstract Builder ifUnmodifiedSince(Date ifUnmodifiedSince); + public abstract Builder ifMatch(String ifMatch); + public abstract Builder ifNoneMatch(String ifNoneMatch); + + abstract Map<String, String> userMetadata(); + abstract CopyOptions autoBuild(); + + public CopyOptions build() { + Map<String, String> userMetadata = userMetadata(); + if (userMetadata != null) { + userMetadata(ImmutableMap.copyOf(userMetadata)); + } + return autoBuild(); + } } } http://git-wip-us.apache.org/repos/asf/jclouds/blob/8945258d/blobstore/src/test/java/org/jclouds/blobstore/integration/internal/BaseBlobIntegrationTest.java ---------------------------------------------------------------------- diff --git a/blobstore/src/test/java/org/jclouds/blobstore/integration/internal/BaseBlobIntegrationTest.java b/blobstore/src/test/java/org/jclouds/blobstore/integration/internal/BaseBlobIntegrationTest.java index 7740dbc..369f98e 100644 --- a/blobstore/src/test/java/org/jclouds/blobstore/integration/internal/BaseBlobIntegrationTest.java +++ b/blobstore/src/test/java/org/jclouds/blobstore/integration/internal/BaseBlobIntegrationTest.java @@ -42,11 +42,13 @@ import java.util.Map; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import javax.ws.rs.core.MediaType; +import org.assertj.core.api.Fail; import org.jclouds.blobstore.BlobStore; import org.jclouds.blobstore.ContainerNotFoundException; import org.jclouds.blobstore.KeyNotFoundException; @@ -948,6 +950,240 @@ public class BaseBlobIntegrationTest extends BaseBlobStoreIntegrationTest { } @Test(groups = { "integration", "live" }) + public void testCopyIfMatch() throws Exception { + BlobStore blobStore = view.getBlobStore(); + String fromName = "source"; + String toName = "to"; + ByteSource payload = TestUtils.randomByteSource().slice(0, 1024); + Blob blob = blobStore + .blobBuilder(fromName) + .payload(payload) + .contentLength(payload.size()) + .build(); + String fromContainer = getContainerName(); + String toContainer = getContainerName(); + try { + String eTag = blobStore.putBlob(fromContainer, blob); + blobStore.copyBlob(fromContainer, fromName, toContainer, toName, CopyOptions.builder().ifMatch(eTag).build()); + Blob toBlob = blobStore.getBlob(toContainer, toName); + InputStream is = null; + try { + is = toBlob.getPayload().openStream(); + assertEquals(ByteStreams.toByteArray(is), payload.read()); + } finally { + Closeables2.closeQuietly(is); + } + } finally { + returnContainer(toContainer); + returnContainer(fromContainer); + } + } + + @Test(groups = { "integration", "live" }) + public void testCopyIfMatchNegative() throws Exception { + BlobStore blobStore = view.getBlobStore(); + String fromName = "source"; + String toName = "to"; + ByteSource payload = TestUtils.randomByteSource().slice(0, 1024); + Blob blob = blobStore + .blobBuilder(fromName) + .payload(payload) + .contentLength(payload.size()) + .build(); + String fromContainer = getContainerName(); + String toContainer = getContainerName(); + try { + blobStore.putBlob(fromContainer, blob); + try { + blobStore.copyBlob(fromContainer, fromName, toContainer, toName, CopyOptions.builder().ifMatch("fake-etag").build()); + Fail.failBecauseExceptionWasNotThrown(HttpResponseException.class); + } catch (HttpResponseException hre) { + assertThat(hre.getResponse().getStatusCode()).isEqualTo(412); + } + } finally { + returnContainer(toContainer); + returnContainer(fromContainer); + } + } + + @Test(groups = { "integration", "live" }) + public void testCopyIfNoneMatch() throws Exception { + BlobStore blobStore = view.getBlobStore(); + String fromName = "source"; + String toName = "to"; + ByteSource payload = TestUtils.randomByteSource().slice(0, 1024); + Blob blob = blobStore + .blobBuilder(fromName) + .payload(payload) + .contentLength(payload.size()) + .build(); + String fromContainer = getContainerName(); + String toContainer = getContainerName(); + try { + blobStore.putBlob(fromContainer, blob); + blobStore.copyBlob(fromContainer, fromName, toContainer, toName, CopyOptions.builder().ifNoneMatch("fake-etag").build()); + Blob toBlob = blobStore.getBlob(toContainer, toName); + InputStream is = null; + try { + is = toBlob.getPayload().openStream(); + assertEquals(ByteStreams.toByteArray(is), payload.read()); + } finally { + Closeables2.closeQuietly(is); + } + } finally { + returnContainer(toContainer); + returnContainer(fromContainer); + } + } + + @Test(groups = { "integration", "live" }) + public void testCopyIfNoneMatchNegative() throws Exception { + BlobStore blobStore = view.getBlobStore(); + String fromName = "source"; + String toName = "to"; + ByteSource payload = TestUtils.randomByteSource().slice(0, 1024); + Blob blob = blobStore + .blobBuilder(fromName) + .payload(payload) + .contentLength(payload.size()) + .build(); + String fromContainer = getContainerName(); + String toContainer = getContainerName(); + try { + String eTag = blobStore.putBlob(fromContainer, blob); + try { + blobStore.copyBlob(fromContainer, fromName, toContainer, toName, CopyOptions.builder().ifNoneMatch(eTag).build()); + Fail.failBecauseExceptionWasNotThrown(HttpResponseException.class); + } catch (HttpResponseException hre) { + assertThat(hre.getResponse().getStatusCode()).isEqualTo(412); + } + } finally { + returnContainer(toContainer); + returnContainer(fromContainer); + } + } + + @Test(groups = { "integration", "live" }) + public void testCopyIfModifiedSince() throws Exception { + BlobStore blobStore = view.getBlobStore(); + String fromName = "source"; + String toName = "to"; + ByteSource payload = TestUtils.randomByteSource().slice(0, 1024); + Blob blob = blobStore + .blobBuilder(fromName) + .payload(payload) + .contentLength(payload.size()) + .build(); + String fromContainer = getContainerName(); + String toContainer = getContainerName(); + try { + blobStore.putBlob(fromContainer, blob); + Date before = new Date(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1)); + blobStore.copyBlob(fromContainer, fromName, toContainer, toName, CopyOptions.builder().ifModifiedSince(before).build()); + Blob toBlob = blobStore.getBlob(toContainer, toName); + InputStream is = null; + try { + is = toBlob.getPayload().openStream(); + assertEquals(ByteStreams.toByteArray(is), payload.read()); + } finally { + Closeables2.closeQuietly(is); + } + } finally { + returnContainer(toContainer); + returnContainer(fromContainer); + } + } + + @Test(groups = { "integration", "live" }) + public void testCopyIfModifiedSinceNegative() throws Exception { + BlobStore blobStore = view.getBlobStore(); + String fromName = "source"; + String toName = "to"; + ByteSource payload = TestUtils.randomByteSource().slice(0, 1024); + Blob blob = blobStore + .blobBuilder(fromName) + .payload(payload) + .contentLength(payload.size()) + .build(); + String fromContainer = getContainerName(); + String toContainer = getContainerName(); + try { + blobStore.putBlob(fromContainer, blob); + // TODO: some problem with S3 and times in the future? + Date after = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); + try { + blobStore.copyBlob(fromContainer, fromName, toContainer, toName, CopyOptions.builder().ifModifiedSince(after).build()); + Fail.failBecauseExceptionWasNotThrown(HttpResponseException.class); + } catch (HttpResponseException hre) { + // most object stores return 412 but swift returns 304 + assertThat(hre.getResponse().getStatusCode()).isIn(304, 412); + } + } finally { + returnContainer(toContainer); + returnContainer(fromContainer); + } + } + + @Test(groups = { "integration", "live" }) + public void testCopyIfUnmodifiedSince() throws Exception { + BlobStore blobStore = view.getBlobStore(); + String fromName = "source"; + String toName = "to"; + ByteSource payload = TestUtils.randomByteSource().slice(0, 1024); + Blob blob = blobStore + .blobBuilder(fromName) + .payload(payload) + .contentLength(payload.size()) + .build(); + String fromContainer = getContainerName(); + String toContainer = getContainerName(); + try { + blobStore.putBlob(fromContainer, blob); + Date after = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); + blobStore.copyBlob(fromContainer, fromName, toContainer, toName, CopyOptions.builder().ifUnmodifiedSince(after).build()); + Blob toBlob = blobStore.getBlob(toContainer, toName); + InputStream is = null; + try { + is = toBlob.getPayload().openStream(); + assertEquals(ByteStreams.toByteArray(is), payload.read()); + } finally { + Closeables2.closeQuietly(is); + } + } finally { + returnContainer(toContainer); + returnContainer(fromContainer); + } + } + + @Test(groups = { "integration", "live" }) + public void testCopyIfUnmodifiedSinceNegative() throws Exception { + BlobStore blobStore = view.getBlobStore(); + String fromName = "source"; + String toName = "to"; + ByteSource payload = TestUtils.randomByteSource().slice(0, 1024); + Blob blob = blobStore + .blobBuilder(fromName) + .payload(payload) + .contentLength(payload.size()) + .build(); + String fromContainer = getContainerName(); + String toContainer = getContainerName(); + try { + blobStore.putBlob(fromContainer, blob); + Date before = new Date(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1)); + try { + blobStore.copyBlob(fromContainer, fromName, toContainer, toName, CopyOptions.builder().ifUnmodifiedSince(before).build()); + Fail.failBecauseExceptionWasNotThrown(HttpResponseException.class); + } catch (HttpResponseException hre) { + assertThat(hre.getResponse().getStatusCode()).isEqualTo(412); + } + } finally { + returnContainer(toContainer); + returnContainer(fromContainer); + } + } + + @Test(groups = { "integration", "live" }) public void testMultipartUploadNoPartsAbort() throws Exception { BlobStore blobStore = view.getBlobStore(); String container = getContainerName();
