From e887a33339461c02e3b7df669541e1dac3299a9b Mon Sep 17 00:00:00 2001 From: ericjding Date: Wed, 31 Mar 2021 11:27:09 -0700 Subject: [PATCH] DEV-7962: implement updateFileRetention (#55) * DEV-7962: implement updateFileRetention * changes after review --- .../backblaze/b2/client/B2StorageClient.java | 12 ++ .../b2/client/B2StorageClientImpl.java | 13 ++ .../b2/client/B2StorageClientWebifier.java | 5 + .../client/B2StorageClientWebifierImpl.java | 12 ++ .../b2/client/structures/B2FileRetention.java | 5 + .../B2UpdateFileRetentionRequest.java | 114 ++++++++++++++++++ .../B2UpdateFileRetentionResponse.java | 65 ++++++++++ .../b2/client/B2StorageClientImplTest.java | 20 +++ .../B2StorageClientWebifierImplTest.java | 33 +++++ .../B2UpdateFileRetentionRequestTest.java | 114 ++++++++++++++++++ 10 files changed, 393 insertions(+) create mode 100644 core/src/main/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionRequest.java create mode 100644 core/src/main/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionResponse.java create mode 100644 core/src/test/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionRequestTest.java diff --git a/core/src/main/java/com/backblaze/b2/client/B2StorageClient.java b/core/src/main/java/com/backblaze/b2/client/B2StorageClient.java index 18af3172c..f80bf5f8f 100644 --- a/core/src/main/java/com/backblaze/b2/client/B2StorageClient.java +++ b/core/src/main/java/com/backblaze/b2/client/B2StorageClient.java @@ -42,6 +42,8 @@ import com.backblaze.b2.client.structures.B2UpdateBucketRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldResponse; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionRequest; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionResponse; import com.backblaze.b2.client.structures.B2UploadFileRequest; import com.backblaze.b2.client.structures.B2UploadListener; import com.backblaze.b2.client.structures.B2UploadPartUrlResponse; @@ -929,6 +931,16 @@ default String getDownloadByNameUrl(String bucketName, */ B2UpdateFileLegalHoldResponse updateFileLegalHold(B2UpdateFileLegalHoldRequest request) throws B2Exception; + /** + * Updates the file retention configuration of the specified file as described by the request. + * + * @param request specifies which file to update and how to update it. + * @return the new state of the file + * @throws B2Exception if there's any trouble. + * @see b2_update_file_retention + */ + B2UpdateFileRetentionResponse updateFileRetention(B2UpdateFileRetentionRequest request) throws B2Exception; + /** * Closes this instance, releasing resources. */ diff --git a/core/src/main/java/com/backblaze/b2/client/B2StorageClientImpl.java b/core/src/main/java/com/backblaze/b2/client/B2StorageClientImpl.java index 85aed06a5..8d2c2afb0 100644 --- a/core/src/main/java/com/backblaze/b2/client/B2StorageClientImpl.java +++ b/core/src/main/java/com/backblaze/b2/client/B2StorageClientImpl.java @@ -53,6 +53,8 @@ import com.backblaze.b2.client.structures.B2UpdateBucketRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldResponse; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionRequest; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionResponse; import com.backblaze.b2.client.structures.B2UploadFileRequest; import com.backblaze.b2.client.structures.B2UploadListener; import com.backblaze.b2.client.structures.B2UploadPartUrlResponse; @@ -169,6 +171,7 @@ public B2CreatedApplicationKey createKey(B2CreateKeyRequest request) throws B2Ex ); } + @SuppressWarnings("RedundantThrows") @Override public B2ListKeysIterable applicationKeys(B2ListKeysRequest request) throws B2Exception { return new B2ListKeysIterable(this, request); @@ -324,11 +327,13 @@ private long getContentLength(B2ContentSource contentSource) throws B2LocalExcep } + @SuppressWarnings("RedundantThrows") @Override public B2ListFilesIterable fileVersions(B2ListFileVersionsRequest request) throws B2Exception { return new B2ListFileVersionsIterable(this, request); } + @SuppressWarnings("RedundantThrows") @Override public B2ListFilesIterable fileNames(B2ListFileNamesRequest request) throws B2Exception { return new B2ListFileNamesIterable(this, request); @@ -501,6 +506,14 @@ public B2UpdateFileLegalHoldResponse updateFileLegalHold(B2UpdateFileLegalHoldRe retryPolicySupplier.get()); } + @Override + public B2UpdateFileRetentionResponse updateFileRetention(B2UpdateFileRetentionRequest request) throws B2Exception { + return retryer.doRetry("b2_update_file_retention", accountAuthCache, + () -> webifier.updateFileRetention(accountAuthCache.get(), request), + retryPolicySupplier.get()); + } + + // // For use by our iterators // XXX: make private somehow, or move to B2StorageClient interface. diff --git a/core/src/main/java/com/backblaze/b2/client/B2StorageClientWebifier.java b/core/src/main/java/com/backblaze/b2/client/B2StorageClientWebifier.java index 271a5cfe2..151277cb9 100644 --- a/core/src/main/java/com/backblaze/b2/client/B2StorageClientWebifier.java +++ b/core/src/main/java/com/backblaze/b2/client/B2StorageClientWebifier.java @@ -49,6 +49,8 @@ import com.backblaze.b2.client.structures.B2UpdateBucketRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldResponse; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionRequest; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionResponse; import com.backblaze.b2.client.structures.B2UploadFileRequest; import com.backblaze.b2.client.structures.B2UploadPartRequest; import com.backblaze.b2.client.structures.B2UploadPartUrlResponse; @@ -171,6 +173,9 @@ String getDownloadByNameUrl(B2AccountAuthorization accountAuth, B2UpdateFileLegalHoldResponse updateFileLegalHold(B2AccountAuthorization accountAuth, B2UpdateFileLegalHoldRequest request) throws B2Exception; + B2UpdateFileRetentionResponse updateFileRetention(B2AccountAuthorization accountAuth, + B2UpdateFileRetentionRequest request) throws B2Exception; + /** * Closes this object and its underlying resources. * This is overridden from AutoCloseable to declare that it can't throw any exception. diff --git a/core/src/main/java/com/backblaze/b2/client/B2StorageClientWebifierImpl.java b/core/src/main/java/com/backblaze/b2/client/B2StorageClientWebifierImpl.java index 5edf0fe5a..1e40b4008 100644 --- a/core/src/main/java/com/backblaze/b2/client/B2StorageClientWebifierImpl.java +++ b/core/src/main/java/com/backblaze/b2/client/B2StorageClientWebifierImpl.java @@ -62,6 +62,8 @@ import com.backblaze.b2.client.structures.B2UpdateBucketRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldResponse; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionRequest; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionResponse; import com.backblaze.b2.client.structures.B2UploadFileRequest; import com.backblaze.b2.client.structures.B2UploadListener; import com.backblaze.b2.client.structures.B2UploadPartRequest; @@ -656,6 +658,16 @@ public B2UpdateFileLegalHoldResponse updateFileLegalHold(B2AccountAuthorization B2UpdateFileLegalHoldResponse.class); } + @Override + public B2UpdateFileRetentionResponse updateFileRetention(B2AccountAuthorization accountAuth, + B2UpdateFileRetentionRequest request) throws B2Exception { + return webApiClient.postJsonReturnJson( + makeUrl(accountAuth, "b2_update_file_retention"), + makeHeaders(accountAuth), + request, + B2UpdateFileRetentionResponse.class); + } + private void addAuthHeader(B2HeadersImpl.Builder builder, B2AccountAuthorization accountAuth) { builder.set(B2Headers.AUTHORIZATION, accountAuth.getAuthorizationToken()); diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2FileRetention.java b/core/src/main/java/com/backblaze/b2/client/structures/B2FileRetention.java index f68fc4777..919b95bd4 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2FileRetention.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2FileRetention.java @@ -24,6 +24,11 @@ public class B2FileRetention { @B2Json.optional private final Long retainUntilTimestamp; + /** + * static field for convenience to use with updateFileRetention() to turn off retention + */ + public static final B2FileRetention NONE = new B2FileRetention(null, null); + @B2Json.constructor(params = "mode, retainUntilTimestamp") public B2FileRetention(String mode, Long retainUntilTimestamp) { this.mode = mode; diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionRequest.java b/core/src/main/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionRequest.java new file mode 100644 index 000000000..d81ce637e --- /dev/null +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionRequest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2021, Backblaze Inc. All Rights Reserved. + * License https://www.backblaze.com/using_b2_code.html + */ +package com.backblaze.b2.client.structures; + +import com.backblaze.b2.json.B2Json; +import com.backblaze.b2.util.B2Preconditions; + +import java.util.Objects; + +public class B2UpdateFileRetentionRequest { + @B2Json.required + public final String fileName; + + @B2Json.required + public final String fileId; + + @B2Json.optional + public final boolean bypassGovernance; + + @B2Json.required + public final B2FileRetention fileRetention; + + @B2Json.constructor(params = "fileName, fileId, bypassGovernance, fileRetention") + private B2UpdateFileRetentionRequest(String fileName, + String fileId, + boolean bypassGovernance, + B2FileRetention fileRetention) { + // perform some simple validation checks: + // 1) make sure mode is valid (i.e., if non-null, then either governance or compliance) + B2Preconditions.checkArgument(fileRetention.getMode() == null || + B2FileRetentionMode.COMPLIANCE.equals(fileRetention.getMode()) || + B2FileRetentionMode.GOVERNANCE.equals(fileRetention.getMode()), + "Invalid value for file retention mode"); + // 2) Both mode and retainUntilTimestamp must be either null or non-null + B2Preconditions.checkArgument( + (fileRetention.getMode() != null && fileRetention.getRetainUntilTimestamp() != null) || + (fileRetention.getMode() == null && fileRetention.getRetainUntilTimestamp() == null), + "Both file retention mode and retainUntilTimestamp are required if either is supplied"); + + this.fileName = fileName; + this.fileId = fileId; + this.bypassGovernance = bypassGovernance; + this.fileRetention = fileRetention; + } + + public String getFileName() { + return fileName; + } + + public String getFileId() { + return fileId; + } + + public boolean isBypassGovernance() { + return bypassGovernance; + } + + public B2FileRetention getFileRetention() { + return fileRetention; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + B2UpdateFileRetentionRequest that = (B2UpdateFileRetentionRequest) o; + return Objects.equals(getFileName(), that.getFileName()) && + Objects.equals(getFileId(), that.getFileId()) && + isBypassGovernance() == that.isBypassGovernance() && + Objects.equals(getFileRetention(), that.getFileRetention()); + } + + @Override + public int hashCode() { + return Objects.hash(getFileName(), getFileId(), isBypassGovernance(), getFileRetention()); + } + + public static Builder builder(String fileName, + String fileId, + B2FileRetention fileRetention) { + return new Builder(fileName, fileId, fileRetention); + } + + public static class Builder { + private final String fileName; + private final String fileId; + private final B2FileRetention fileRetention; + + private boolean bypassGovernance; + + public Builder(String fileName, + String fileId, + B2FileRetention fileRetention) { + this.fileName = fileName; + this.fileId = fileId; + this.fileRetention = fileRetention; + } + + public Builder setBypassGovernance(boolean bypassGovernance) { + this.bypassGovernance = bypassGovernance; + return this; + } + + public B2UpdateFileRetentionRequest build() { + return new B2UpdateFileRetentionRequest( + fileName, + fileId, + bypassGovernance, + fileRetention); + } + } +} diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionResponse.java b/core/src/main/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionResponse.java new file mode 100644 index 000000000..98f40a0f4 --- /dev/null +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionResponse.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021, Backblaze Inc. All Rights Reserved. + * License https://www.backblaze.com/using_b2_code.html + */ +package com.backblaze.b2.client.structures; + +import com.backblaze.b2.json.B2Json; + +import java.util.Objects; + +public class B2UpdateFileRetentionResponse { + @B2Json.required + public final String fileName; + + @B2Json.required + public final String fileId; + + @B2Json.required + public final B2FileRetention fileRetention; + + @B2Json.constructor(params = "fileName, fileId, fileRetention") + public B2UpdateFileRetentionResponse(String fileName, + String fileId, + B2FileRetention fileRetention) { + this.fileName = fileName; + this.fileId = fileId; + this.fileRetention = fileRetention; + } + + public String getFileName() { + return fileName; + } + + public String getFileId() { + return fileId; + } + + public B2FileRetention getFileRetention() { + return fileRetention; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + B2UpdateFileRetentionResponse that = (B2UpdateFileRetentionResponse) o; + return Objects.equals(getFileName(), that.getFileName()) && + Objects.equals(getFileId(), that.getFileId()) && + Objects.equals(getFileRetention(), that.getFileRetention()); + } + + @Override + public int hashCode() { + return Objects.hash(getFileName(), getFileId(), getFileRetention()); + } + + @Override + public String toString() { + return "B2UpdateFileLegalHoldResponse {" + + "fileId='" + fileId + '\'' + + ", fileName='" + fileName + '\'' + + ", fileRetention='" + fileRetention + '\'' + + '}'; + } +} diff --git a/core/src/test/java/com/backblaze/b2/client/B2StorageClientImplTest.java b/core/src/test/java/com/backblaze/b2/client/B2StorageClientImplTest.java index 07ab41cb4..e202c8b08 100644 --- a/core/src/test/java/com/backblaze/b2/client/B2StorageClientImplTest.java +++ b/core/src/test/java/com/backblaze/b2/client/B2StorageClientImplTest.java @@ -27,6 +27,8 @@ import com.backblaze.b2.client.structures.B2DownloadAuthorization; import com.backblaze.b2.client.structures.B2DownloadByIdRequest; import com.backblaze.b2.client.structures.B2DownloadByNameRequest; +import com.backblaze.b2.client.structures.B2FileRetention; +import com.backblaze.b2.client.structures.B2FileRetentionMode; import com.backblaze.b2.client.structures.B2FileVersion; import com.backblaze.b2.client.structures.B2FinishLargeFileRequest; import com.backblaze.b2.client.structures.B2GetDownloadAuthorizationRequest; @@ -52,6 +54,8 @@ import com.backblaze.b2.client.structures.B2UpdateBucketRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldResponse; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionRequest; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionResponse; import com.backblaze.b2.client.structures.B2UploadFileRequest; import com.backblaze.b2.client.structures.B2UploadListener; import com.backblaze.b2.client.structures.B2UploadPartRequest; @@ -1062,6 +1066,22 @@ public void testUpdateFileLegalHold() throws B2Exception { verify(webifier, times(1)).updateFileLegalHold(any(), eq(request)); } + @Test + public void testUpdateFileRetention() throws B2Exception { + final B2FileRetention fileRetention = new B2FileRetention(B2FileRetentionMode.COMPLIANCE, 10000L); + final B2UpdateFileRetentionRequest request = B2UpdateFileRetentionRequest + .builder(fileName(1), fileId(1), fileRetention) + .build(); + final B2UpdateFileRetentionResponse response = + new B2UpdateFileRetentionResponse(fileName(1), fileId(1), fileRetention); + when(webifier.updateFileRetention(any(), eq(request))).thenReturn(response); + + assertSame(response, client.updateFileRetention(request)); + + verify(webifier, times(1)).authorizeAccount(any()); + verify(webifier, times(1)).updateFileRetention(any(), eq(request)); + } + @Test public void testClose() { final B2AccountAuthorizer authorizer = mock(B2AccountAuthorizer.class); diff --git a/core/src/test/java/com/backblaze/b2/client/B2StorageClientWebifierImplTest.java b/core/src/test/java/com/backblaze/b2/client/B2StorageClientWebifierImplTest.java index c285333f9..1646b8da5 100644 --- a/core/src/test/java/com/backblaze/b2/client/B2StorageClientWebifierImplTest.java +++ b/core/src/test/java/com/backblaze/b2/client/B2StorageClientWebifierImplTest.java @@ -45,6 +45,7 @@ import com.backblaze.b2.client.structures.B2TestMode; import com.backblaze.b2.client.structures.B2UpdateBucketRequest; import com.backblaze.b2.client.structures.B2UpdateFileLegalHoldRequest; +import com.backblaze.b2.client.structures.B2UpdateFileRetentionRequest; import com.backblaze.b2.client.structures.B2UploadFileRequest; import com.backblaze.b2.client.structures.B2UploadListener; import com.backblaze.b2.client.structures.B2UploadPartRequest; @@ -1880,6 +1881,38 @@ public void testUpdateFileLegalHold() throws B2Exception { checkRequestCategory(OTHER, w -> w.updateFileLegalHold(ACCOUNT_AUTH, requestReal)); } + @Test + public void testUpdateFileRetention() throws B2Exception { + final B2FileRetention fileRetention = new B2FileRetention(B2FileRetentionMode.COMPLIANCE, 10000L); + final B2UpdateFileRetentionRequest requestReal = B2UpdateFileRetentionRequest + .builder(fileName(1), fileId(1), fileRetention) + .build(); + webifier.updateFileRetention(ACCOUNT_AUTH, requestReal); + + webApiClient.check("postJsonReturnJson.\n" + + "url:\n" + + " apiUrl1/b2api/v2/b2_update_file_retention\n" + + "headers:\n" + + " Authorization: accountToken1\n" + + " User-Agent: SecretAgentMan/3.19.28\n" + + " X-Bz-Test-Mode: force_cap_exceeded\n" + + "request:\n" + + " {\n" + + " \"bypassGovernance\": false,\n" + + " \"fileId\": \"4_zBlah_0000001\",\n" + + " \"fileName\": \"files/\u81ea\u7531/0001\",\n" + + " \"fileRetention\": {\n" + + " \"mode\": \"compliance\",\n" + + " \"retainUntilTimestamp\": 10000\n" + + " }\n" + + " }\n" + + "responseClass:\n" + + " B2UpdateFileRetentionResponse\n" + ); + + checkRequestCategory(OTHER, w -> w.updateFileRetention(ACCOUNT_AUTH, requestReal)); + } + @Test public void testTestModes() throws B2Exception { // test each possible testMode, including "none". diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionRequestTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionRequestTest.java new file mode 100644 index 000000000..ca6a2689f --- /dev/null +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2UpdateFileRetentionRequestTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2021, Backblaze Inc. All Rights Reserved. + * License https://www.backblaze.com/using_b2_code.html + */ +package com.backblaze.b2.client.structures; + +import com.backblaze.b2.json.B2Json; +import com.backblaze.b2.util.B2BaseTest; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.assertEquals; + +public class B2UpdateFileRetentionRequestTest extends B2BaseTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testNormal() { + final B2UpdateFileRetentionRequest request = B2UpdateFileRetentionRequest + .builder("fluffy.jpg", "file-id", new B2FileRetention( + B2FileRetentionMode.GOVERNANCE, 1000L)) + .build(); + final String requestJson = B2Json.toJsonOrThrowRuntime(request); + + final String expectedJson = "{\n" + + " \"bypassGovernance\": false,\n" + + " \"fileId\": \"file-id\",\n" + + " \"fileName\": \"fluffy.jpg\",\n" + + " \"fileRetention\": {\n" + + " \"mode\": \"governance\",\n" + + " \"retainUntilTimestamp\": 1000\n" + + " }\n" + + "}"; + + assertEquals(expectedJson, requestJson); + } + + @Test + public void testNormalWithBypassGovernance() { + final B2UpdateFileRetentionRequest request = B2UpdateFileRetentionRequest + .builder("fluffy.jpg", "file-id", new B2FileRetention( + B2FileRetentionMode.GOVERNANCE, 1000L)) + .setBypassGovernance(true) + .build(); + final String requestJson = B2Json.toJsonOrThrowRuntime(request); + + final String expectedJson = "{\n" + + " \"bypassGovernance\": true,\n" + + " \"fileId\": \"file-id\",\n" + + " \"fileName\": \"fluffy.jpg\",\n" + + " \"fileRetention\": {\n" + + " \"mode\": \"governance\",\n" + + " \"retainUntilTimestamp\": 1000\n" + + " }\n" + + "}"; + + assertEquals(expectedJson, requestJson); + } + + @Test + public void testWithNullModeAndRetainUntilTimestamp() { + final B2UpdateFileRetentionRequest request = B2UpdateFileRetentionRequest + .builder("fluffy.jpg", "file-id", B2FileRetention.NONE) + .setBypassGovernance(true) + .build(); + final String requestJson = B2Json.toJsonOrThrowRuntime(request); + + final String expectedJson = "{\n" + + " \"bypassGovernance\": true,\n" + + " \"fileId\": \"file-id\",\n" + + " \"fileName\": \"fluffy.jpg\",\n" + + " \"fileRetention\": {\n" + + " \"mode\": null,\n" + + " \"retainUntilTimestamp\": null\n" + + " }\n" + + "}"; + + assertEquals(expectedJson, requestJson); + } + + @Test + public void testInvalidModeThrowsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Invalid value for file retention mode"); + + final B2UpdateFileRetentionRequest ignored = B2UpdateFileRetentionRequest + .builder("fluffy.jpg", "file-id", new B2FileRetention("keeper", 1000L)) + .build(); + } + + @Test + public void testNullModeWithValidRetainUntilTimestampThrowsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Both file retention mode and retainUntilTimestamp are required if either is supplied"); + + final B2UpdateFileRetentionRequest ignored = B2UpdateFileRetentionRequest + .builder("fluffy.jpg", "file-id", new B2FileRetention(null, 1000L)) + .build(); + } + + @Test + public void testValidModeWithNullRetainUntilTimestampThrowsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Both file retention mode and retainUntilTimestamp are required if either is supplied"); + + final B2UpdateFileRetentionRequest ignored = B2UpdateFileRetentionRequest + .builder("fluffy.jpg", "file-id", new B2FileRetention(B2FileRetentionMode.GOVERNANCE, null)) + .build(); + } + +} \ No newline at end of file