Skip to content

Commit 4171b5b

Browse files
refactor(S3BitStore): Remove maxConnections and connectionTimeout properties; add support for AWS session tokens
refL DSC-2621
1 parent 8bc87cf commit 4171b5b

File tree

3 files changed

+73
-84
lines changed

3 files changed

+73
-84
lines changed

dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java

Lines changed: 73 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.springframework.beans.factory.annotation.Autowired;
4848
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
4949
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
50+
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
5051
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
5152
import software.amazon.awssdk.awscore.exception.AwsServiceException;
5253
import software.amazon.awssdk.core.async.AsyncRequestBody;
@@ -71,34 +72,23 @@
7172
*/
7273

7374
public class S3BitStoreService extends BaseBitStoreService {
75+
protected static final String DEFAULT_BUCKET_PREFIX = "dspace-asset-";
7476
public static final long DEFAULT_EXPIRATION = Duration.ofMinutes(2).toSeconds();
77+
// Prefix indicating a registered bitstream
78+
protected final String REGISTERED_FLAG = "-R";
79+
/**
80+
* log4j log
81+
*/
82+
private static final Logger log = LogManager.getLogger(S3BitStoreService.class);
83+
7584
public static final String TEMP_PREFIX = "s3-virtual-path";
7685
public static final String TEMP_SUFFIX = "temp";
77-
protected static final String DEFAULT_BUCKET_PREFIX = "dspace-asset-";
78-
// These settings control the way an identifier is hashed into
79-
// directory and file names
80-
//
81-
// With digitsPerLevel 2 and directoryLevels 3, an identifier
82-
// like 12345678901234567890 turns into the relative name
83-
// /12/34/56/12345678901234567890.
84-
//
85-
// You should not change these settings if you have data in the
86-
// asset store, as the BitstreamStorageManager will be unable
87-
// to find your existing data.
88-
protected static final int digitsPerLevel = 2;
89-
protected static final int directoryLevels = 3;
86+
9087
/**
9188
* Checksum algorithm
9289
*/
9390
static final String CSA = "MD5";
94-
/**
95-
* log4j log
96-
*/
97-
private static final Logger log = LogManager.getLogger(S3BitStoreService.class);
98-
private static final ConfigurationService configurationService
99-
= DSpaceServicesFactory.getInstance().getConfigurationService();
100-
// Prefix indicating a registered bitstream
101-
protected final String REGISTERED_FLAG = "-R";
91+
10292
private boolean enabled = false;
10393
/**
10494
* Override AWS endpoint if not null
@@ -113,8 +103,7 @@ public class S3BitStoreService extends BaseBitStoreService {
113103
private long minPartSizeBytes = 8 * 1024 * 1024L;
114104
private ChecksumAlgorithm s3ChecksumAlgorithm = ChecksumAlgorithm.CRC32;
115105
private Integer maxConcurrency = null;
116-
private Integer maxConnections;
117-
private Integer connectionTimeout;
106+
118107
/**
119108
* container for all the assets
120109
*/
@@ -128,6 +117,9 @@ public class S3BitStoreService extends BaseBitStoreService {
128117
*/
129118
private S3AsyncClient s3AsyncClient = null;
130119

120+
private static final ConfigurationService configurationService
121+
= DSpaceServicesFactory.getInstance().getConfigurationService();
122+
131123
public S3BitStoreService() {
132124
}
133125

@@ -140,24 +132,46 @@ protected S3BitStoreService(S3AsyncClient s3AsyncClient) {
140132
this.s3AsyncClient = s3AsyncClient;
141133
}
142134

135+
/**
136+
* Creates an AWS credentials provider that supports both basic credentials and session tokens.
137+
*
138+
* @param accessKey AWS access key
139+
* @param secretKey AWS secret key
140+
* @param sessionToken AWS session token (optional)
141+
* @return StaticCredentialsProvider with appropriate credentials
142+
*/
143+
protected static StaticCredentialsProvider createCredentialsProvider(
144+
String accessKey, String secretKey, String sessionToken
145+
) {
146+
if (StringUtils.isNotBlank(sessionToken)) {
147+
return StaticCredentialsProvider.create(
148+
AwsSessionCredentials.create(accessKey, secretKey, sessionToken)
149+
);
150+
} else {
151+
return StaticCredentialsProvider.create(
152+
AwsBasicCredentials.create(accessKey, secretKey)
153+
);
154+
}
155+
}
156+
143157
/**
144158
* Utility method for generate AmazonS3 builder
145159
*
146-
* @param regions wanted regions in client
147-
* @param awsCredentials credentials of the client
148-
* @param endpoint custom AWS endpoint
160+
* @param region wanted regions in client
161+
* @param credentialsProvider credentials of the client
162+
* @param endpoint custom AWS endpoint
149163
* @param targetThroughput target throughput in Gbps
150-
* @param minPartSize minimum part size in bytes
151-
* @param maxConcurrency maximum number of concurrent requests
164+
* @param minPartSize minimum part size in bytes
165+
* @param maxConcurrency maximum number of concurrent requests
152166
* @return builder with the specified parameters
153167
*/
154168
protected static Supplier<S3AsyncClient> amazonClientBuilderBy(
155-
Region region,
156-
AwsCredentialsProvider credentialsProvider,
157-
String endpoint,
158-
double targetThroughput,
159-
long minPartSize,
160-
Integer maxConcurrency
169+
Region region,
170+
AwsCredentialsProvider credentialsProvider,
171+
String endpoint,
172+
double targetThroughput,
173+
long minPartSize,
174+
Integer maxConcurrency
161175
) {
162176
return () -> {
163177
S3CrtAsyncClientBuilder crtBuilder = S3AsyncClient.crtBuilder();
@@ -242,10 +256,6 @@ public boolean isEnabled() {
242256
return this.enabled;
243257
}
244258

245-
public void setEnabled(boolean enabled) {
246-
this.enabled = enabled;
247-
}
248-
249259
/**
250260
* Initialize the asset store
251261
* S3 Requires:
@@ -272,24 +282,25 @@ public void init() throws IOException {
272282
}
273283
}
274284

275-
StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider
276-
.create(AwsBasicCredentials.create(getAwsAccessKey(), getAwsSecretKey()));
285+
StaticCredentialsProvider credentialsProvider = createCredentialsProvider(
286+
getAwsAccessKey(), getAwsSecretKey(), getAwsSessionToken());
287+
277288
// init client
278289
s3AsyncClient = FunctionalUtils.getDefaultOrBuild(
279-
this.s3AsyncClient,
280-
amazonClientBuilderBy(
281-
region,
282-
credentialsProvider, endpoint, targetThroughputGbps,
283-
minPartSizeBytes, maxConcurrency)
284-
);
290+
this.s3AsyncClient,
291+
amazonClientBuilderBy(
292+
region,
293+
credentialsProvider, endpoint, targetThroughputGbps,
294+
minPartSizeBytes, maxConcurrency)
295+
);
285296

286297
log.warn("S3 Region set to: " + region.id());
287298
} else {
288299
log.info("Using a IAM role or aws environment credentials");
289300
s3AsyncClient = FunctionalUtils.getDefaultOrBuild(
290-
this.s3AsyncClient,
291-
amazonClientBuilderBy(null, null, endpoint, targetThroughputGbps,
292-
minPartSizeBytes, maxConcurrency));
301+
this.s3AsyncClient,
302+
amazonClientBuilderBy(null, null , endpoint, targetThroughputGbps,
303+
minPartSizeBytes, maxConcurrency));
293304
}
294305

295306
// bucket name
@@ -359,7 +370,7 @@ public InputStream get(Bitstream bitstream) throws IOException {
359370

360371
try {
361372
return s3AsyncClient.getObject(r -> r.bucket(bucketName).key(objectKey),
362-
AsyncResponseTransformer.toBlockingInputStream()).join();
373+
AsyncResponseTransformer.toBlockingInputStream()).join();
363374
} catch (CompletionException e) {
364375
throw new IOException(e.getCause());
365376
}
@@ -384,11 +395,11 @@ public void put(Bitstream bitstream, InputStream in) throws IOException {
384395
try (DigestInputStream dis = new DigestInputStream(in, MessageDigest.getInstance(CSA))) {
385396
AsyncRequestBody body = AsyncRequestBody.fromInputStream(dis, null, executor);
386397

387-
s3AsyncClient.putObject(b -> b.bucket(bucketName).key(key).checksumAlgorithm(s3ChecksumAlgorithm),
388-
body).join();
398+
s3AsyncClient.putObject(b -> b.bucket(bucketName).key(key).checksumAlgorithm(s3ChecksumAlgorithm),
399+
body).join();
389400

390401
bitstream.setSizeBytes(s3AsyncClient.headObject(r -> r.bucket(bucketName).key(key))
391-
.join().contentLength());
402+
.join().contentLength());
392403

393404
// we cannot use the S3 ETAG here as it could be not a MD5 in case of multipart upload (large files) or if
394405
// the bucket is encrypted
@@ -476,7 +487,7 @@ public void remove(Bitstream bitstream) throws IOException {
476487
String key = getFullKey(bitstream.getInternalId());
477488
try {
478489
s3AsyncClient.deleteObject(r -> r.bucket(bucketName).key(key)).join();
479-
} catch (CompletionException e) {
490+
} catch (CompletionException e) {
480491
log.error("remove(" + key + ")", e.getCause());
481492
throw new IOException(e.getCause());
482493
}
@@ -503,7 +514,7 @@ public String getFullKey(String id) {
503514

504515
if (log.isDebugEnabled()) {
505516
log.debug("S3 filepath for " + id + " is "
506-
+ bufFilename.toString());
517+
+ bufFilename.toString());
507518
}
508519

509520
return bufFilename.toString();
@@ -513,15 +524,15 @@ public String getFullKey(String id) {
513524
* there are 2 cases:
514525
* - conventional bitstream, conventional storage
515526
* - registered bitstream, conventional storage
516-
* conventional bitstream: dspace ingested, dspace random name/path
517-
* registered bitstream: registered to dspace, any name/path
527+
* conventional bitstream: dspace ingested, dspace random name/path
528+
* registered bitstream: registered to dspace, any name/path
518529
*
519530
* @param sInternalId
520531
* @return Computed Relative path
521532
*/
522533
public String getRelativePath(String sInternalId) {
523534
BitstreamStorageService bitstreamStorageService = StorageServiceFactory.getInstance()
524-
.getBitstreamStorageService();
535+
.getBitstreamStorageService();
525536

526537
String sIntermediatePath = StringUtils.EMPTY;
527538
if (bitstreamStorageService.isRegisteredBitstream(sInternalId)) {
@@ -534,6 +545,10 @@ public String getRelativePath(String sInternalId) {
534545
return sIntermediatePath + sInternalId;
535546
}
536547

548+
public void setEnabled(boolean enabled) {
549+
this.enabled = enabled;
550+
}
551+
537552
public String getAwsAccessKey() {
538553
return awsAccessKey;
539554
}
@@ -625,22 +640,6 @@ public void setEndpoint(String endpoint) {
625640
this.endpoint = endpoint;
626641
}
627642

628-
public Integer getMaxConnections() {
629-
return maxConnections;
630-
}
631-
632-
public void setMaxConnections(Integer maxConnections) {
633-
this.maxConnections = maxConnections;
634-
}
635-
636-
public Integer getConnectionTimeout() {
637-
return connectionTimeout;
638-
}
639-
640-
public void setConnectionTimeout(Integer connectionTimeout) {
641-
this.connectionTimeout = connectionTimeout;
642-
}
643-
644643
public String getAwsSessionToken() {
645644
return awsSessionToken;
646645
}

dspace/config/modules/assetstore.cfg

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,6 @@ assetstore.s3.maxConcurrency =
7373
# The algorithm the S3 client will use to create a checksum when doing putObject.
7474
assetstore.s3.s3ChecksumAlgorithm = CRC32
7575

76-
# If this property is set, overrides the default number of connections used by the S3 Client to get connected
77-
# to remote service
78-
assetstore.s3.maxConnections =
79-
80-
# If this property is set, overrides the default connection timeout (milliseconds) used by the S3 Client to get connected
81-
# to remote service
82-
assetstore.s3.connectionTimeout =
83-
8476
# If this property is set, changes the endpoint of the S3 service
8577
assetstore.s3.endpoint =
8678

dspace/config/spring/api/bitstore.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@
4040
<!-- Subfolder to organize assets within the bucket, in case this bucket is shared -->
4141
<!-- Optional, default is root level of bucket -->
4242
<property name="subfolder" value="${assetstore.s3.subfolder}"/>
43-
<property name="maxConnections" value="${assetstore.s3.maxConnections}"/>
44-
<property name="connectionTimeout" value="${assetstore.s3.connectionTimeout}"/>
4543

4644
<!-- The target throughput for transfer requests in Gbps. Higher value means more connections will be established with S3. -->
4745
<property name="targetThroughputGbps" value="${assetstore.s3.targetThroughputGbps}"/>

0 commit comments

Comments
 (0)