diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java index c6ea7a2cea..dfe8bcc06f 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java @@ -538,6 +538,12 @@ Builder setHardDeleteTime(OffsetDateTime hardDeleteTime) { return this; } + @Override + Builder setRestoreToken(String restoreToken){ + infoBuilder.setRestoreToken(restoreToken); + return this; + } + @Override public Builder setRetention(Retention retention) { infoBuilder.setRetention(retention); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java index 52ce09fd7e..e034e9d7b2 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java @@ -110,6 +110,7 @@ public class BlobInfo implements Serializable { private final Retention retention; private final OffsetDateTime softDeleteTime; private final OffsetDateTime hardDeleteTime; + private final String restoreToken; private final transient ImmutableSet modifiedFields; /** This class is meant for internal use only. Users are discouraged from using this class. */ @@ -531,6 +532,8 @@ Builder setRetentionExpirationTimeOffsetDateTime(OffsetDateTime retentionExpirat abstract Builder setHardDeleteTime(OffsetDateTime hardDeleteTIme); + abstract Builder setRestoreToken(String restoreToken); + public abstract Builder setRetention(Retention retention); /** Creates a {@code BlobInfo} object. */ @@ -634,6 +637,7 @@ static final class BuilderImpl extends Builder { private Retention retention; private OffsetDateTime softDeleteTime; private OffsetDateTime hardDeleteTime; + private String restoreToken; private final ImmutableSet.Builder modifiedFields = ImmutableSet.builder(); BuilderImpl(BlobId blobId) { @@ -674,6 +678,7 @@ static final class BuilderImpl extends Builder { retention = blobInfo.retention; softDeleteTime = blobInfo.softDeleteTime; hardDeleteTime = blobInfo.hardDeleteTime; + restoreToken = blobInfo.restoreToken; } @Override @@ -1065,6 +1070,15 @@ Builder setHardDeleteTime(OffsetDateTime hardDeleteTime) { return this; } + @Override + Builder setRestoreToken(String restoreToken) { + if(!Objects.equals(this.restoreToken, restoreToken)){ + modifiedFields.add(BlobField.RESTORE_TOKEN); + } + this.restoreToken = restoreToken; + return this; + } + @Override public Builder setRetention(Retention retention) { // todo: b/308194853 @@ -1299,6 +1313,7 @@ Builder clearRetentionExpirationTime() { retention = builder.retention; softDeleteTime = builder.softDeleteTime; hardDeleteTime = builder.hardDeleteTime; + restoreToken = builder.restoreToken; modifiedFields = builder.modifiedFields.build(); } @@ -1704,6 +1719,14 @@ public OffsetDateTime getHardDeleteTime() { return hardDeleteTime; } + /** + * If this is a soft-deleted object in an HNS-enabled bucket, returns the restore token which will + * be necessary to restore it if there's a name conflict with another object. + */ + public String getRestoreToken() { + return restoreToken; + } + /** Returns the object's Retention policy. */ public Retention getRetention() { return retention; @@ -1761,7 +1784,8 @@ public int hashCode() { retention, retentionExpirationTime, softDeleteTime, - hardDeleteTime); + hardDeleteTime, + restoreToken); } @Override @@ -1805,7 +1829,8 @@ public boolean equals(Object o) { && Objects.equals(retentionExpirationTime, blobInfo.retentionExpirationTime) && Objects.equals(retention, blobInfo.retention) && Objects.equals(softDeleteTime, blobInfo.softDeleteTime) - && Objects.equals(hardDeleteTime, blobInfo.hardDeleteTime); + && Objects.equals(hardDeleteTime, blobInfo.hardDeleteTime) + && Objects.equals(restoreToken, blobInfo.restoreToken); } ImmutableSet getModifiedFields() { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java index 7396e0ae69..17d74a1d4d 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java @@ -886,6 +886,8 @@ private Object blobInfoEncode(BlobInfo from) { ifNonNull(from.getCustomTimeOffsetDateTime(), timestampCodec::encode, toBuilder::setCustomTime); ifNonNull(from.getSoftDeleteTime(), timestampCodec::encode, toBuilder::setSoftDeleteTime); ifNonNull(from.getHardDeleteTime(), timestampCodec::encode, toBuilder::setHardDeleteTime); + // TODO: uncomment when grpc is available + //ifNonNull(from.getRestoreToken(), toBuilder::setRestoreToken); ifNonNull( from.getCustomerEncryption(), customerEncryptionCodec::encode, @@ -957,6 +959,10 @@ private BlobInfo blobInfoDecode(Object from) { if (from.hasHardDeleteTime()) { toBuilder.setHardDeleteTime(timestampCodec.decode(from.getHardDeleteTime())); } + /* TODO: uncomment when grpc is available + if (from.hasRestoreToken()) { + toBuilder.setRestoreToken(from.getRestoreToken()); + }*/ String storageClass = from.getStorageClass(); if (!storageClass.isEmpty()) { toBuilder.setStorageClass(StorageClass.valueOf(storageClass)); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java index 12562cb020..316cd00335 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java @@ -254,6 +254,7 @@ private StorageObject blobInfoEncode(BlobInfo from) { ifNonNull(from.getSoftDeleteTime(), dateTimeCodec::encode, to::setSoftDeleteTime); ifNonNull(from.getHardDeleteTime(), dateTimeCodec::encode, to::setHardDeleteTime); + ifNonNull(from.getRestoreToken(), to::setRestoreToken); // todo: clean this up once retention is enabled in grpc // This is a workaround so that explicitly null retention objects are only included when the @@ -338,6 +339,7 @@ private BlobInfo blobInfoDecode(StorageObject from) { ifNonNull(from.getRetention(), this::retentionDecode, to::setRetention); ifNonNull(from.getSoftDeleteTime(), dateTimeCodec::decode, to::setSoftDeleteTime); ifNonNull(from.getHardDeleteTime(), dateTimeCodec::decode, to::setHardDeleteTime); + ifNonNull(from.getRestoreToken(), to::setRestoreToken); return to.build(); } 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 0729b47225..6e4bd2aea9 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 @@ -326,8 +326,10 @@ enum BlobField implements FieldSelector, NamedField { @TransportCompatibility({Transport.HTTP, Transport.GRPC}) HARD_DELETE_TIME( - "hardDeleteTime", "hard_delete_time", com.google.api.client.util.DateTime.class); + "hardDeleteTime", "hard_delete_time", com.google.api.client.util.DateTime.class), + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + RESTORE_TOKEN("restoreToken", "restore_token", String.class); static final List REQUIRED_FIELDS = ImmutableList.of(BUCKET, NAME); private static final Map JSON_FIELD_NAME_INDEX; @@ -1687,6 +1689,15 @@ public static BlobGetOption softDeleted(boolean softDeleted) { return new BlobGetOption(UnifiedOpts.softDeleted(softDeleted)); } + /** + * Returns an option that must be specified when getting a soft-deleted object from an HNS-enabled + * bucket that has a name/generation conflict with another object in the same bucket. + */ + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + public static BlobGetOption restoreToken(String restoreToken){ + return new BlobGetOption(UnifiedOpts.restoreToken(restoreToken)); + } + /** * Deduplicate any options which are the same parameter. The value which comes last in {@code * os} will be the value included in the return. @@ -1775,6 +1786,15 @@ public static BlobRestoreOption metagenerationNotMatch(long generation) { public static BlobRestoreOption copySourceAcl(boolean copySourceAcl) { return new BlobRestoreOption(UnifiedOpts.copySourceAcl(copySourceAcl)); } + + /** + * Returns an option that must be specified when getting a soft-deleted object from an HNS-enabled + * bucket that has a name/generation conflict with another object in the same bucket. + */ + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + public static BlobRestoreOption restoreToken(String restoreToken) { + return new BlobRestoreOption(UnifiedOpts.restoreToken(restoreToken)); + } } /** Class for specifying bucket list options. */ 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 bcc987ea75..45d87dbf16 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 @@ -502,6 +502,10 @@ static SoftDeleted softDeleted(boolean softDeleted) { return new SoftDeleted(softDeleted); } + static RestoreToken restoreToken(String restoreToken){ + return new RestoreToken(restoreToken); + } + static CopySourceAcl copySourceAcl(boolean copySourceAcl) { return new CopySourceAcl(copySourceAcl); } @@ -719,6 +723,27 @@ public Mapper getObject() { } } + static final class RestoreToken extends RpcOptVal implements ObjectSourceOpt { + + private static final long serialVersionUID = 4215757108268532746L; + + private RestoreToken(String val) { + super(StorageRpc.Option.RESTORE_TOKEN, val); + } + + /* TODO: uncomment when grpc is available + @Override + public Mapper restoreObject() { + return b -> b.setRestoreToken(val); + } + + @Override + public Mapper getObject() { + return b-> b.setRestoreToken(val); + } + */ + } + static final class CopySourceAcl extends RpcOptVal implements ObjectSourceOpt { private static final long serialVersionUID = 2033755749149128119L; 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..b4a2ddfcc6 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 @@ -75,6 +75,7 @@ enum Option { RETURN_RAW_INPUT_STREAM("returnRawInputStream"), OVERRIDE_UNLOCKED_RETENTION("overrideUnlockedRetention"), SOFT_DELETED("softDeleted"), + RESTORE_TOKEN("restoreToken"), COPY_SOURCE_ACL("copySourceAcl"), GENERATION("generation"), INCLUDE_FOLDERS_AS_PREFIXES("includeFoldersAsPrefixes"), diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java index 99ed548098..ec838406dd 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java @@ -1525,4 +1525,38 @@ public void testUpdateBlob_noModification() { Blob gen2 = storage.update(gen1); assertThat(gen2).isEqualTo(gen1); } + + @Test + @Exclude(transports = Transport.GRPC) // TODO: remove when grpc is available + public void testRestoreToken() { + String bucketName = generator.randomBucketName(); + storage.create( + BucketInfo.newBuilder(bucketName) + .setHierarchicalNamespace( + BucketInfo.HierarchicalNamespace.newBuilder().setEnabled(true).build()) + .setIamConfiguration( + BucketInfo.IamConfiguration.newBuilder() + .setIsUniformBucketLevelAccessEnabled(true) + .build()) + .build()); + BlobInfo info = BlobInfo.newBuilder(bucketName, generator.randomObjectName()).build(); + try { + Blob delobj = storage.create(info); + storage.delete(delobj.getBlobId()); + + Blob got = storage.get(delobj.getBlobId(), BlobGetOption.softDeleted(true)); + assertThat(got.getRestoreToken()).isNotNull(); + + Blob gotWithRestoreToken = storage.get(delobj.getBlobId(), BlobGetOption.softDeleted(true), BlobGetOption.restoreToken(got.getRestoreToken())); + assertThat(gotWithRestoreToken).isNotNull(); + + storage.restore(got.getBlobId(), Storage.BlobRestoreOption.restoreToken(got.getRestoreToken())); + assertThat(storage.get(bucketName, delobj.getName())).isNotNull();; + + } finally { + storage.delete(info.getBlobId()); + storage.delete(bucketName); + } + + } }