From 0700c727b3957f1d31e999f22c4330e42d01874b Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Tue, 14 Jan 2025 21:04:31 +0000 Subject: [PATCH 1/2] feat: add new Storage#moveBlob method to atomically rename an object --- .../clirr-ignored-differences.xml | 11 ++ .../storage/GrpcRetryAlgorithmManager.java | 7 + .../google/cloud/storage/GrpcStorageImpl.java | 26 +++ .../storage/HttpRetryAlgorithmManager.java | 9 + .../cloud/storage/OtelStorageDecorator.java | 19 +++ .../com/google/cloud/storage/Storage.java | 157 ++++++++++++++++++ .../com/google/cloud/storage/StorageImpl.java | 21 +++ .../com/google/cloud/storage/UnifiedOpts.java | 85 ++++++++++ .../cloud/storage/spi/v1/HttpStorageRpc.java | 37 +++++ .../cloud/storage/spi/v1/StorageRpc.java | 7 + .../storage/testing/StorageRpcTestBase.java | 10 ++ .../cloud/storage/it/ITFoldersTest.java | 43 ++++- .../runner/registry/AbstractStorageProxy.java | 5 + 13 files changed, 435 insertions(+), 2 deletions(-) diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml index 0b19230fdd..060db7c33f 100644 --- a/google-cloud-storage/clirr-ignored-differences.xml +++ b/google-cloud-storage/clirr-ignored-differences.xml @@ -108,5 +108,16 @@ io.opentelemetry.api.OpenTelemetry getOpenTelemetry() + + + 7012 + com/google/cloud/storage/Storage + * moveBlob(*) + + + 7012 + com/google/cloud/storage/spi/v1/StorageRpc + * moveObject(*) + diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java index d8a126c956..ef125fa4e2 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java @@ -32,6 +32,7 @@ import com.google.storage.v2.ListBucketsRequest; import com.google.storage.v2.ListObjectsRequest; import com.google.storage.v2.LockBucketRetentionPolicyRequest; +import com.google.storage.v2.MoveObjectRequest; import com.google.storage.v2.QueryWriteStatusRequest; import com.google.storage.v2.ReadObjectRequest; import com.google.storage.v2.RestoreObjectRequest; @@ -124,6 +125,12 @@ public ResultRetryAlgorithm getFor(RewriteObjectRequest req) { : retryStrategy.getNonidempotentHandler(); } + public ResultRetryAlgorithm getFor(MoveObjectRequest req) { + return req.hasIfGenerationMatch() + ? retryStrategy.getIdempotentHandler() + : retryStrategy.getNonidempotentHandler(); + } + public ResultRetryAlgorithm getFor(SetIamPolicyRequest req) { if (req.getPolicy().getEtag().equals(ByteString.empty())) { return retryStrategy.getNonidempotentHandler(); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java index 428f85bc73..69781270b0 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java @@ -90,6 +90,7 @@ import com.google.storage.v2.ListObjectsRequest; import com.google.storage.v2.ListObjectsResponse; import com.google.storage.v2.LockBucketRetentionPolicyRequest; +import com.google.storage.v2.MoveObjectRequest; import com.google.storage.v2.Object; import com.google.storage.v2.ObjectAccessControl; import com.google.storage.v2.ReadObjectRequest; @@ -1418,6 +1419,31 @@ public BlobWriteSession blobWriteSession(BlobInfo info, BlobWriteOption... optio return BlobWriteSessions.of(writableByteChannelSession); } + @Override + public Blob moveBlob(MoveBlobRequest request) { + Object srcObj = codecs.blobId().encode(request.getSource()); + Object dstObj = codecs.blobId().encode(request.getTarget()); + Opts srcOpts = + Opts.unwrap(request.getSourceOptions()).resolveFrom(request.getSource()).projectAsSource(); + Opts dstOpts = + Opts.unwrap(request.getTargetOptions()).resolveFrom(request.getTarget()); + MoveObjectRequest.Builder b = + MoveObjectRequest.newBuilder() + .setBucket(srcObj.getBucket()) + .setSourceObject(srcObj.getName()) + .setDestinationObject(dstObj.getName()); + + srcOpts.moveObjectsRequest().apply(b); + dstOpts.moveObjectsRequest().apply(b); + + MoveObjectRequest req = b.build(); + return Retrying.run( + getOptions(), + retryAlgorithmManager.getFor(req), + () -> storageClient.moveObjectCallable().call(req), + syntaxDecoders.blob); + } + @Override public GrpcStorageOptions getOptions() { return (GrpcStorageOptions) super.getOptions(); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java index b2315cd825..a2564860c2 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java @@ -27,6 +27,7 @@ import com.google.cloud.storage.spi.v1.StorageRpc; import com.google.cloud.storage.spi.v1.StorageRpc.RewriteRequest; import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; import java.io.Serializable; import java.util.List; import java.util.Map; @@ -236,6 +237,14 @@ public ResultRetryAlgorithm getForObjectsRewrite(RewriteRequest pb) { : retryStrategy.getNonidempotentHandler(); } + public ResultRetryAlgorithm getForObjectsMove( + ImmutableMap sourceOptions, + ImmutableMap targetOptions) { + return targetOptions.containsKey(StorageRpc.Option.IF_GENERATION_MATCH) + ? retryStrategy.getIdempotentHandler() + : retryStrategy.getNonidempotentHandler(); + } + public ResultRetryAlgorithm getForObjectsCompose( List sources, StorageObject target, Map optionsMap) { return optionsMap.containsKey(StorageRpc.Option.IF_GENERATION_MATCH) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelStorageDecorator.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelStorageDecorator.java index 2b13fbfb71..dc92c3420f 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelStorageDecorator.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelStorageDecorator.java @@ -1434,6 +1434,25 @@ public BlobWriteSession blobWriteSession(BlobInfo blobInfo, BlobWriteOption... o } } + @Override + public Blob moveBlob(MoveBlobRequest request) { + Span span = + tracer + .spanBuilder("moveBlob") + .setAttribute("gsutil.uri.source", request.getSource().toGsUtilUriWithGeneration()) + .setAttribute("gsutil.uri.target", request.getTarget().toGsUtilUriWithGeneration()) + .startSpan(); + try (Scope ignore = span.makeCurrent()) { + return delegate.moveBlob(request); + } catch (Throwable t) { + span.recordException(t); + span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName()); + throw t; + } finally { + span.end(); + } + } + @Override public StorageOptions getOptions() { return delegate.getOptions(); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java index 8b719396a6..f18e2bc3b3 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java @@ -50,6 +50,7 @@ import com.google.cloud.storage.UnifiedOpts.ObjectSourceOpt; import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt; import com.google.cloud.storage.UnifiedOpts.Opts; +import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -73,6 +74,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -2778,6 +2780,150 @@ public static Builder newBuilder() { } } + /** + * A class to contain all information needed for a Google Cloud Storage Object Move. + * + * @since 2.48.0 + * @see Storage#moveBlob(MoveBlobRequest) + */ + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + final class MoveBlobRequest { + private final BlobId source; + private final BlobId target; + private final ImmutableList sourceOptions; + private final ImmutableList targetOptions; + + MoveBlobRequest( + BlobId source, + BlobId target, + ImmutableList sourceOptions, + ImmutableList targetOptions) { + this.source = source; + this.target = target; + this.sourceOptions = sourceOptions; + this.targetOptions = targetOptions; + } + + public BlobId getSource() { + return source; + } + + public BlobId getTarget() { + return target; + } + + public List getSourceOptions() { + return sourceOptions; + } + + public List getTargetOptions() { + return targetOptions; + } + + public Builder toBuilder() { + return new Builder(source, target, sourceOptions, targetOptions); + } + + public static Builder newBuilder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MoveBlobRequest)) { + return false; + } + MoveBlobRequest that = (MoveBlobRequest) o; + return Objects.equals(source, that.source) + && Objects.equals(target, that.target) + && Objects.equals(sourceOptions, that.sourceOptions) + && Objects.equals(targetOptions, that.targetOptions); + } + + @Override + public int hashCode() { + return Objects.hash(source, target, sourceOptions, targetOptions); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("source", source) + .add("target", target) + .add("sourceOptions", sourceOptions) + .add("targetOptions", targetOptions) + .toString(); + } + + public static final class Builder { + + private BlobId source; + private BlobId target; + private ImmutableList sourceOptions; + private ImmutableList targetOptions; + + private Builder() { + this(null, null, ImmutableList.of(), ImmutableList.of()); + } + + private Builder( + BlobId source, + BlobId target, + ImmutableList sourceOptions, + ImmutableList targetOptions) { + this.source = source; + this.target = target; + this.sourceOptions = sourceOptions; + this.targetOptions = targetOptions; + } + + public Builder setSource(BlobId source) { + this.source = requireNonNull(source, "source must be non null"); + return this; + } + + public Builder setTarget(BlobId target) { + this.target = requireNonNull(target, "target must be non null"); + return this; + } + + public Builder setSourceOptions(Iterable sourceOptions) { + this.sourceOptions = + ImmutableList.copyOf(requireNonNull(sourceOptions, "sourceOptions must be non null")); + return this; + } + + public Builder setTargetOptions(Iterable targetOptions) { + this.targetOptions = + ImmutableList.copyOf(requireNonNull(targetOptions, "targetOptions must be non null")); + return this; + } + + public Builder setSourceOptions(BlobSourceOption... sourceOptions) { + this.sourceOptions = + ImmutableList.copyOf(requireNonNull(sourceOptions, "sourceOptions must be non null")); + return this; + } + + public Builder setTargetOptions(BlobTargetOption... targetOptions) { + this.targetOptions = + ImmutableList.copyOf(requireNonNull(targetOptions, "targetOptions must be non null")); + return this; + } + + public MoveBlobRequest build() { + return new MoveBlobRequest( + requireNonNull(source, "source must be non null"), + requireNonNull(target, "target must be non null"), + sourceOptions, + targetOptions); + } + } + } + /** * Creates a new bucket. * @@ -4882,4 +5028,15 @@ default void close() throws Exception {} default BlobWriteSession blobWriteSession(BlobInfo blobInfo, BlobWriteOption... options) { return throwGrpcOnly(fmtMethodName("blobWriteSession", BlobInfo.class, BlobWriteOption.class)); } + + /** + * Atomically move an object from one name to another. + * + *

This new method is an atomic equivalent of the previous rewrite + delete, however without + * the ability to change metadata fields for the target object. + * + * @since 2.48.0 + */ + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + Blob moveBlob(MoveBlobRequest request); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java index efb627459b..e5b023a3a0 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java @@ -1696,6 +1696,27 @@ public BlobWriteSession blobWriteSession(BlobInfo blobInfo, BlobWriteOption... o return BlobWriteSessions.of(writableByteChannelSession); } + @Override + public Blob moveBlob(MoveBlobRequest request) { + Opts srcOpts = + Opts.unwrap(request.getSourceOptions()).resolveFrom(request.getSource()).projectAsSource(); + Opts dstOpts = + Opts.unwrap(request.getTargetOptions()).resolveFrom(request.getTarget()); + ImmutableMap sourceOptions = srcOpts.getRpcOptions(); + ImmutableMap targetOptions = dstOpts.getRpcOptions(); + + return run( + retryAlgorithmManager.getForObjectsMove(sourceOptions, targetOptions), + () -> + storageRpc.moveObject( + request.getSource().getBucket(), + request.getSource().getName(), + request.getTarget().getName(), + sourceOptions, + targetOptions), + o -> codecs.blobInfo().decode(o).asBlob(this)); + } + @Override public BlobInfo internalCreateFrom(Path path, BlobInfo info, Opts opts) throws IOException { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java index 1f51429d02..6c9828c1b1 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java @@ -48,6 +48,7 @@ import com.google.storage.v2.ListBucketsRequest; import com.google.storage.v2.ListObjectsRequest; import com.google.storage.v2.LockBucketRetentionPolicyRequest; +import com.google.storage.v2.MoveObjectRequest; import com.google.storage.v2.ReadObjectRequest; import com.google.storage.v2.RestoreObjectRequest; import com.google.storage.v2.RewriteObjectRequest; @@ -155,6 +156,10 @@ default Mapper rewriteObject() { return Mapper.identity(); } + default Mapper moveObject() { + return Mapper.identity(); + } + default Mapper restoreObject() { return Mapper.identity(); } @@ -193,6 +198,10 @@ default Mapper rewriteObject() { return Mapper.identity(); } + default Mapper moveObject() { + return Mapper.identity(); + } + default Mapper startResumableWrite() { return Mapper.identity(); } @@ -295,6 +304,11 @@ default Mapper getObject() { default Mapper rewriteObject() { return Mapper.identity(); } + + @Override + default Mapper moveObject() { + return Mapper.identity(); + } } /** @@ -877,6 +891,11 @@ public Mapper rewriteObject() { return Mapper.identity(); } + @Override + public Mapper moveObject() { + return Mapper.identity(); + } + /** * Define a decoder which can clear out any fields which may have not been selected. * @@ -1090,6 +1109,11 @@ public Mapper rewriteObject() { return b -> b.setIfGenerationMatch(val); } + @Override + public Mapper moveObject() { + return b -> b.setIfGenerationMatch(val); + } + @Override public SourceGenerationMatch asSource() { return new SourceGenerationMatch(val); @@ -1154,6 +1178,11 @@ public Mapper rewriteObject() { return b -> b.setIfGenerationNotMatch(val); } + @Override + public Mapper moveObject() { + return b -> b.setIfGenerationNotMatch(val); + } + @Override public SourceGenerationNotMatch asSource() { return new SourceGenerationNotMatch(val); @@ -1329,6 +1358,11 @@ public Mapper rewriteObject() { return b -> b.setIfMetagenerationMatch(val); } + @Override + public Mapper moveObject() { + return b -> b.setIfMetagenerationMatch(val); + } + @Override public Mapper updateBucket() { return b -> b.setIfMetagenerationMatch(val); @@ -1417,6 +1451,11 @@ public Mapper rewriteObject() { return b -> b.setIfMetagenerationNotMatch(val); } + @Override + public Mapper moveObject() { + return b -> b.setIfMetagenerationNotMatch(val); + } + @Override public Mapper updateBucket() { return b -> b.setIfMetagenerationNotMatch(val); @@ -1620,6 +1659,11 @@ private SourceGenerationMatch(@NonNull Long val) { public Mapper rewriteObject() { return b -> b.setIfSourceGenerationMatch(val); } + + @Override + public Mapper moveObject() { + return b -> b.setIfSourceGenerationMatch(val); + } } /** @@ -1638,6 +1682,11 @@ private SourceGenerationNotMatch(@NonNull Long val) { public Mapper rewriteObject() { return b -> b.setIfSourceGenerationNotMatch(val); } + + @Override + public Mapper moveObject() { + return b -> b.setIfSourceGenerationNotMatch(val); + } } /** @@ -1656,6 +1705,11 @@ private SourceMetagenerationMatch(@NonNull Long val) { public Mapper rewriteObject() { return b -> b.setIfSourceMetagenerationMatch(val); } + + @Override + public Mapper moveObject() { + return b -> b.setIfSourceMetagenerationMatch(val); + } } /** @@ -1674,6 +1728,11 @@ private SourceMetagenerationNotMatch(@NonNull Long val) { public Mapper rewriteObject() { return b -> b.setIfSourceMetagenerationNotMatch(val); } + + @Override + public Mapper moveObject() { + return b -> b.setIfSourceMetagenerationNotMatch(val); + } } static final class RequestedPolicyVersion extends RpcOptVal<@NonNull Long> @@ -1854,6 +1913,11 @@ public Mapper getGrpcMetadataMapper() { public Mapper rewriteObject() { return Mapper.identity(); } + + @Override + public Mapper moveObject() { + return Mapper.identity(); + } } static final class VersionsFilter extends RpcOptVal<@NonNull Boolean> implements ObjectListOpt { @@ -2448,6 +2512,27 @@ Mapper rewriteObjectsRequest() { .reduce(Mapper.identity(), Mapper::andThen); } + Mapper moveObjectsRequest() { + return opts.stream() + .filter(isInstanceOf(ObjectTargetOpt.class).or(isInstanceOf(ObjectSourceOpt.class))) + .map( + o -> { + // TODO: Do we need to formalize this type of dual relationship with it's own + // interface? + if (o instanceof ObjectTargetOpt) { + ObjectTargetOpt oto = (ObjectTargetOpt) o; + return oto.moveObject(); + } else if (o instanceof ObjectSourceOpt) { + ObjectSourceOpt oso = (ObjectSourceOpt) o; + return oso.moveObject(); + } else { + // in practice this shouldn't happen because of the filter guard upstream + throw new IllegalStateException("Unexpected type: %s" + o.getClass()); + } + }) + .reduce(Mapper.identity(), Mapper::andThen); + } + Mapper getIamPolicyRequest() { return fuseMappers(BucketSourceOpt.class, BucketSourceOpt::getIamPolicy); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java index 5341051a25..c276c511af 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java @@ -46,6 +46,7 @@ import com.google.api.services.storage.Storage.Objects.Compose; import com.google.api.services.storage.Storage.Objects.Get; import com.google.api.services.storage.Storage.Objects.Insert; +import com.google.api.services.storage.Storage.Objects.Move; import com.google.api.services.storage.model.Bucket; import com.google.api.services.storage.model.Bucket.RetentionPolicy; import com.google.api.services.storage.model.BucketAccessControl; @@ -1143,6 +1144,42 @@ public String open(String signedURL) { } } + @Override + public StorageObject moveObject( + String bucket, + String sourceObject, + String destinationObject, + Map sourceOptions, + Map targetOptions) { + + String userProject = Option.USER_PROJECT.getString(sourceOptions); + if (userProject == null) { + userProject = Option.USER_PROJECT.getString(targetOptions); + } + try { + Move move = + storage + .objects() + .move(bucket, sourceObject, destinationObject) + .setIfSourceMetagenerationMatch( + Option.IF_SOURCE_METAGENERATION_MATCH.getLong(sourceOptions)) + .setIfSourceMetagenerationNotMatch( + Option.IF_SOURCE_METAGENERATION_NOT_MATCH.getLong(sourceOptions)) + .setIfSourceGenerationMatch(Option.IF_SOURCE_GENERATION_MATCH.getLong(sourceOptions)) + .setIfSourceGenerationNotMatch( + Option.IF_SOURCE_GENERATION_NOT_MATCH.getLong(sourceOptions)) + .setIfMetagenerationMatch(Option.IF_METAGENERATION_MATCH.getLong(targetOptions)) + .setIfMetagenerationNotMatch( + Option.IF_METAGENERATION_NOT_MATCH.getLong(targetOptions)) + .setIfGenerationMatch(Option.IF_GENERATION_MATCH.getLong(targetOptions)) + .setIfGenerationNotMatch(Option.IF_GENERATION_NOT_MATCH.getLong(targetOptions)) + .setUserProject(userProject); + return move.execute(); + } catch (IOException e) { + throw translate(e); + } + } + @Override public RewriteResponse openRewrite(RewriteRequest rewriteRequest) { Span span = startSpan(HttpStorageRpcSpans.SPAN_NAME_OPEN_REWRITE); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java index 78747d42d6..ffd1766b53 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java @@ -393,6 +393,13 @@ StorageObject writeWithResponse( int length, boolean last); + StorageObject moveObject( + String bucket, + String sourceObject, + String destinationObject, + Map sourceOptions, + Map targetOptions); + /** * Sends a rewrite request to open a rewrite channel. * diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java index 6686cb925e..8f835f5bf3 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java @@ -322,6 +322,16 @@ public ServiceAccount getServiceAccount(String projectId) { throw new UnsupportedOperationException("Not implemented yet"); } + @Override + public StorageObject moveObject( + String bucket, + String sourceObject, + String destinationObject, + Map sourceOptions, + Map targetOptions) { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public Storage getStorage() { throw new UnsupportedOperationException("Not implemented yet"); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITFoldersTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITFoldersTest.java index a595336671..1eceea6ce0 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITFoldersTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITFoldersTest.java @@ -19,14 +19,24 @@ import static com.google.cloud.storage.TestUtils.assertAll; import static com.google.common.truth.Truth.assertThat; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; import com.google.cloud.storage.BucketInfo; +import com.google.cloud.storage.DataGenerator; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.Storage.BlobSourceOption; +import com.google.cloud.storage.Storage.BlobTargetOption; +import com.google.cloud.storage.Storage.MoveBlobRequest; +import com.google.cloud.storage.TransportCompatibility.Transport; import com.google.cloud.storage.it.runner.StorageITRunner; import com.google.cloud.storage.it.runner.annotations.Backend; import com.google.cloud.storage.it.runner.annotations.BucketFixture; import com.google.cloud.storage.it.runner.annotations.BucketType; +import com.google.cloud.storage.it.runner.annotations.CrossRun; import com.google.cloud.storage.it.runner.annotations.Inject; -import com.google.cloud.storage.it.runner.annotations.SingleBackend; import com.google.cloud.storage.it.runner.registry.Generator; +import com.google.common.collect.ImmutableMap; import com.google.storage.control.v2.BucketName; import com.google.storage.control.v2.CreateFolderRequest; import com.google.storage.control.v2.Folder; @@ -36,10 +46,13 @@ import org.junit.runner.RunWith; @RunWith(StorageITRunner.class) -@SingleBackend(Backend.PROD) +@CrossRun( + backends = Backend.PROD, + transports = {Transport.HTTP, Transport.GRPC}) public class ITFoldersTest { @Inject public StorageControlClient ctrl; + @Inject public Storage storage; @Inject @BucketFixture(BucketType.HNS) @@ -61,4 +74,30 @@ public void createFolder() throws Exception { () -> assertThat(folder.getName()).isEqualTo(FolderName.format("_", bucketName, folderId)), () -> assertThat(folder.getMetageneration()).isGreaterThan(0)); } + + @Test + public void moveObject() throws Exception { + ChecksummedTestContent testContent = + ChecksummedTestContent.of(DataGenerator.base64Characters().genBytes(5286)); + + BlobId id1 = BlobId.of(bucket.getName(), generator.randomObjectName()); + BlobId id2 = BlobId.of(bucket.getName(), generator.randomObjectName()); + + ImmutableMap metadata = ImmutableMap.of("a", "b", "c", "d"); + BlobInfo info1 = BlobInfo.newBuilder(id1).setMetadata(metadata).build(); + Blob blob1 = storage.create(info1, testContent.getBytes(), BlobTargetOption.doesNotExist()); + + Blob blob2 = + storage.moveBlob( + MoveBlobRequest.newBuilder() + .setSource(blob1.getBlobId()) + .setTarget(id2) + .setSourceOptions(BlobSourceOption.generationMatch()) + .setTargetOptions(BlobTargetOption.doesNotExist()) + .build()); + + assertAll( + () -> assertThat(blob2.getCrc32c()).isEqualTo(testContent.getCrc32cBase64()), + () -> assertThat(blob2.getMetadata()).isEqualTo(metadata)); + } } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java index 9e5e9691e9..f0dc3f3499 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java @@ -494,6 +494,11 @@ public void close() throws Exception { delegate.close(); } + @Override + public Blob moveBlob(MoveBlobRequest request) { + return delegate.moveBlob(request); + } + @Override public StorageOptions getOptions() { return delegate.getOptions(); From a178a6ada28d838193affe1062d25f90dbba4f10 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Wed, 22 Jan 2025 17:15:10 +0000 Subject: [PATCH 2/2] chore: fix comment --- .../src/main/java/com/google/cloud/storage/Storage.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java index f18e2bc3b3..2fa0960281 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java @@ -5032,8 +5032,9 @@ default BlobWriteSession blobWriteSession(BlobInfo blobInfo, BlobWriteOption... /** * Atomically move an object from one name to another. * - *

This new method is an atomic equivalent of the previous rewrite + delete, however without - * the ability to change metadata fields for the target object. + *

This new method is an atomic equivalent of the existing {@link Storage#copy(CopyRequest)} + + * {@link Storage#delete(BlobId)}, however without the ability to change metadata fields for the + * target object. * * @since 2.48.0 */