Skip to content

Commit 4e8bd90

Browse files
authored
feat: Folder provisioning implementation (#94)
* feat: Folder provisioning implementation * feat: Folder provisioning implementation adjustments (dataAddress validation, formatting) * feat: Folder provisioning implementation -dependencies update
1 parent 2428cff commit 4e8bd90

File tree

20 files changed

+333
-56
lines changed

20 files changed

+333
-56
lines changed

DEPENDENCIES

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ maven/mavencentral/com.azure/azure-core-http-netty/1.13.9, MIT AND Apache-2.0, a
3333
maven/mavencentral/com.azure/azure-core-management/1.11.5, MIT, approved, #10033
3434
maven/mavencentral/com.azure/azure-core-management/1.11.7, MIT, approved, #10033
3535
maven/mavencentral/com.azure/azure-core/1.43.0, MIT AND Apache-2.0, approved, #10548
36-
maven/mavencentral/com.azure/azure-core/1.44.1, , restricted, clearlydefined
36+
maven/mavencentral/com.azure/azure-core/1.44.1, MIT, approved, clearlydefined
3737
maven/mavencentral/com.azure/azure-identity/1.10.1, MIT AND Apache-2.0, approved, #10086
3838
maven/mavencentral/com.azure/azure-json/1.1.0, MIT AND Apache-2.0, approved, #10547
39-
maven/mavencentral/com.azure/azure-messaging-eventgrid/4.19.0, , restricted, clearlydefined
39+
maven/mavencentral/com.azure/azure-messaging-eventgrid/4.19.0, MIT, approved, clearlydefined
4040
maven/mavencentral/com.azure/azure-security-keyvault-keys/4.7.1, MIT, approved, #10872
4141
maven/mavencentral/com.azure/azure-security-keyvault-secrets/4.7.0, MIT, approved, #10868
4242
maven/mavencentral/com.azure/azure-security-keyvault-secrets/4.7.1, MIT, approved, #10868
@@ -182,15 +182,16 @@ maven/mavencentral/io.setl/rdf-urdna/1.1, Apache-2.0, approved, clearlydefined
182182
maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.1.13, Apache-2.0, approved, clearlydefined
183183
maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.2.15, Apache-2.0, approved, #5947
184184
maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.2.18, Apache-2.0, approved, #5947
185+
maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.2.19, Apache-2.0, approved, #5947
185186
maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.1.13, Apache-2.0, approved, clearlydefined
186187
maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.15, Apache-2.0, approved, #5929
187188
maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.18, Apache-2.0, approved, #5929
188189
maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.1.13, Apache-2.0, approved, clearlydefined
189-
maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.2.15, Apache-2.0, approved, clearlydefined
190-
maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.2.18, , restricted, clearlydefined
190+
maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.2.15, Apache-2.0, approved, #11475
191+
maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.2.18, Apache-2.0, approved, #11475
191192
maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.1.13, Apache-2.0, approved, clearlydefined
192-
maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.2.15, Apache-2.0, approved, clearlydefined
193-
maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.2.18, , restricted, clearlydefined
193+
maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.2.15, Apache-2.0, approved, #11477
194+
maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.2.18, Apache-2.0, approved, #11477
194195
maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.1.13, Apache-2.0, approved, clearlydefined
195196
maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.15, Apache-2.0, approved, #5919
196197
maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.18, Apache-2.0, approved, #5919
@@ -409,21 +410,15 @@ maven/mavencentral/org.junit-pioneer/junit-pioneer/2.1.0, EPL-2.0, approved, #10
409410
maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.10.0, EPL-2.0, approved, #9714
410411
maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.10.1, EPL-2.0, approved, #9714
411412
maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.9.2, EPL-2.0, approved, #3133
412-
maven/mavencentral/org.junit.jupiter/junit-jupiter-engine/5.10.0, EPL-2.0, approved, #9711
413413
maven/mavencentral/org.junit.jupiter/junit-jupiter-engine/5.10.1, EPL-2.0, approved, #9711
414414
maven/mavencentral/org.junit.jupiter/junit-jupiter-engine/5.9.2, EPL-2.0, approved, #3125
415-
maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.10.0, EPL-2.0, approved, #9708
416415
maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.10.1, EPL-2.0, approved, #9708
417416
maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.9.2, EPL-2.0, approved, #3134
418-
maven/mavencentral/org.junit.platform/junit-platform-commons/1.10.0, EPL-2.0, approved, #9715
419417
maven/mavencentral/org.junit.platform/junit-platform-commons/1.10.1, EPL-2.0, approved, #9715
420418
maven/mavencentral/org.junit.platform/junit-platform-commons/1.9.2, EPL-2.0, approved, #3130
421-
maven/mavencentral/org.junit.platform/junit-platform-engine/1.10.0, EPL-2.0, approved, #9709
422419
maven/mavencentral/org.junit.platform/junit-platform-engine/1.10.1, EPL-2.0, approved, #9709
423420
maven/mavencentral/org.junit.platform/junit-platform-engine/1.9.2, EPL-2.0, approved, #3128
424-
maven/mavencentral/org.junit.platform/junit-platform-launcher/1.10.0, EPL-2.0, approved, #9704
425421
maven/mavencentral/org.junit.platform/junit-platform-launcher/1.10.1, EPL-2.0, approved, #9704
426-
maven/mavencentral/org.junit/junit-bom/5.10.0, EPL-2.0, approved, #9844
427422
maven/mavencentral/org.junit/junit-bom/5.10.1, EPL-2.0, approved, #9844
428423
maven/mavencentral/org.junit/junit-bom/5.9.2, EPL-2.0, approved, #4711
429424
maven/mavencentral/org.jvnet.mimepull/mimepull/1.9.15, CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0, approved, CQ21484

extensions/common/azure/azure-blob-core/src/main/java/org/eclipse/edc/azure/blob/AzureBlobStoreSchema.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ private AzureBlobStoreSchema() {
2727
public static final String CONTAINER_NAME = "container";
2828
public static final String ACCOUNT_NAME = "account";
2929
public static final String BLOB_NAME = "blobName";
30+
public static final String BLOB_PREFIX = "blobPrefix";
3031
public static final String FOLDER_NAME = "folderName";
3132
public static final String CORRELATION_ID = "correlationId";
3233
}

extensions/common/azure/azure-blob-core/src/main/java/org/eclipse/edc/azure/blob/api/BlobStoreApi.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ public interface BlobStoreApi {
3535

3636
List<BlobItem> listContainer(String accountName, String containerName);
3737

38+
/**
39+
* List all blobs from given folder in the container on a storage account.
40+
*
41+
* @param accountName The name of the storage account
42+
* @param containerName The name of the container within the storage account
43+
* @param directory The name of the folder within the container of the storage account
44+
* @return Lazy loaded list of blobs from folder specified by the input parameters
45+
*/
46+
List<BlobItem> listContainerFolder(String accountName, String containerName, String directory);
47+
3848
void putBlob(String accountName, String containerName, String blobName, byte[] data);
3949

4050
String createAccountSas(String accountName, String containerName, String racwxdl, OffsetDateTime expiry);

extensions/common/azure/azure-blob-core/src/main/java/org/eclipse/edc/azure/blob/api/BlobStoreApiImpl.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.azure.storage.blob.BlobServiceClient;
2121
import com.azure.storage.blob.BlobServiceClientBuilder;
2222
import com.azure.storage.blob.models.BlobItem;
23+
import com.azure.storage.blob.models.ListBlobsOptions;
2324
import com.azure.storage.blob.sas.BlobContainerSasPermission;
2425
import com.azure.storage.blob.sas.BlobServiceSasSignatureValues;
2526
import com.azure.storage.common.StorageSharedKeyCredential;
@@ -75,6 +76,12 @@ public List<BlobItem> listContainer(String accountName, String containerName) {
7576
return getBlobServiceClient(accountName).getBlobContainerClient(containerName).listBlobs().stream().toList();
7677
}
7778

79+
@Override
80+
public List<BlobItem> listContainerFolder(String accountName, String containerName, String directory) {
81+
var options = new ListBlobsOptions().setPrefix(directory);
82+
return getBlobServiceClient(accountName).getBlobContainerClient(containerName).listBlobs(options, null).stream().toList();
83+
}
84+
7885
@Override
7986
public void putBlob(String accountName, String containerName, String blobName, byte[] data) {
8087
var blobServiceClient = getBlobServiceClient(accountName);
@@ -105,16 +112,15 @@ private BlobServiceClient getBlobServiceClient(String accountName) {
105112
}
106113

107114
var accountKey = vault.resolveSecret(accountName + "-key1");
108-
109-
if (accountKey == null) {
110-
throw new IllegalArgumentException("No Object Storage credential found in vault!");
111-
}
112-
113115
var endpoint = createEndpoint(accountName);
114-
var blobServiceClient = new BlobServiceClientBuilder()
115-
.credential(createCredential(accountKey, accountName))
116-
.endpoint(endpoint)
117-
.buildClient();
116+
117+
var blobServiceClient = accountKey == null ?
118+
new BlobServiceClientBuilder().credential(new DefaultAzureCredentialBuilder().build())
119+
.endpoint(endpoint)
120+
.buildClient() :
121+
new BlobServiceClientBuilder().credential(createCredential(accountKey, accountName))
122+
.endpoint(endpoint)
123+
.buildClient();
118124

119125
cache.put(accountName, blobServiceClient);
120126
return blobServiceClient;

extensions/common/azure/azure-blob-core/src/main/java/org/eclipse/edc/azure/blob/validator/AzureStorageValidator.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class AzureStorageValidator {
3535
private static final int BLOB_MAX_LENGTH = 1024;
3636
private static final int METADATA_MIN_LENGTH = 1;
3737
private static final int METADATA_MAX_LENGTH = 4096;
38+
private static final int PATH_SEGMENTS_MAX = 254;
3839
private static final Pattern ACCOUNT_REGEX = Pattern.compile("^[a-z0-9]+$");
3940
private static final Pattern CONTAINER_REGEX = Pattern.compile("^[a-z0-9]+(-[a-z0-9]+)*$");
4041
private static final Pattern METADATA_REGEX = Pattern.compile("^[ -~]*$"); // US-ASCII
@@ -44,10 +45,13 @@ public class AzureStorageValidator {
4445
private static final String CONTAINER = "container";
4546
private static final String KEY_NAME = "keyName";
4647
private static final String METADATA = "metadata";
48+
private static final String PREFIX = "prefix";
49+
50+
private static final String INVALID_PREFIX = "Invalid %s prefix, prefix must end with a '/' character";
4751
private static final String INVALID_RESOURCE_NAME = "Invalid %s name";
4852
private static final String INVALID_RESOURCE_NAME_LENGTH = "Invalid %s name length, the name must be between %s and %s characters long";
4953
private static final String RESOURCE_NAME_EMPTY = "Invalid %s name, the name may not be null, empty or blank";
50-
private static final String TOO_MANY_PATH_SEGMENTS = "The number of URL path segments (strings between '/' characters) as part of the blob name cannot exceed 254.";
54+
private static final String TOO_MANY_PATH_SEGMENTS = "The number of URL path segments (strings between '/' characters) as part of the blob name cannot exceed %s.";
5155

5256
/**
5357
* Checks if an account name is valid.
@@ -87,12 +91,25 @@ public static void validateContainerName(String containerName) {
8791
*/
8892
public static void validateBlobName(String blobName) {
8993
checkLength(blobName, BLOB, BLOB_MIN_LENGTH, BLOB_MAX_LENGTH);
94+
checkSegments(blobName);
95+
}
9096

91-
var slashCount = blobName.chars().filter(ch -> ch == '/').count();
97+
/**
98+
* Checks if a blob prefix is valid.
99+
* The restriction is based on Azure Blob Storage folder 'virtualization' which is base on the forward slash (/)
100+
* used in the blob path as delimiter. Prefix has to ends with '/'.
101+
*
102+
* @param blobPrefix A String representing the blob prefix to validate.
103+
* @throws IllegalArgumentException if the string does not represent a valid prefix name.
104+
*/
105+
public static void validateBlobPrefix(String blobPrefix) {
106+
checkLength(blobPrefix, PREFIX, BLOB_MIN_LENGTH, BLOB_MAX_LENGTH);
107+
checkSegments(blobPrefix);
92108

93-
if (slashCount >= 254) {
94-
throw new IllegalArgumentException(TOO_MANY_PATH_SEGMENTS);
109+
if (!blobPrefix.endsWith("/")) {
110+
throw new IllegalArgumentException(String.format(INVALID_PREFIX, blobPrefix));
95111
}
112+
96113
}
97114

98115
/**
@@ -133,4 +150,12 @@ private static void checkLength(String name, String resourceType, int minLength,
133150
throw new IllegalArgumentException(String.format(INVALID_RESOURCE_NAME_LENGTH, resourceType, minLength, maxLength));
134151
}
135152
}
153+
154+
private static void checkSegments(String name) {
155+
var slashCount = name.chars().filter(ch -> ch == '/').count();
156+
157+
if (slashCount >= PATH_SEGMENTS_MAX) {
158+
throw new IllegalArgumentException(String.format(TOO_MANY_PATH_SEGMENTS, PATH_SEGMENTS_MAX));
159+
}
160+
}
136161
}

extensions/common/azure/azure-blob-core/src/test/java/org/eclipse/edc/azure/blob/validator/AzureStorageValidatorTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ void validateBlobName_fail(String input) {
9090
.isThrownBy(() -> AzureStorageValidator.validateBlobName(input));
9191
}
9292

93+
@ParameterizedTest
94+
@ArgumentsSource(ValidBlobPrefixProvider.class)
95+
void validateBlobPrefix_success(String input) {
96+
AzureStorageValidator.validateBlobPrefix(input);
97+
}
98+
99+
@ParameterizedTest
100+
@ArgumentsSource(InvalidBlobPrefixProvider.class)
101+
@NullSource
102+
void validateBlobPrefix_fail(String input) {
103+
assertThatExceptionOfType(IllegalArgumentException.class)
104+
.isThrownBy(() -> AzureStorageValidator.validateBlobPrefix(input));
105+
}
106+
93107
@ParameterizedTest
94108
@ValueSource(strings = { "abcdefghijklmnop", "-", "a/%!_- $K1~"})
95109
void validateMetadata_success(String input) {
@@ -129,6 +143,33 @@ public Stream<? extends Arguments> provideArguments(ExtensionContext context) th
129143
}
130144
}
131145

146+
private static class InvalidBlobPrefixProvider implements ArgumentsProvider {
147+
@Override
148+
public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
149+
return Stream.of(
150+
Arguments.of(""),
151+
Arguments.of("dfhjdhfjhsd"),
152+
Arguments.of("abcdefghijklmnop".repeat(64) + "/"),
153+
Arguments.of("a/b".repeat(253) + "/")
154+
);
155+
}
156+
}
157+
158+
private static class ValidBlobPrefixProvider implements ArgumentsProvider {
159+
@Override
160+
public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
161+
return Stream.of(
162+
Arguments.of("geq/"),
163+
Arguments.of("Qja143/"),
164+
Arguments.of("ABE/"),
165+
Arguments.of("a name/"),
166+
Arguments.of("end space /"),
167+
Arguments.of("je`~3j4k%$':\\/"),
168+
Arguments.of("abcdefghijklmnop".repeat(63) + "/"),
169+
Arguments.of("a/b".repeat(252) + "/"));
170+
}
171+
}
172+
132173
private static class InvalidMetadataProvider implements ArgumentsProvider {
133174
@Override
134175
public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {

extensions/common/azure/azure-blob-core/src/testFixtures/java/org/eclipse/edc/azure/blob/testfixtures/AzureStorageTestFixtures.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ public static String createBlobName() {
5050
return "blob-" + UUID.randomUUID();
5151
}
5252

53+
public static String createBlobPrefix() {
54+
return "blobFolder-" + UUID.randomUUID() + "/";
55+
}
56+
5357
public static String createSharedKey() {
5458
return "SK-" + UUID.randomUUID();
5559
}

extensions/data-plane/data-plane-azure-storage/README.md

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,85 @@
44

55
This module contains a Data Plane extension to copy data to and from Azure Blob storage.
66

7-
When used as a source, it currently only supports copying a single blob.
7+
When used as a source, it supports copying a single or multiple blobs.
88

99
The source `keyName` should reference a vault entry containing a storage [Shared Key](https://docs.microsoft.com/rest/api/storageservices/authorize-with-shared-key).
1010

1111
The destination `keyName` should reference a vault entry containing a JSON-serialized `AzureSasToken` object wrapping a [storage access signature](https://docs.microsoft.com/azure/storage/common/storage-sas-overview).
1212

13+
### AzureStorage DataAddress Configuration
14+
15+
The behavior of blobs transfers can be customized using DataAddress properties.
16+
17+
- When blobPrefix is present, transfer all blobs with names that start with the specified prefix.
18+
- When blobPrefix is not present, transfer only the blob with a name matching the blobName property.
19+
- Precedence: blobPrefix takes precedence over blobName when determining which objects to transfer. It allows for both multiple blobs transfers and fetching a single blob when necessary.
20+
21+
>Note: Using blobPrefix introduces an additional step to list all blobs whose name match the specified prefix.
22+
23+
24+
An example source address:
25+
26+
- Single blob:
27+
```json
28+
{
29+
"dataAddress": {
30+
"properties": {
31+
"type": "AzureStorage",
32+
"container": "containerName",
33+
"account": "accountName",
34+
"blobName": "test/blob.bin",
35+
"keyName": "(see above)"
36+
}
37+
}
38+
}
39+
```
40+
- Multiple blobs:
41+
```json
42+
{
43+
"dataAddress": {
44+
"properties": {
45+
"type": "AzureStorage",
46+
"container": "containerName",
47+
"account": "accountName",
48+
"blobPrefix": "test/",
49+
"keyName": "(see above)"
50+
}
51+
}
52+
}
53+
```
1354
An example destination address:
55+
56+
- Single blob:
1457
```json
1558
{
1659
"dataDestination": {
1760
"properties": {
1861
"type": "AzureStorage",
1962
"container": "containerName",
2063
"account": "accountName",
21-
"folderName": "test/",
64+
"folderName": "destinationFolder/",
2265
"blobName": "new-name",
2366
"keyName": "(see above)"
2467
}
2568
}
2669
}
2770
```
2871

29-
An example source address:
72+
- Multiple blobs:
3073
```json
3174
{
32-
"dataAddress": {
33-
"properties": {
34-
"type": "AzureStorage",
35-
"container": "containerName",
36-
"account": "accountName",
37-
"blobName": "test/blob.bin",
38-
"keyName": "(see above)"
75+
"dataDestination": {
76+
"properties": {
77+
"type": "AzureStorage",
78+
"container": "containerName",
79+
"account": "accountName",
80+
"folderName": "destinationFolder/",
81+
"keyName": "(see above)"
82+
}
3983
}
40-
}
4184
}
4285
```
43-
The `folderName` and the `blobName` are optional properties.
86+
The `folderName` and the `blobName` are optional properties in destination address.
4487

4588

extensions/data-plane/data-plane-azure-storage/src/main/java/org/eclipse/edc/connector/dataplane/azure/storage/pipeline/AzureStorageDataSink.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public class AzureStorageDataSink extends ParallelSink {
4242
private String containerName;
4343
private String folderName;
4444
private String blobName;
45+
private String blobPrefix;
4546
private String sharedAccessSignature;
4647
private BlobStoreApi blobStoreApi;
4748
private DataFlowRequest request;
@@ -111,7 +112,7 @@ private StreamResult<Object> getTransferResult(Exception e, String logMessage, O
111112
}
112113

113114
String getDestinationBlobName(String partName) {
114-
var name = !StringUtils.isNullOrEmpty(blobName) ? blobName : partName;
115+
var name = !StringUtils.isNullOrEmpty(blobName) && StringUtils.isNullOrBlank(blobPrefix) ? blobName : partName;
115116
if (!StringUtils.isNullOrEmpty(folderName)) {
116117
return folderName.endsWith("/") ? folderName + name : folderName + "/" + name;
117118
} else {
@@ -149,6 +150,11 @@ public Builder blobName(String blobName) {
149150
return this;
150151
}
151152

153+
public Builder blobPrefix(String blobPrefix) {
154+
sink.blobPrefix = blobPrefix;
155+
return this;
156+
}
157+
152158
public Builder sharedAccessSignature(String sharedAccessSignature) {
153159
sink.sharedAccessSignature = sharedAccessSignature;
154160
return this;

0 commit comments

Comments
 (0)