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,16 @@ 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 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
+ */
+ @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