This is an automated email from the ASF dual-hosted git repository.
rishabhdaim pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
The following commit(s) were added to refs/heads/trunk by this push:
new 77d365073c OAK-12241 : support for S3 tests for oak-blob-cloud to run
on S3Mock (#2964)
77d365073c is described below
commit 77d365073c5ca41d967e6dbc698852368d0bb83c
Author: Rishabh Kumar <[email protected]>
AuthorDate: Sat Jun 20 20:50:09 2026 +0530
OAK-12241 : support for S3 tests for oak-blob-cloud to run on S3Mock (#2964)
* OAK-12241 : updated S3 tests for oak-blob-cloud to run on S3Mock using
docker
* OAK-12241 : fixed test cases
* OAK-12241 : skip S3Mock small cache concurrency test
* OAK-12241 : skip S3Mock incompatible oak-it checks
* OAK-12241 : skip S3Mock record timestamp check
* OAK-12241 : fixed unit cases for oak-run
* OAK-12241 : remove S3-GCP emulator tests plan from PR
* OAK-12241 : addressed review comments by AI
* OAK-12241 : used executorcloser to close timeout executor
* OAK-12241 : fixed the error where we were reusing already closed
blobStore for validation
* OAK-12241 : removed timeout executor from production code
* OAK-12241: make blob fixture close idempotent
* OAK-12241: add blob wrapper regression test
* OAK-12241 : fixed sonar warnings
---
oak-blob-cloud/pom.xml | 5 +
.../jackrabbit/oak/blob/cloud/s3/S3Backend.java | 19 ++-
.../jackrabbit/oak/blob/cloud/s3/S3Constants.java | 8 +
.../apache/jackrabbit/oak/blob/cloud/s3/Utils.java | 32 +++-
.../cloud/s3/S3DataRecordAccessProviderIT.java | 8 +-
.../cloud/s3/S3DataRecordAccessProviderTest.java | 161 ++++++++++++++++++-
.../oak/blob/cloud/s3/S3DataStoreUtils.java | 128 +++++++++++----
.../oak/blob/cloud/s3/S3EmulatorSupport.java | 128 +++++++++++++++
.../oak/blob/cloud/s3/S3EmulatorSupportTest.java | 94 +++++++++++
.../jackrabbit/oak/blob/cloud/s3/S3MockRule.java | 109 +++++++++++++
.../oak/blob/cloud/s3/TestS3DSWithSmallCache.java | 10 ++
.../jackrabbit/oak/blob/cloud/s3/TestS3Ds.java | 25 +++
.../oak/blob/cloud/s3/TestS3DsCacheOff.java | 24 +++
.../jackrabbit/oak/blob/cloud/s3/UtilsTest.java | 171 ++++++++++++++++++++-
oak-doc/src/site/markdown/osgi_config.md | 4 +
.../plugins/document/S3SharedBlobStoreGCTest.java | 10 ++
.../document/SharedS3DataStoreUtilsTest.java | 18 +++
.../oak/run/cli/BlobStoreFixtureProvider.java | 22 ++-
.../oak/run/cli/ReadOnlyBlobStoreWrapper.java | 16 +-
.../oak/run/cli/ReadOnlyBlobStoreWrapperTest.java | 25 ++-
.../jackrabbit/oak/run/DataStoreCommandTest.java | 53 ++++++-
21 files changed, 1015 insertions(+), 55 deletions(-)
diff --git a/oak-blob-cloud/pom.xml b/oak-blob-cloud/pom.xml
index 9bccf0d064..5b70e6453f 100644
--- a/oak-blob-cloud/pom.xml
+++ b/oak-blob-cloud/pom.xml
@@ -487,6 +487,11 @@
<artifactId>org.osgi.service.cm</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3Backend.java
b/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3Backend.java
index b91119cc72..01cb97cb5f 100644
---
a/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3Backend.java
+++
b/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3Backend.java
@@ -133,6 +133,7 @@ public class S3Backend extends AbstractSharedBackend {
private static final int MAX_ALLOWABLE_UPLOAD_URIS = 10000; // AWS
limitation
private S3Client s3Client;
+ private S3Client s3PresignClient;
private S3AsyncClient s3AsyncClient;
// needed only in case of transfer acceleration is enabled for presigned
URIs
@@ -286,8 +287,10 @@ public class S3Backend extends AbstractSharedBackend {
}
void setBinaryTransferAccelerationEnabled(final boolean enabled) {
+ closePresignClient();
if (!enabled) {
- s3PresignService = Utils.createPresigner(s3Client, properties);
+ s3PresignClient = s3Client;
+ s3PresignService = Utils.createPresigner(s3Client, properties,
false);
return;
}
@@ -295,15 +298,24 @@ public class S3Backend extends AbstractSharedBackend {
b -> b.bucket(bucket).build());
if (Objects.equals(BucketAccelerateStatus.ENABLED,
accelerateConfig.status())) {
- s3PresignService =
Utils.createPresigner(Utils.openService(properties, true), properties);
+ s3PresignClient = Utils.openService(properties, true);
+ s3PresignService = Utils.createPresigner(s3PresignClient,
properties, true);
LOG.info("S3 Transfer Acceleration enabled for presigned URIs.");
} else {
LOG.warn("S3 Transfer Acceleration is not enabled on the bucket
{}. Will create normal, non-accelerated presigned URIs. To enable set {}",
bucket, S3Constants.PRESIGNED_URI_ENABLE_ACCELERATION);
- s3PresignService = Utils.createPresigner(s3Client, properties);
+ s3PresignClient = s3Client;
+ s3PresignService = Utils.createPresigner(s3Client, properties,
false);
}
}
+ private void closePresignClient() {
+ if (s3PresignClient != null && s3PresignClient != s3Client) {
+ s3PresignClient.close();
+ }
+ s3PresignClient = null;
+ }
+
/**
* It uploads file to Amazon S3. If file size is greater than 5MB, this
* method uses parallel concurrent connections to upload.
@@ -481,6 +493,7 @@ public class S3Backend extends AbstractSharedBackend {
if (s3PresignService != null) {
s3PresignService.close();
}
+ closePresignClient();
s3Client.close();
}
LOG.info("S3Backend closed.");
diff --git
a/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3Constants.java
b/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3Constants.java
index 7e70b6f13d..5f7238e3b2 100644
---
a/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3Constants.java
+++
b/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3Constants.java
@@ -169,6 +169,14 @@ public final class S3Constants {
*/
public static final String PRESIGNED_HTTP_DOWNLOAD_URI_VERIFY_EXISTS =
"presignedHttpDownloadURIVerifyExists";
+ /**
+ * Boolean property to enable path-style access for S3 clients ({@code
pathStyleAccess=true}).
+ * When enabled, requests use {@code http://endpoint/bucket/key} instead of
+ * {@code https://bucket.endpoint/key}. Defaults to {@code false}.
+ * Always enabled automatically when the remote storage mode is GCP.
+ */
+ public static final String PATH_STYLE_ACCESS = "pathStyleAccess";
+
/**
* private constructor so that class cannot initialized from outside.
*/
diff --git
a/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/Utils.java
b/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/Utils.java
index 6c479076a1..5686b8bad9 100644
---
a/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/Utils.java
+++
b/oak-blob-cloud/src/main/java/org/apache/jackrabbit/oak/blob/cloud/s3/Utils.java
@@ -116,7 +116,6 @@ public final class Utils {
* @return {@link S3Client}
*/
public static S3Client openService(final Properties prop, boolean accReq) {
-
S3ClientBuilder builder = S3Client.builder();
configureBuilder(builder, prop, accReq);
@@ -165,15 +164,24 @@ public final class Utils {
* @return a configured {@link S3Presigner} instance
*/
public static S3Presigner createPresigner(final S3Client s3Client, final
Properties props) {
+ return createPresigner(s3Client, props, false);
+ }
+
+ static S3Presigner createPresigner(final S3Client s3Client, final
Properties props, final boolean accReq) {
final boolean isGCP = Objects.equals(RemoteStorageMode.GCP,
props.get(S3Constants.MODE));
- return S3Presigner.builder().s3Client(s3Client)
+ String region = Utils.getRegion(props);
+ S3Presigner.Builder builder = S3Presigner.builder().s3Client(s3Client)
.credentialsProvider(Utils.getAwsCredentials(props))
- .region(Region.of(Utils.getRegion(props)))
+ .region(Region.of(region))
.serviceConfiguration(S3Configuration.builder()
- .pathStyleAccessEnabled(isGCP)
+
.pathStyleAccessEnabled(isPathStyleAccessEnabled(props, isGCP))
.chunkedEncodingEnabled(!isGCP)
- .build())
- .build();
+ .build());
+
+ if (accReq || hasCustomEndpoint(props)) {
+ builder.endpointOverride(getEndPointUri(props, accReq, region));
+ }
+ return builder.build();
}
/**
@@ -419,7 +427,6 @@ public final class Utils {
int apiTimeout = Math.max(connectionTimeOut * 10, 300000); // At least
5 minutes
ClientOverrideConfiguration.Builder builder =
ClientOverrideConfiguration.builder();
-
builder.retryStrategy(b -> b.maxAttempts(maxErrorRetry));
builder.apiCallTimeout(Duration.ofMillis(apiTimeout)); // Long timeout
for large uploads
builder.apiCallAttemptTimeout(Duration.ofMillis(connectionTimeOut));
// Per-attempt timeout
@@ -584,12 +591,21 @@ public final class Utils {
builder.serviceConfiguration(
S3Configuration.builder()
- .pathStyleAccessEnabled(isGCP) // enable for GCP
+ .pathStyleAccessEnabled(isPathStyleAccessEnabled(prop,
isGCP)) // always on for GCP; opt-in for S3 via pathStyleAccess property
.chunkedEncodingEnabled(!isGCP) // Disable for GCP
.useArnRegionEnabled(!isGCP) // Disable for GCP
.build());
}
+ private static boolean isPathStyleAccessEnabled(Properties prop, boolean
isGCP) {
+ return isGCP ||
Boolean.parseBoolean(prop.getProperty(S3Constants.PATH_STYLE_ACCESS, "false"));
+ }
+
+ private static boolean hasCustomEndpoint(Properties prop) {
+ String endpoint = prop.getProperty(S3Constants.S3_END_POINT);
+ return endpoint != null && !endpoint.isEmpty();
+ }
+
// Helper class to hold common Http config
private static class HttpClientConfig {
final int connectionTimeout;
diff --git
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataRecordAccessProviderIT.java
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataRecordAccessProviderIT.java
index fa36a39a29..0958b3d941 100644
---
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataRecordAccessProviderIT.java
+++
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataRecordAccessProviderIT.java
@@ -20,6 +20,7 @@ package org.apache.jackrabbit.oak.blob.cloud.s3;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
import java.net.URI;
import javax.net.ssl.HttpsURLConnection;
@@ -80,6 +81,11 @@ public class S3DataRecordAccessProviderIT extends
AbstractDataRecordAccessProvid
@Override
protected HttpsURLConnection getHttpsConnection(long length, URI uri)
throws IOException {
- return S3DataStoreUtils.getHttpsConnection(length, uri);
+ throw new UnsupportedOperationException("Upload is handled by
doHttpsUpload; see S3DataStoreUtils.doHttpUpload");
+ }
+
+ @Override
+ protected void doHttpsUpload(InputStream in, long contentLength, URI uri)
throws IOException {
+ S3DataStoreUtils.doHttpUpload(in, contentLength, uri);
}
}
diff --git
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataRecordAccessProviderTest.java
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataRecordAccessProviderTest.java
index cf59f79645..fc095532ad 100644
---
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataRecordAccessProviderTest.java
+++
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataRecordAccessProviderTest.java
@@ -22,6 +22,8 @@ import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.getFixtur
import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.getS3Config;
import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.getS3DataStore;
import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.isS3Configured;
+import static
org.apache.jackrabbit.oak.plugins.blob.datastore.DataStoreUtils.randomStream;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
@@ -29,13 +31,19 @@ import static org.junit.Assume.assumeTrue;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.net.HttpURLConnection;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import javax.net.ssl.HttpsURLConnection;
+import org.apache.jackrabbit.oak.api.blob.BlobDownloadOptions;
+import
org.apache.jackrabbit.oak.plugins.blob.datastore.directaccess.DataRecordAccessProvider;
+import
org.apache.jackrabbit.oak.plugins.blob.datastore.directaccess.DataRecordDownloadOptions;
import org.apache.jackrabbit.oak.spi.blob.data.DataIdentifier;
import org.apache.jackrabbit.oak.spi.blob.data.DataRecord;
import org.apache.jackrabbit.oak.spi.blob.data.DataStore;
@@ -131,7 +139,12 @@ public class S3DataRecordAccessProviderTest extends
AbstractDataRecordAccessProv
@Override
protected HttpsURLConnection getHttpsConnection(long length, URI uri)
throws IOException {
- return S3DataStoreUtils.getHttpsConnection(length, uri);
+ throw new UnsupportedOperationException("S3Mock uses HTTP; upload is
handled by doHttpsUpload");
+ }
+
+ @Override
+ protected void doHttpsUpload(InputStream in, long contentLength, URI uri)
throws IOException {
+ S3DataStoreUtils.doHttpUpload(in, contentLength, uri);
}
@Test
@@ -168,4 +181,150 @@ public class S3DataRecordAccessProviderTest extends
AbstractDataRecordAccessProv
upload = ds.initiateDataRecordUpload(uploadSize, -1);
assertEquals(expectedNumURIs, upload.getUploadURIs().size());
}
+
+ @Override
+ @Test
+ public void testGetDownloadURIIT() throws DataStoreException, IOException {
+ assumeTrue("SSE-C doesn't support presigned GET URLs",
!isSSECustomerKeyEncryption());
+ DataRecord dataRecord = null;
+ DataRecordAccessProvider store = getDataStore();
+ try {
+ InputStream testStream = randomStream(0, 256);
+ dataRecord = doSynchronousAddRecord((DataStore) store, testStream);
+ URI uri = store.getDownloadURI(dataRecord.getIdentifier(),
DataRecordDownloadOptions.DEFAULT);
+ HttpURLConnection conn = (HttpURLConnection)
uri.toURL().openConnection();
+ try {
+ conn.setRequestMethod("GET");
+ assertEquals(200, conn.getResponseCode());
+
+ testStream.reset();
+ try (InputStream responseStream = conn.getInputStream()) {
+ assertArrayEquals(testStream.readAllBytes(),
responseStream.readAllBytes());
+ }
+ } finally {
+ conn.disconnect();
+ }
+ } finally {
+ if (null != dataRecord) {
+ doDeleteRecord((DataStore) store, dataRecord.getIdentifier());
+ }
+ }
+ }
+
+ @Override
+ @Test
+ public void testGetDownloadURIWithCustomHeadersIT() throws
DataStoreException, IOException {
+ assumeTrue("SSE-C doesn't support presigned GET URLs",
!isSSECustomerKeyEncryption());
+ String umlautFilename = "Umläutfile.png";
+ String umlautfilenameIso88591 = new String(
+ StandardCharsets.ISO_8859_1.encode(umlautFilename).array(),
+ StandardCharsets.ISO_8859_1
+ );
+ List<String> fileNames = List.of(
+ "image.png",
+ "beautiful landscape.png",
+ "\"filename-with-double-quotes\".png",
+ "filename-with-one\"double-quote.jpg",
+ umlautFilename
+ );
+ List<String> iso88591FileNames = List.of(
+ "image.png",
+ "beautiful landscape.png",
+ "\\\"filename-with-double-quotes\\\".png",
+ "filename-with-one\\\"double-quote.jpg",
+ umlautfilenameIso88591
+ );
+ List<String> rfc8187FileNames = List.of(
+ "image.png",
+ "beautiful%20landscape.png",
+ "%22filename-with-double-quotes%22.png",
+ "filename-with-one%22double-quote.jpg",
+ "Uml%C3%A4utfile.png"
+ );
+
+ DataRecord dataRecord = null;
+ DataRecordAccessProvider store = getDataStore();
+ try {
+ InputStream testStream = randomStream(0, 256);
+ dataRecord = doSynchronousAddRecord((DataStore) store, testStream);
+ String mimeType = "image/png";
+ String dispositionType = "inline";
+ for (int i = 0; i < fileNames.size(); i++) {
+ String fileName = fileNames.get(i);
+ String iso88591FileName = iso88591FileNames.get(i);
+ String encodedFileName = rfc8187FileNames.get(i);
+ DataRecordDownloadOptions downloadOptions =
+ DataRecordDownloadOptions.fromBlobDownloadOptions(
+ new BlobDownloadOptions(mimeType, null,
fileName, dispositionType)
+ );
+ URI uri = store.getDownloadURI(dataRecord.getIdentifier(),
downloadOptions);
+
+ HttpURLConnection conn = (HttpURLConnection)
uri.toURL().openConnection();
+ try {
+ conn.setRequestMethod("GET");
+ assertEquals(200, conn.getResponseCode());
+
+ assertEquals(mimeType,
conn.getHeaderField("Content-Type"));
+ assertEquals(
+ String.format("%s; filename=\"%s\";
filename*=UTF-8''%s",
+ dispositionType, iso88591FileName,
encodedFileName),
+ conn.getHeaderField("Content-Disposition")
+ );
+
+ testStream.reset();
+ try (InputStream responseStream = conn.getInputStream()) {
+ assertArrayEquals(testStream.readAllBytes(),
responseStream.readAllBytes());
+ }
+ } finally {
+ conn.disconnect();
+ }
+ }
+ } finally {
+ if (null != dataRecord) {
+ doDeleteRecord((DataStore) store, dataRecord.getIdentifier());
+ }
+ }
+ }
+
+ @Override
+ @Test
+ public void testSinglePutDirectUploadIT() throws
DataRecordUploadException, DataStoreException, IOException {
+ super.testSinglePutDirectUploadIT();
+ }
+
+ @Override
+ @Test
+ public void testCompleteAlreadyUploadedBinaryReturnsSameBinaryIT() throws
DataStoreException, DataRecordUploadException, IOException {
+ super.testCompleteAlreadyUploadedBinaryReturnsSameBinaryIT();
+ }
+
+ @Override
+ @Test
+ public void testGetExpiredReadURIFailsIT() throws DataStoreException,
IOException {
+ assumeTrue("SSE-C doesn't support presigned GET URLs",
!isSSECustomerKeyEncryption());
+ assumeTrue("S3Mock does not enforce presigned URL expiry",
!S3EmulatorSupport.isAvailable());
+ DataRecord dataRecord = null;
+ ConfigurableDataRecordAccessProvider store = getDataStore();
+ try {
+ store.setDirectDownloadURIExpirySeconds(2);
+ dataRecord = doSynchronousAddRecord((DataStore) store,
randomStream(0, 256));
+ URI uri = store.getDownloadURI(dataRecord.getIdentifier(),
DataRecordDownloadOptions.DEFAULT);
+ try {
+ Thread.sleep(5 * 1000);
+ } catch (InterruptedException ignored) {
+ }
+ HttpURLConnection conn = (HttpURLConnection)
uri.toURL().openConnection();
+ try {
+ conn.setRequestMethod("GET");
+ assertEquals(403, conn.getResponseCode());
+ } finally {
+ conn.disconnect();
+ }
+ } finally {
+ if (null != dataRecord) {
+ doDeleteRecord((DataStore) store, dataRecord.getIdentifier());
+ }
+ store.setDirectDownloadURIExpirySeconds(expirySeconds);
+ }
+ }
}
diff --git
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataStoreUtils.java
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataStoreUtils.java
index 5b695ebe84..4cb3b6626a 100644
---
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataStoreUtils.java
+++
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3DataStoreUtils.java
@@ -22,15 +22,17 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
import java.net.URI;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
-import javax.net.ssl.HttpsURLConnection;
-
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.oak.spi.blob.data.DataStore;
import org.apache.jackrabbit.oak.commons.PropertiesUtil;
@@ -80,6 +82,14 @@ public class S3DataStoreUtils extends DataStoreUtils {
return true;
}
+ public static boolean isS3EmulatorConfigured() {
+ Properties props = getS3Config();
+ return Objects.equals(S3EmulatorSupport.ACCESS_KEY,
props.getProperty(S3Constants.ACCESS_KEY))
+ && Objects.equals(S3EmulatorSupport.SECRET_KEY,
props.getProperty(S3Constants.SECRET_KEY))
+ &&
"true".equals(props.getProperty(S3Constants.PATH_STYLE_ACCESS))
+ && props.getProperty(S3Constants.S3_END_POINT,
"").startsWith("http://127.0.0.1:");
+ }
+
/**
*
* @return true if SSE_C encryption is configured
@@ -125,9 +135,21 @@ public class S3DataStoreUtils extends DataStoreUtils {
props = new Properties();
props.putAll(filtered);
}
+ // Fall back to emulator when no real credentials are present.
+ if (!hasRealCredentials(props) && S3EmulatorSupport.isAvailable()) {
+ return S3EmulatorSupport.getEmulatorProperties();
+ }
return props;
}
+ private static boolean hasRealCredentials(Properties props) {
+ String accessKey = props.getProperty(S3Constants.ACCESS_KEY,
"").trim();
+ String secretKey = props.getProperty(S3Constants.SECRET_KEY,
"").trim();
+ String region = props.getProperty(S3Constants.S3_REGION, "").trim();
+ String endpoint = props.getProperty(S3Constants.S3_END_POINT,
"").trim();
+ return !accessKey.isEmpty() && !secretKey.isEmpty() &&
(!region.isEmpty() || !endpoint.isEmpty());
+ }
+
public static DataStore getS3DataStore(String className, Properties props,
String homeDir) throws Exception {
DataStore ds =
Class.forName(className).asSubclass(DataStore.class).newInstance();
PropertiesUtil.populate(ds, Utils.asMap(props), false);
@@ -150,38 +172,78 @@ public class S3DataStoreUtils extends DataStoreUtils {
S3BackendHelper.deleteBucketAndAbortMultipartUploads(bucket, date,
props);
}
- protected static HttpsURLConnection getHttpsConnection(long length, URI
uri) throws IOException {
- HttpsURLConnection conn = (HttpsURLConnection)
uri.toURL().openConnection();
- conn.setDoOutput(true);
- conn.setRequestMethod("PUT");
- conn.setRequestProperty("Content-Length", String.valueOf(length));
-
- final Properties props = getS3Config();
- DataEncryption encryption = Utils.getDataEncryption(props);
-
- switch (encryption) {
- case SSE_S3:
- conn.setRequestProperty("x-amz-server-side-encryption",
"AES256");
- System.out.println("Added SSE-S3 header: AES256");
- break;
- case SSE_KMS:
- conn.setRequestProperty("x-amz-server-side-encryption",
"aws:kms");
- System.out.println("Added SSE-KMS header: aws:kms");
- if
(Objects.nonNull(props.getProperty(S3Constants.S3_SSE_KMS_KEYID))) {
-
conn.setRequestProperty("x-amz-server-side-encryption-aws-kms-key-id",
props.getProperty(S3Constants.S3_SSE_KMS_KEYID));
- System.out.println("Added KMS Key ID: " +
props.getProperty(S3Constants.S3_SSE_KMS_KEYID));
- }
- break;
- case SSE_C:
- String customerKey =
props.getProperty(S3Constants.S3_SSE_C_KEY);
- String customerKeyMD5 = Utils.calculateMD5(customerKey);
-
conn.setRequestProperty("x-amz-server-side-encryption-customer-algorithm",
"AES256");
-
conn.setRequestProperty("x-amz-server-side-encryption-customer-key",
customerKey);
-
conn.setRequestProperty("x-amz-server-side-encryption-customer-key-MD5",
customerKeyMD5);
- break;
- case NONE:
- break;
+ protected static HttpURLConnection getHttpConnection(long length, URI uri)
throws IOException {
+ URLConnection raw = uri.toURL().openConnection();
+ if (!(raw instanceof HttpURLConnection)) {
+ throw new IOException("Expected HTTP(S) connection for URI: " +
uri);
+ }
+ HttpURLConnection conn = (HttpURLConnection) raw;
+ try {
+ conn.setDoOutput(true);
+ conn.setRequestMethod("PUT");
+ conn.setRequestProperty("Content-Length", String.valueOf(length));
+
+ final Properties props = getS3Config();
+ DataEncryption encryption = Utils.getDataEncryption(props);
+
+ switch (encryption) {
+ case SSE_S3:
+ conn.setRequestProperty("x-amz-server-side-encryption",
"AES256");
+ System.out.println("Added SSE-S3 header: AES256");
+ break;
+ case SSE_KMS:
+ conn.setRequestProperty("x-amz-server-side-encryption",
"aws:kms");
+ System.out.println("Added SSE-KMS header: aws:kms");
+ if
(Objects.nonNull(props.getProperty(S3Constants.S3_SSE_KMS_KEYID))) {
+
conn.setRequestProperty("x-amz-server-side-encryption-aws-kms-key-id",
props.getProperty(S3Constants.S3_SSE_KMS_KEYID));
+ System.out.println("Added KMS Key ID: " +
props.getProperty(S3Constants.S3_SSE_KMS_KEYID));
+ }
+ break;
+ case SSE_C:
+ String customerKey =
props.getProperty(S3Constants.S3_SSE_C_KEY);
+ String customerKeyMD5 = Utils.calculateMD5(customerKey);
+
conn.setRequestProperty("x-amz-server-side-encryption-customer-algorithm",
"AES256");
+
conn.setRequestProperty("x-amz-server-side-encryption-customer-key",
customerKey);
+
conn.setRequestProperty("x-amz-server-side-encryption-customer-key-MD5",
customerKeyMD5);
+ break;
+ case NONE:
+ break;
+ }
+ } catch (RuntimeException e) {
+ conn.disconnect();
+ throw e;
}
return conn;
}
+
+ protected static void doHttpUpload(InputStream in, long contentLength, URI
uri) throws IOException {
+ HttpURLConnection conn = getHttpConnection(contentLength, uri);
+ try {
+ try (OutputStream out = conn.getOutputStream()) {
+ IOUtils.copy(in, out);
+ }
+ int responseCode = conn.getResponseCode();
+ if (responseCode >= 400) {
+ String errorBody = getErrorBody(conn);
+ org.junit.Assert.fail(conn.getResponseMessage()
+ + (StringUtils.isEmpty(errorBody) ? "" : ": " +
errorBody));
+ }
+ } finally {
+ conn.disconnect();
+ }
+ }
+
+ private static String getErrorBody(HttpURLConnection conn) {
+ InputStream err = conn.getErrorStream();
+ if (err == null) {
+ return "";
+ }
+ try {
+ return IOUtils.toString(err, StandardCharsets.UTF_8);
+ } catch (IOException ignored) {
+ return "";
+ } finally {
+ IOUtils.closeQuietly(err);
+ }
+ }
}
diff --git
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3EmulatorSupport.java
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3EmulatorSupport.java
new file mode 100644
index 0000000000..d8fc9cdc84
--- /dev/null
+++
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3EmulatorSupport.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.blob.cloud.s3;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Properties;
+
+/**
+ * Lazy singleton that starts an {@link S3MockRule} Docker container on first
use and exposes
+ * S3 emulator properties for use in {@link S3DataStoreUtils#getS3Config()}.
+ *
+ * <p>GCP emulator coverage is intentionally out of scope because S3Mock is
not compatible
+ * with Oak's GCP client configuration.</p>
+ *
+ * <p>If Docker is unavailable, {@link #isAvailable()} returns {@code false}
and tests
+ * skip rather than fail.</p>
+ */
+public final class S3EmulatorSupport {
+
+ private static final Logger log =
LoggerFactory.getLogger(S3EmulatorSupport.class);
+
+ /** System property reserved for selecting emulator mode; only S3 is
currently supported. */
+ static final String S3_TEST_MODE_PROP = "s3.test.mode";
+
+ /** Dummy access key accepted by S3Mock. */
+ static final String ACCESS_KEY = "foo";
+
+ /** Dummy secret key accepted by S3Mock. */
+ static final String SECRET_KEY = "bar";
+
+ /** Default bucket name used when no test-specific bucket is configured. */
+ static final String DEFAULT_BUCKET = "s3mock-default-test-bucket";
+
+ private static volatile S3MockRule instance;
+ private static volatile boolean startupAttempted = false;
+
+ private S3EmulatorSupport() {}
+
+ /**
+ * Returns {@code true} if the S3Mock container started successfully and
is available for tests.
+ */
+ public static boolean isAvailable() {
+ if (isUnsupportedMode()) {
+ return false;
+ }
+ ensureStarted();
+ return instance != null;
+ }
+
+ /**
+ * Returns a {@link Properties} instance configured for the S3 emulator.
+ * Returns an empty {@link Properties} if the container is not available.
+ */
+ public static Properties getEmulatorProperties() {
+ if (isUnsupportedMode()) {
+ return new Properties();
+ }
+ ensureStarted();
+ S3MockRule rule = instance;
+ if (rule == null) {
+ return new Properties();
+ }
+ String endpoint = rule.getHttpEndpoint();
+ Properties props = new Properties();
+ props.setProperty(S3Constants.ACCESS_KEY, ACCESS_KEY);
+ props.setProperty(S3Constants.SECRET_KEY, SECRET_KEY);
+ props.setProperty(S3Constants.S3_BUCKET, DEFAULT_BUCKET);
+ props.setProperty(S3Constants.S3_CONN_PROTOCOL, "http");
+ props.setProperty(S3Constants.PATH_STYLE_ACCESS, "true");
+ props.setProperty(S3Constants.S3_ENCRYPTION,
S3Constants.S3_ENCRYPTION_NONE);
+ props.setProperty(S3Constants.S3_END_POINT, endpoint);
+ props.setProperty(S3Constants.S3_REGION, "us-east-1");
+ props.setProperty(S3Constants.S3_MAX_ERR_RETRY, "3");
+ props.setProperty(S3Constants.S3_CONN_TIMEOUT, "10000");
+ props.setProperty(S3Constants.S3_SOCK_TIMEOUT, "30000");
+ props.setProperty(S3Constants.S3_MAX_CONNS, "10");
+ return props;
+ }
+
+ private static void ensureStarted() {
+ if (isUnsupportedMode() || instance != null) {
+ return;
+ }
+ synchronized (S3EmulatorSupport.class) {
+ if (instance != null || startupAttempted) {
+ return;
+ }
+ startupAttempted = true;
+ S3MockRule rule = new S3MockRule();
+ try {
+ rule.before();
+ instance = rule;
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ rule.after();
+ } catch (Exception ignored) {
+ }
+ }));
+ } catch (Throwable e) {
+ try {
+ rule.after();
+ } catch (Exception ignored) {
+ }
+ log.warn("S3Mock container failed to start — emulator tests
will be skipped", e);
+ }
+ }
+ }
+
+ private static boolean isUnsupportedMode() {
+ return !"S3".equalsIgnoreCase(System.getProperty(S3_TEST_MODE_PROP,
"S3"));
+ }
+}
diff --git
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3EmulatorSupportTest.java
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3EmulatorSupportTest.java
new file mode 100644
index 0000000000..0e58146efc
--- /dev/null
+++
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3EmulatorSupportTest.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.blob.cloud.s3;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Properties;
+
+/**
+ * Tests for {@link S3EmulatorSupport}.
+ */
+public class S3EmulatorSupportTest {
+
+ private String previousMode;
+
+ @Before
+ public void setUp() {
+ previousMode = System.getProperty(S3EmulatorSupport.S3_TEST_MODE_PROP);
+ }
+
+ @After
+ public void tearDown() {
+ if (previousMode == null) {
+ System.clearProperty(S3EmulatorSupport.S3_TEST_MODE_PROP);
+ } else {
+ System.setProperty(S3EmulatorSupport.S3_TEST_MODE_PROP,
previousMode);
+ }
+ }
+
+ @Test
+ public void isAvailableReturnsFalseForUnsupportedGcpMode() {
+ System.setProperty(S3EmulatorSupport.S3_TEST_MODE_PROP, "GCP");
+ Assert.assertFalse(S3EmulatorSupport.isAvailable());
+ }
+
+ @Test
+ public void isAvailableReturnsTrueInS3ModeWhenDockerAvailable() {
+ System.clearProperty(S3EmulatorSupport.S3_TEST_MODE_PROP);
+ Assume.assumeTrue("Docker is not available",
S3EmulatorSupport.isAvailable());
+ Assert.assertTrue(S3EmulatorSupport.isAvailable());
+ }
+
+ @Test
+ public void getEmulatorPropertiesReturnsEmptyForUnsupportedGcpMode() {
+ System.setProperty(S3EmulatorSupport.S3_TEST_MODE_PROP, "GCP");
+ Assert.assertTrue(S3EmulatorSupport.getEmulatorProperties().isEmpty());
+ }
+
+ @Test
+ public void getEmulatorPropertiesReturnsEmptyWhenUnavailable() {
+ // When Docker is not available, isAvailable() is false and
getEmulatorProperties()
+ // must return empty properties rather than throwing.
+ Assume.assumeFalse("Docker is available",
S3EmulatorSupport.isAvailable());
+ Properties props = S3EmulatorSupport.getEmulatorProperties();
+ Assert.assertTrue("Expected empty properties when emulator is
unavailable", props.isEmpty());
+ }
+
+ @Test
+ public void getEmulatorPropertiesInS3ModeContainsRequiredKeys() {
+ System.clearProperty(S3EmulatorSupport.S3_TEST_MODE_PROP);
+ Assume.assumeTrue("Docker is not available",
S3EmulatorSupport.isAvailable());
+
+ Properties props = S3EmulatorSupport.getEmulatorProperties();
+ Assert.assertNotNull(props.getProperty(S3Constants.ACCESS_KEY));
+ Assert.assertNotNull(props.getProperty(S3Constants.SECRET_KEY));
+ Assert.assertNotNull(props.getProperty(S3Constants.S3_END_POINT));
+ Assert.assertNotNull(props.getProperty(S3Constants.S3_REGION));
+ Assert.assertNotNull(props.getProperty(S3Constants.S3_BUCKET));
+ Assert.assertEquals("true",
props.getProperty(S3Constants.PATH_STYLE_ACCESS));
+ Assert.assertNotNull(props.getProperty(S3Constants.S3_MAX_ERR_RETRY));
+ Assert.assertNotNull(props.getProperty(S3Constants.S3_CONN_TIMEOUT));
+ Assert.assertEquals("http",
props.getProperty(S3Constants.S3_CONN_PROTOCOL));
+ Assert.assertEquals(S3Constants.S3_ENCRYPTION_NONE,
props.getProperty(S3Constants.S3_ENCRYPTION));
+ Assert.assertNull("S3 mode must not set MODE",
props.get(S3Constants.MODE));
+ }
+}
diff --git
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3MockRule.java
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3MockRule.java
new file mode 100644
index 0000000000..d1878c7749
--- /dev/null
+++
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/S3MockRule.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.blob.cloud.s3;
+
+import org.junit.Assume;
+import org.junit.rules.ExternalResource;
+import org.junit.runner.Description;
+import org.junit.runners.model.MultipleFailureException;
+import org.junit.runners.model.Statement;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * JUnit rule that starts an Adobe S3Mock Docker container for emulator-based
S3 integration tests.
+ * Mirrors the pattern used by {@code AzuriteDockerRule} in
oak-blob-cloud-azure.
+ *
+ * <p>On Docker failure, the rule calls {@link Assume#assumeNoException} so
tests are skipped
+ * rather than failed, preserving the same behaviour as when no credentials
are configured.</p>
+ */
+public class S3MockRule extends ExternalResource {
+
+ private static final DockerImageName DOCKER_IMAGE =
DockerImageName.parse("adobe/s3mock:5.0.0");
+ static final int HTTP_PORT = 9090;
+ private final AtomicReference<Exception> startupException = new
AtomicReference<>();
+
+ private GenericContainer<?> container;
+
+ @Override
+ protected void before() throws Throwable {
+ startupException.set(null);
+ container = new GenericContainer<>(DOCKER_IMAGE)
+ .withExposedPorts(HTTP_PORT)
+ .withStartupTimeout(Duration.ofSeconds(60));
+ try {
+ container.start();
+ } catch (Exception e) {
+ startupException.set(e);
+ throw e;
+ }
+ }
+
+ @Override
+ protected void after() {
+ if (container != null) {
+ container.stop();
+ }
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ try {
+ before();
+ } catch (Exception e) {
+ Assume.assumeNoException(e);
+ throw e;
+ }
+ List<Throwable> errors = new ArrayList<>();
+ try {
+ base.evaluate();
+ } catch (Throwable t) {
+ errors.add(t);
+ } finally {
+ try {
+ after();
+ } catch (Throwable t) {
+ errors.add(t);
+ }
+ }
+ MultipleFailureException.assertEmpty(errors);
+ }
+ };
+ }
+
+ /**
+ * Returns the mapped HTTP port on the host for the running S3Mock
container.
+ */
+ public int getHttpPort() {
+ return container.getMappedPort(HTTP_PORT);
+ }
+
+ /**
+ * Returns the HTTP endpoint URL for the running S3Mock container, e.g.
{@code http://127.0.0.1:PORT}.
+ */
+ public String getHttpEndpoint() {
+ return "http://127.0.0.1:" + getHttpPort();
+ }
+}
diff --git
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3DSWithSmallCache.java
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3DSWithSmallCache.java
index 227e7019bc..bab5189dea 100644
---
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3DSWithSmallCache.java
+++
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3DSWithSmallCache.java
@@ -18,7 +18,9 @@ package org.apache.jackrabbit.oak.blob.cloud.s3;
import org.apache.jackrabbit.oak.spi.blob.data.CachingDataStore;
import org.apache.jackrabbit.oak.spi.blob.data.LocalCache;
+import org.junit.Assume;
import org.junit.Before;
+import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,4 +43,12 @@ public class TestS3DSWithSmallCache extends TestS3Ds {
props.setProperty("cacheSize", String.valueOf(dataLength * 10));
super.setUp();
}
+
+ @Override
+ @Test
+ public void testMultiThreaded() {
+ Assume.assumeTrue("S3Mock is not reliable with concurrent reads
through the very small local cache",
+ !S3DataStoreUtils.isS3EmulatorConfigured());
+ super.testMultiThreaded();
+ }
}
diff --git
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3Ds.java
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3Ds.java
index 00e79a73c2..5826a0de23 100644
---
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3Ds.java
+++
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3Ds.java
@@ -70,6 +70,7 @@ import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.getFixtur
import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.getS3Config;
import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.getS3DataStore;
import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.isS3Configured;
+import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.isS3EmulatorConfigured;
import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.isSseCustomerKeyEncrypted;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
@@ -315,6 +316,16 @@ public class TestS3Ds extends AbstractDataStoreTest {
public void testDeleteAllOlderThan() {
}
+ // deleteRecord removes the object from S3 but leaves the local file cache
intact.
+ // getRecordIfStored then returns the cached copy, causing a spurious
failure.
+ // The cache-off subclass (TestS3DsCacheOff) re-enables this test where it
is valid.
+ @Override
+ public void testDeleteRecord() {
+ Assume.assumeFalse("S3 local cache masks deletions; not verifiable
against the emulator",
+ isS3EmulatorConfigured());
+ super.testDeleteRecord();
+ }
+
// helper methods
private PutObjectResponse httpPut(@Nullable DataRecordUpload
uploadContext, InputStream inputstream, long length) {
@@ -381,6 +392,9 @@ public class TestS3Ds extends AbstractDataStoreTest {
private static String extractBucketFromUri(S3Client s3Client, URI uri) {
LOG.info("Extracting bucket from URI {}", uri);
+ if (isLocalPathStyleUri(uri)) {
+ return uri.getPath().substring(1).split("/", 2)[0];
+ }
S3Utilities s3Utilities = s3Client.utilities();
S3Uri s3Uri = s3Utilities.parseUri(uri);
@@ -390,10 +404,21 @@ public class TestS3Ds extends AbstractDataStoreTest {
private static String extractKeyFromUri(S3Client s3Client, URI uri) {
LOG.info("Extracting key from URI {}", uri);
+ if (isLocalPathStyleUri(uri)) {
+ return uri.getPath().substring(1).split("/", 2)[1];
+ }
S3Utilities s3Utilities = s3Client.utilities();
S3Uri s3Uri = s3Utilities.parseUri(uri);
return s3Uri.key().orElse(null);
}
+
+ private static boolean isLocalPathStyleUri(URI uri) {
+ String host = uri.getHost();
+ String path = uri.getPath();
+ return ("127.0.0.1".equals(host) || "localhost".equals(host))
+ && path != null
+ && path.substring(1).contains("/");
+ }
}
diff --git
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3DsCacheOff.java
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3DsCacheOff.java
index fb62f1ee8e..00969076ca 100644
---
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3DsCacheOff.java
+++
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/TestS3DsCacheOff.java
@@ -17,7 +17,9 @@
package org.apache.jackrabbit.oak.blob.cloud.s3;
import org.apache.jackrabbit.oak.spi.blob.data.CachingDataStore;
+import org.junit.Assume;
import org.junit.Before;
+import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,4 +42,26 @@ public class TestS3DsCacheOff extends TestS3Ds {
props.setProperty("cacheSize", "0");
super.setUp();
}
+
+ // Re-enable: with cache off, deleteRecord is immediately visible via S3
so the
+ // assertion that getRecordIfStored returns null after deletion is valid
here.
+ @Override
+ @Test
+ public void testDeleteRecord() {
+ try {
+ doDeleteRecordTest();
+ } catch (Exception e) {
+ throw new AssertionError("Failed to delete S3 record with cache
disabled", e);
+ }
+ }
+
+ // S3Backend updates duplicate records via CopyObject (copy-to-self).
S3Mock does not
+ // support that operation, so this test cannot run against the emulator.
+ @Override
+ @Test
+ public void testAddDuplicateRecord() {
+ Assume.assumeTrue("S3Mock does not support CopyObject used by
S3Backend for duplicate record updates",
+ !S3DataStoreUtils.isS3EmulatorConfigured());
+ super.testAddDuplicateRecord();
+ }
}
diff --git
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/UtilsTest.java
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/UtilsTest.java
index 580c111e3c..6c3eba2b20 100644
---
a/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/UtilsTest.java
+++
b/oak-blob-cloud/src/test/java/org/apache/jackrabbit/oak/blob/cloud/s3/UtilsTest.java
@@ -18,11 +18,23 @@
package org.apache.jackrabbit.oak.blob.cloud.s3;
import org.junit.Assert;
+import org.junit.Assume;
import org.junit.Test;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
import java.net.URI;
+import java.time.Duration;
import java.util.Properties;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import
software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
+import
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
+
/**
* Unit cases for Utils class
*/
@@ -201,4 +213,161 @@ public class UtilsTest {
Assert.assertEquals(S3Backend.RemoteStorageMode.GCP,
props.get(S3Constants.MODE));
}
-}
\ No newline at end of file
+ @Test
+ public void testSetRemoteStorageModeDefaultsPreSetGCPToS3() {
+ Properties props = new Properties();
+ props.setProperty("s3EndPoint", "http://127.0.0.1:9090");
+ props.put(S3Constants.MODE, S3Backend.RemoteStorageMode.GCP);
+ Utils.setRemoteStorageMode(props);
+ Assert.assertEquals(S3Backend.RemoteStorageMode.S3,
props.get(S3Constants.MODE));
+ }
+
+ @Test
+ public void testSetRemoteStorageModeDefaultsBlankModeToS3() {
+ Properties props = new Properties();
+ props.setProperty("s3EndPoint", "https://s3.amazonaws.com");
+ props.setProperty(S3Constants.MODE, "");
+ Utils.setRemoteStorageMode(props);
+ Assert.assertEquals(S3Backend.RemoteStorageMode.S3,
props.get(S3Constants.MODE));
+ }
+
+ @Test
+ public void testPathStyleAccessConstantHasExpectedStringValue() {
+ Assert.assertEquals("pathStyleAccess", S3Constants.PATH_STYLE_ACCESS);
+ }
+
+ @Test
+ public void testPathStyleAccessDefaultsToFalseWhenPropertyAbsent() {
+ Properties props = new Properties();
+
Assert.assertFalse(Boolean.parseBoolean(props.getProperty(S3Constants.PATH_STYLE_ACCESS,
"false")));
+ }
+
+ @Test
+ public void testPathStyleAccessTrueWhenPropertySetToTrue() {
+ Properties props = new Properties();
+ props.setProperty(S3Constants.PATH_STYLE_ACCESS, "true");
+
Assert.assertTrue(Boolean.parseBoolean(props.getProperty(S3Constants.PATH_STYLE_ACCESS,
"false")));
+ }
+
+ @Test
+ public void createPresignerUsesConfiguredEndpointOverride() {
+ Properties props = clientProperties();
+ props.setProperty(S3Constants.S3_END_POINT, "http://127.0.0.1:9090");
+ props.setProperty(S3Constants.PATH_STYLE_ACCESS, "true");
+
+ try (S3Client client = Utils.openService(props, false);
+ S3Presigner presigner = Utils.createPresigner(client, props)) {
+ GetObjectRequest getObject =
GetObjectRequest.builder().bucket("bucket").key("key").build();
+ GetObjectPresignRequest presignRequest =
GetObjectPresignRequest.builder()
+ .signatureDuration(Duration.ofMinutes(5))
+ .getObjectRequest(getObject)
+ .build();
+ PresignedGetObjectRequest presigned =
presigner.presignGetObject(presignRequest);
+
+ Assert.assertEquals("127.0.0.1", presigned.url().getHost());
+ Assert.assertEquals(9090, presigned.url().getPort());
+ Assert.assertEquals("/bucket/key", presigned.url().getPath());
+ }
+ }
+
+ @Test
+ public void createPresignerUsesAccelerationEndpointWhenRequested() {
+ Properties props = clientProperties();
+
+ try (S3Client client = Utils.openService(props, true);
+ S3Presigner presigner = Utils.createPresigner(client, props,
true)) {
+ GetObjectRequest getObject =
GetObjectRequest.builder().bucket("bucket").key("key").build();
+ GetObjectPresignRequest presignRequest =
GetObjectPresignRequest.builder()
+ .signatureDuration(Duration.ofMinutes(5))
+ .getObjectRequest(getObject)
+ .build();
+ PresignedGetObjectRequest presigned =
presigner.presignGetObject(presignRequest);
+
+
Assert.assertTrue(presigned.url().getHost().contains("s3-accelerate"));
+ }
+ }
+
+ @Test
+ public void
isS3ConfiguredReturnsFalseForPartialCredentialsWithNoRegionOrEndpoint() throws
IOException {
+ // accessKey + secretKey without region or endpoint must not count as
real credentials
+ File tmp = File.createTempFile("s3test-partial-creds", ".properties");
+ String previousConfig = System.getProperty("s3.config");
+ try {
+ Properties partial = new Properties();
+ partial.setProperty(S3Constants.ACCESS_KEY, "somekey");
+ partial.setProperty(S3Constants.SECRET_KEY, "somesecret");
+ try (OutputStream out = new FileOutputStream(tmp)) {
+ partial.store(out, null);
+ }
+ System.setProperty("s3.config", tmp.getAbsolutePath());
+ // Skip when emulator is available — getS3Config() would fall back
to it, making isS3Configured() true
+ Assume.assumeFalse("Emulator available — would override partial
creds", S3EmulatorSupport.isAvailable());
+ Assert.assertFalse("Partial credentials (no region/endpoint) must
not be treated as real",
+ S3DataStoreUtils.isS3Configured());
+ } finally {
+ if (previousConfig == null) {
+ System.clearProperty("s3.config");
+ } else {
+ System.setProperty("s3.config", previousConfig);
+ }
+ tmp.delete();
+ }
+ }
+
+ @Test
+ public void isS3EmulatorConfiguredReturnsTrueForEmulatorProperties()
throws IOException {
+ Properties props = clientProperties();
+ props.setProperty(S3Constants.ACCESS_KEY,
S3EmulatorSupport.ACCESS_KEY);
+ props.setProperty(S3Constants.SECRET_KEY,
S3EmulatorSupport.SECRET_KEY);
+ props.setProperty(S3Constants.S3_END_POINT, "http://127.0.0.1:9090");
+ props.setProperty(S3Constants.PATH_STYLE_ACCESS, "true");
+
+ withS3Config(props, () ->
Assert.assertTrue(S3DataStoreUtils.isS3EmulatorConfigured()));
+ }
+
+ @Test
+ public void isS3EmulatorConfiguredReturnsFalseForRealS3Properties() throws
IOException {
+ Properties props = clientProperties();
+ props.setProperty(S3Constants.S3_END_POINT,
"https://s3.amazonaws.com");
+
+ withS3Config(props, () ->
Assert.assertFalse(S3DataStoreUtils.isS3EmulatorConfigured()));
+ }
+
+ private static Properties clientProperties() {
+ Properties props = new Properties();
+ props.setProperty(S3Constants.ACCESS_KEY, "accessKey");
+ props.setProperty(S3Constants.SECRET_KEY, "secretKey");
+ props.setProperty(S3Constants.S3_REGION, "us-east-1");
+ props.setProperty(S3Constants.S3_MAX_ERR_RETRY, "3");
+ props.setProperty(S3Constants.S3_CONN_TIMEOUT, "1000");
+ props.setProperty(S3Constants.S3_SOCK_TIMEOUT, "1000");
+ props.setProperty(S3Constants.S3_MAX_CONNS, "2");
+ props.setProperty(S3Constants.S3_CONN_PROTOCOL, "http");
+ props.setProperty(S3Constants.S3_CROSS_REGION_ACCESS, "false");
+ return props;
+ }
+
+ private static void withS3Config(Properties props, ConfigAssertion
assertion) throws IOException {
+ File tmp = File.createTempFile("s3test-config", ".properties");
+ String previousConfig = System.getProperty("s3.config");
+ try {
+ try (OutputStream out = new FileOutputStream(tmp)) {
+ props.store(out, null);
+ }
+ System.setProperty("s3.config", tmp.getAbsolutePath());
+ assertion.run();
+ } finally {
+ if (previousConfig == null) {
+ System.clearProperty("s3.config");
+ } else {
+ System.setProperty("s3.config", previousConfig);
+ }
+ tmp.delete();
+ }
+ }
+
+ private interface ConfigAssertion {
+ void run();
+ }
+
+}
diff --git a/oak-doc/src/site/markdown/osgi_config.md
b/oak-doc/src/site/markdown/osgi_config.md
index 67c471500a..0b8ae52598 100644
--- a/oak-doc/src/site/markdown/osgi_config.md
+++ b/oak-doc/src/site/markdown/osgi_config.md
@@ -367,6 +367,10 @@ s3Region
s3EndPoint
: S3 rest API endpoint. Can help reduce latency of redirection from standard
endpoint if a different region configured.
+pathStyleAccess
+: Default - `false`
+: Set to `true` to use path-style S3 requests (`http://endpoint/bucket/key`)
instead of virtual-host-style requests. This is useful for S3-compatible
endpoints that do not support bucket names in the host. Note: always enabled
automatically when the remote storage mode is GCP; the property has no effect
in that case.
+
connectionTimeout
: S3 connection timeout. See [AWS S3
documentation](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/section-client-configuration.html).
diff --git
a/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/document/S3SharedBlobStoreGCTest.java
b/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/document/S3SharedBlobStoreGCTest.java
index 91f5e1be73..aea3ac203b 100644
---
a/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/document/S3SharedBlobStoreGCTest.java
+++
b/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/document/S3SharedBlobStoreGCTest.java
@@ -27,9 +27,11 @@ import
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils;
import org.apache.jackrabbit.oak.plugins.blob.datastore.DataStoreBlobStore;
import org.junit.After;
import org.junit.BeforeClass;
+import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
/**
@@ -71,6 +73,14 @@ public class S3SharedBlobStoreGCTest extends
SharedBlobStoreGCTest {
S3DataStoreUtils.getS3DataStore(s3Class, props,
rootFolder.getAbsolutePath()));
}
+ @Override
+ @Test
+ public void testGCWithNodeSpecialChars() throws Exception {
+ assumeFalse("S3Mock is not reliable for this shared-blob GC
special-character write path",
+ S3DataStoreUtils.isS3EmulatorConfigured());
+ super.testGCWithNodeSpecialChars();
+ }
+
@Override
protected void sleep() throws InterruptedException {
if (S3DataStoreUtils.isS3Configured()) {
diff --git
a/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/document/SharedS3DataStoreUtilsTest.java
b/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/document/SharedS3DataStoreUtilsTest.java
index dcc76af645..7a087039a2 100644
---
a/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/document/SharedS3DataStoreUtilsTest.java
+++
b/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/document/SharedS3DataStoreUtilsTest.java
@@ -30,12 +30,14 @@ import
org.apache.jackrabbit.oak.plugins.blob.SharedDataStoreUtilsTest;
import org.apache.jackrabbit.oak.plugins.blob.datastore.DataStoreBlobStore;
import org.junit.After;
import org.junit.BeforeClass;
+import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static
org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils.deleteBucket;
+import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
/**
@@ -71,6 +73,22 @@ public class SharedS3DataStoreUtilsTest extends
SharedDataStoreUtilsTest {
S3DataStoreUtils.getS3DataStore(s3Class, props,
rootFolder.getAbsolutePath()));
}
+ @Override
+ @Test
+ public void test() throws Exception {
+ assumeFalse("S3Mock returns metadata lastModified values with second
precision",
+ S3DataStoreUtils.isS3EmulatorConfigured());
+ super.test();
+ }
+
+ @Override
+ @Test
+ public void testStreamFromGetAllRecords() throws Exception {
+ assumeFalse("S3Mock returns getAllRecords lastModified values with
second precision",
+ S3DataStoreUtils.isS3EmulatorConfigured());
+ super.testStreamFromGetAllRecords();
+ }
+
@After
public void close() throws IOException {
try {
diff --git
a/oak-run-commons/src/main/java/org/apache/jackrabbit/oak/run/cli/BlobStoreFixtureProvider.java
b/oak-run-commons/src/main/java/org/apache/jackrabbit/oak/run/cli/BlobStoreFixtureProvider.java
index 0f1a3d2e0f..083754c8a5 100644
---
a/oak-run-commons/src/main/java/org/apache/jackrabbit/oak/run/cli/BlobStoreFixtureProvider.java
+++
b/oak-run-commons/src/main/java/org/apache/jackrabbit/oak/run/cli/BlobStoreFixtureProvider.java
@@ -29,6 +29,7 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
+import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
@@ -100,7 +101,8 @@ public class BlobStoreFixtureProvider {
}
delegate.init(null);
}
- DataStoreBlobStore blobStore = new DataStoreBlobStore(delegate);
+ // The command fixture and DocumentNodeStore can both tear down the
same blob store instance.
+ DataStoreBlobStore blobStore = new
IdempotentDataStoreBlobStore(delegate);
return new DataStoreFixture(blobStore, closer,
(!options.getCommonOpts().isReadWrite() && !bsopts.isReadWrite()));
}
@@ -140,8 +142,8 @@ public class BlobStoreFixtureProvider {
@Override
public void close() throws IOException {
- closer.close();
try {
+ closer.close();
blobStore.close();
} catch (DataStoreException e) {
throw new IOException(e);
@@ -149,6 +151,22 @@ public class BlobStoreFixtureProvider {
}
}
+ private static final class IdempotentDataStoreBlobStore extends
DataStoreBlobStore {
+ private final AtomicBoolean closed = new AtomicBoolean();
+
+ private IdempotentDataStoreBlobStore(DataStore delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public void close() throws DataStoreException {
+ // DocumentNodeStore and the outer fixture can both own teardown
of the same blob store.
+ if (closed.compareAndSet(false, true)) {
+ super.close();
+ }
+ }
+ }
+
private static Properties loadAndTransformProps(String cfgPath) throws
IOException {
Dictionary dict = ConfigurationHandler.read(new
FileInputStream(cfgPath));
Properties props = new Properties();
diff --git
a/oak-run-commons/src/main/java/org/apache/jackrabbit/oak/run/cli/ReadOnlyBlobStoreWrapper.java
b/oak-run-commons/src/main/java/org/apache/jackrabbit/oak/run/cli/ReadOnlyBlobStoreWrapper.java
index 782d0be246..f2a9414a89 100644
---
a/oak-run-commons/src/main/java/org/apache/jackrabbit/oak/run/cli/ReadOnlyBlobStoreWrapper.java
+++
b/oak-run-commons/src/main/java/org/apache/jackrabbit/oak/run/cli/ReadOnlyBlobStoreWrapper.java
@@ -21,6 +21,7 @@ package org.apache.jackrabbit.oak.run.cli;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
+import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.jackrabbit.oak.spi.blob.BlobStore;
@@ -28,12 +29,25 @@ import org.apache.jackrabbit.oak.spi.blob.BlobStore;
class ReadOnlyBlobStoreWrapper {
public static BlobStore wrap(BlobStore delegate){
- Class[] interfaces = delegate.getClass().getInterfaces();
+ Class<?>[] interfaces = getAllInterfaces(delegate);
return (BlobStore)
Proxy.newProxyInstance(ReadOnlyBlobStoreWrapper.class.getClassLoader(),
interfaces,
new ReadOnlyHandler(delegate));
}
+ private static Class<?>[] getAllInterfaces(BlobStore delegate) {
+ Set<Class<?>> interfaces = new LinkedHashSet<>();
+ Class<?> type = delegate.getClass();
+ while (type != null) {
+ for (Class<?> iface : type.getInterfaces()) {
+ interfaces.add(iface);
+ }
+ type = type.getSuperclass();
+ }
+ interfaces.add(BlobStore.class);
+ return interfaces.toArray(new Class<?>[0]);
+ }
+
private static class ReadOnlyHandler implements InvocationHandler {
private final Set<String> writableMethods = Set.of(
"writeBlob", //BlobStore
diff --git
a/oak-run-commons/src/test/java/org/apache/jackrabbit/oak/run/cli/ReadOnlyBlobStoreWrapperTest.java
b/oak-run-commons/src/test/java/org/apache/jackrabbit/oak/run/cli/ReadOnlyBlobStoreWrapperTest.java
index 1d6f19689c..7f9c51a060 100644
---
a/oak-run-commons/src/test/java/org/apache/jackrabbit/oak/run/cli/ReadOnlyBlobStoreWrapperTest.java
+++
b/oak-run-commons/src/test/java/org/apache/jackrabbit/oak/run/cli/ReadOnlyBlobStoreWrapperTest.java
@@ -23,9 +23,12 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
+import org.apache.jackrabbit.oak.plugins.blob.BlobTrackingStore;
import org.apache.jackrabbit.oak.spi.blob.data.FileDataStore;
import org.apache.jackrabbit.oak.plugins.blob.datastore.DataStoreBlobStore;
+import org.apache.jackrabbit.oak.plugins.blob.datastore.TypedDataStore;
import org.apache.jackrabbit.oak.spi.blob.BlobStore;
+import org.apache.jackrabbit.oak.spi.blob.GarbageCollectableBlobStore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -62,4 +65,24 @@ public class ReadOnlyBlobStoreWrapperTest {
}
-}
\ No newline at end of file
+ @Test
+ public void readOnlySubclassPreservesBlobStoreInterfaces() {
+ FileDataStore fds = new FileDataStore();
+ fds.setPath(temporaryFolder.getRoot().getAbsolutePath());
+ fds.init(null);
+
+ BlobStore readOnly = ReadOnlyBlobStoreWrapper.wrap(new
DerivedDataStoreBlobStore(fds));
+
+ assertTrue(readOnly instanceof BlobStore);
+ assertTrue(readOnly instanceof GarbageCollectableBlobStore);
+ assertTrue(readOnly instanceof TypedDataStore);
+ assertTrue(readOnly instanceof BlobTrackingStore);
+ }
+
+ private static final class DerivedDataStoreBlobStore extends
DataStoreBlobStore {
+ private DerivedDataStoreBlobStore(FileDataStore delegate) {
+ super(delegate);
+ }
+ }
+
+}
diff --git
a/oak-run/src/test/java/org/apache/jackrabbit/oak/run/DataStoreCommandTest.java
b/oak-run/src/test/java/org/apache/jackrabbit/oak/run/DataStoreCommandTest.java
index 0a477cdfa0..9d29d89322 100644
---
a/oak-run/src/test/java/org/apache/jackrabbit/oak/run/DataStoreCommandTest.java
+++
b/oak-run/src/test/java/org/apache/jackrabbit/oak/run/DataStoreCommandTest.java
@@ -27,6 +27,8 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
+import java.util.Dictionary;
+import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -843,10 +845,16 @@ public class DataStoreCommandTest {
}
Set<String> blobsBeforeGc = SetUtils.difference(data.added,
data.missingDataStore);
- if (maxAge <= 0) {
- assertEquals(SetUtils.difference(blobsBeforeGc, data.deleted),
blobs(setupDataStore));
- } else {
- assertEquals(blobsBeforeGc, blobs(setupDataStore));
+ // storeFixture.close() may dispose the original blob store, so
validate through a fresh handle.
+ DataStoreBlobStore validationBlobStore =
blobFixture.openDataStore(temporaryFolder.newFolder().getAbsolutePath());
+ try {
+ if (maxAge <= 0) {
+ assertEquals(SetUtils.difference(blobsBeforeGc, data.deleted),
blobs(validationBlobStore));
+ } else {
+ assertEquals(blobsBeforeGc, blobs(validationBlobStore));
+ }
+ } finally {
+ validationBlobStore.close();
}
}
@@ -967,6 +975,21 @@ public class DataStoreCommandTest {
return cfgFile.getAbsolutePath();
}
+ // Reuse the same OSGi config format as the command under test when
reopening a datastore.
+ private static Properties readConfigProperties(String cfgFilePath) throws
IOException {
+ Dictionary<?, ?> dict;
+ try (FileInputStream fis = new FileInputStream(cfgFilePath)) {
+ dict = ConfigurationHandler.read(fis);
+ }
+ Properties props = new Properties();
+ Enumeration<?> keys = dict.keys();
+ while (keys.hasMoreElements()) {
+ String key = (String) keys.nextElement();
+ props.put(key, dict.get(key));
+ }
+ return props;
+ }
+
private static Set<String> encodedIdsAndPath(Set<String> ids, Type
dsOption, Map<String, String> idToNodes,
boolean encodeId) {
return SetUtils.toSet(IteratorUtils.transform(ids.iterator(), input ->
String.join(",", encodeId ? encodeId(input, dsOption) : input,
idToNodes.get(input))));
@@ -1244,6 +1267,8 @@ public class DataStoreCommandTest {
DataStoreBlobStore getDataStore();
+ DataStoreBlobStore openDataStore(String homeDir) throws Exception;
+
String getConfigPath();
Type getType();
@@ -1281,6 +1306,12 @@ public class DataStoreCommandTest {
return blobStore;
}
+ @Override public DataStoreBlobStore openDataStore(String homeDir)
throws Exception {
+ Properties props = readConfigProperties(cfgFilePath);
+ DataStore ds =
S3DataStoreUtils.getS3DataStore(S3DataStoreUtils.getFixtures().get(0), props,
homeDir);
+ return new DataStoreBlobStore(ds);
+ }
+
@Override public String getConfigPath() {
return cfgFilePath;
}
@@ -1324,6 +1355,12 @@ public class DataStoreCommandTest {
return blobStore;
}
+ @Override public DataStoreBlobStore openDataStore(String homeDir)
throws Exception {
+ Properties props = readConfigProperties(cfgFilePath);
+ DataStore ds = AzureDataStoreUtils.getAzureDataStore(props,
homeDir);
+ return new DataStoreBlobStore(ds);
+ }
+
@Override public String getConfigPath() {
return cfgFilePath;
}
@@ -1370,6 +1407,14 @@ public class DataStoreCommandTest {
return blobStore;
}
+ @Override public DataStoreBlobStore openDataStore(String homeDir)
throws Exception {
+ OakFileDataStore delegate = new OakFileDataStore();
+ Properties props = readConfigProperties(cfgFilePath);
+ delegate.setPath(props.getProperty("path"));
+ delegate.init(null);
+ return new DataStoreBlobStore(delegate);
+ }
+
@Override public String getConfigPath() {
return cfgFilePath;
}