From 3a5544054e12e6583decf1f70f5d365d05311865 Mon Sep 17 00:00:00 2001 From: Mykhailo Nesterenko Date: Fri, 13 Mar 2026 19:45:32 +0200 Subject: [PATCH 1/3] Allow multiple filesystems for a single storage account --- .../azure-storage-blob-nio/CHANGELOG.md | 1 + .../blob/nio/AzureFileSystemProvider.java | 78 ++++++++++++++----- .../nio/AzureFileSystemProviderTests.java | 39 ++++++++-- .../storage/blob/nio/BlobNioTestBase.java | 7 +- 4 files changed, 96 insertions(+), 29 deletions(-) diff --git a/sdk/storage/azure-storage-blob-nio/CHANGELOG.md b/sdk/storage/azure-storage-blob-nio/CHANGELOG.md index ab2a498f661b..cd00905de2c4 100644 --- a/sdk/storage/azure-storage-blob-nio/CHANGELOG.md +++ b/sdk/storage/azure-storage-blob-nio/CHANGELOG.md @@ -1,6 +1,7 @@ # Release History ## 12.0.0-beta.37 (Unreleased) +- Extend azure nio filesystem url to support a unique identifer: `azb://?endpoint=&uid=` ### Features Added diff --git a/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystemProvider.java b/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystemProvider.java index 408590992ad5..6daad5303e5e 100644 --- a/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystemProvider.java +++ b/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystemProvider.java @@ -11,8 +11,6 @@ import com.azure.storage.blob.models.BlobCopyInfo; import com.azure.storage.blob.models.BlobErrorCode; import com.azure.storage.blob.models.ParallelTransferOptions; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import java.io.IOException; import java.io.InputStream; @@ -171,6 +169,8 @@ private static final class ClientLoggerHolder { public static final String CACHE_CONTROL = "Cache-Control"; private static final String ENDPOINT_QUERY_KEY = "endpoint"; + private static final String UID_QUERY_KEY = "uid"; + private static final int COPY_TIMEOUT_SECONDS = 30; private static final Set OUTPUT_STREAM_DEFAULT_OPTIONS = Collections.unmodifiableSet(new HashSet<>( Arrays.asList(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING))); @@ -217,15 +217,15 @@ public String getScheme() { */ @Override public FileSystem newFileSystem(URI uri, Map config) throws IOException { - String endpoint = extractAccountEndpoint(uri); + FileSystemUriParameters parameters = parseFileSystemParameters(uri); - if (this.openFileSystems.containsKey(endpoint)) { + if (this.openFileSystems.containsKey(parameters.uid())) { throw LoggingUtility.logError(ClientLoggerHolder.LOGGER, - new FileSystemAlreadyExistsException("Name: " + endpoint)); + new FileSystemAlreadyExistsException("Name: " + parameters.endpoint() + " UID: " + parameters.uid())); } - AzureFileSystem afs = new AzureFileSystem(this, endpoint, config); - this.openFileSystems.put(endpoint, afs); + AzureFileSystem afs = new AzureFileSystem(this, parameters.endpoint(), config); + this.openFileSystems.put(parameters.uid(), afs); return afs; } @@ -246,12 +246,12 @@ public FileSystem newFileSystem(URI uri, Map config) throws IOExcepti */ @Override public FileSystem getFileSystem(URI uri) { - String endpoint = extractAccountEndpoint(uri); - if (!this.openFileSystems.containsKey(endpoint)) { + FileSystemUriParameters parameters = parseFileSystemParameters(uri); + if (!this.openFileSystems.containsKey(parameters.uid())) { throw LoggingUtility.logError(ClientLoggerHolder.LOGGER, - new FileSystemNotFoundException("Name: " + endpoint)); + new FileSystemNotFoundException("Name: " + parameters.endpoint())); } - return this.openFileSystems.get(endpoint); + return this.openFileSystems.get(parameters.uid()); } /** @@ -1144,7 +1144,12 @@ void closeFileSystem(String fileSystemName) { this.openFileSystems.remove(fileSystemName); } - private String extractAccountEndpoint(URI uri) { + /** + * Uses azure blob file system uri to extract the file system parameters. + * @param uri The URI to extract the file system details from. Format: {@code azb://?endpoint=&uid=} + * @return The file system details. + */ + private FileSystemUriParameters parseFileSystemParameters(URI uri) { if (!uri.getScheme().equals(this.getScheme())) { throw LoggingUtility.logError(ClientLoggerHolder.LOGGER, new IllegalArgumentException("URI scheme does not match this provider")); @@ -1155,19 +1160,50 @@ private String extractAccountEndpoint(URI uri) { + "the format \"azb://?endpoint=\".")); } - String endpoint = Flux.fromArray(uri.getQuery().split("&")) - .filter(s -> s.startsWith(ENDPOINT_QUERY_KEY + "=")) - .switchIfEmpty(Mono.defer(() -> Mono.error(LoggingUtility.logError(ClientLoggerHolder.LOGGER, - new IllegalArgumentException("URI does not contain an \"" + ENDPOINT_QUERY_KEY + "=\" parameter. " - + "FileSystems require a URI of the format \"azb://?endpoint=\""))))) - .map(s -> s.substring(ENDPOINT_QUERY_KEY.length() + 1)) // Trim the query key and = - .blockLast(); + String endpoint = "", uid = ""; + for (String queryPart : uri.getQuery().split("&")) { + String[] parts = queryPart.split("="); + switch (parts[0]) { + case ENDPOINT_QUERY_KEY: + endpoint = parts.length > 1 ? parts[1] : ""; + break; + + case UID_QUERY_KEY: + uid = parts.length > 1 ? parts[1] : ""; + break; + } + } + uid = CoreUtils.isNullOrEmpty(uid) ? endpoint : uid; + String expectedFormat + = "FileSystems require a URI of the format \"azb://?endpoint=&uid=\""; + if (CoreUtils.isNullOrEmpty(uid)) { + throw LoggingUtility.logError(ClientLoggerHolder.LOGGER, new IllegalArgumentException( + "URI does not contain an \"" + UID_QUERY_KEY + "=\" parameter. " + expectedFormat)); + } if (CoreUtils.isNullOrEmpty(endpoint)) { throw LoggingUtility.logError(ClientLoggerHolder.LOGGER, - new IllegalArgumentException("No account endpoint provided in URI query.")); + new IllegalArgumentException("No account endpoint provided in URI query. " + expectedFormat)); } - return endpoint; + return new FileSystemUriParameters(endpoint, uid); + } + + static class FileSystemUriParameters { + private final String endpoint; + private final String uid; + + public FileSystemUriParameters(String endpoint, String uid) { + this.endpoint = Objects.requireNonNull(endpoint); + this.uid = Objects.requireNonNull(uid); + } + + public String endpoint() { + return endpoint; + } + + public String uid() { + return uid; + } } } diff --git a/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzureFileSystemProviderTests.java b/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzureFileSystemProviderTests.java index 2231ca4825de..bf46aca67174 100644 --- a/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzureFileSystemProviderTests.java +++ b/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzureFileSystemProviderTests.java @@ -80,6 +80,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -109,9 +110,30 @@ public void createFileSystem() throws IOException { URI uri = getFileSystemUri(); provider.newFileSystem(uri, config); - assertTrue(provider.getFileSystem(uri).isOpen()); - assertEquals(primaryBlobServiceClient.getAccountUrl(), - ((AzureFileSystem) provider.getFileSystem(uri)).getFileSystemUrl()); + verifyFileSystem((AzureFileSystem) provider.getFileSystem(uri)); + } + + @Test + public void createFileSystemDifferentUid() throws IOException { + config.put(AzureFileSystem.AZURE_STORAGE_SHARED_KEY_CREDENTIAL, ENV.getPrimaryAccount().getCredential()); + config.put(AzureFileSystem.AZURE_STORAGE_FILE_STORES, generateContainerName()); + URI uri1 = getFileSystemUri("uid1"); + URI uri2 = getFileSystemUri("uid2"); + + provider.newFileSystem(uri1, config); + provider.newFileSystem(uri2, config); + + AzureFileSystem fs1 = (AzureFileSystem) provider.getFileSystem(uri1); + AzureFileSystem fs2 = (AzureFileSystem) provider.getFileSystem(uri2); + + assertNotSame(fs1, fs2); + verifyFileSystem(fs1); + verifyFileSystem(fs2); + } + + private void verifyFileSystem(AzureFileSystem fileSystem) { + assertTrue(fileSystem.isOpen()); + assertEquals(primaryBlobServiceClient.getAccountUrl(), fileSystem.getFileSystemUrl()); } @ParameterizedTest @@ -120,13 +142,16 @@ public void createFileSystemInvalidUri(String uri) { assertThrows(IllegalArgumentException.class, () -> provider.newFileSystem(new URI(uri), config)); } - @Test - public void createFileSystemDuplicate() throws IOException { + @ParameterizedTest + @ValueSource(strings = { "", "test-uid" }) + public void createFileSystemDuplicate(String uid) throws IOException { + URI uri = getFileSystemUri(uid == "" ? null : uid); + config.put(AzureFileSystem.AZURE_STORAGE_FILE_STORES, generateContainerName()); config.put(AzureFileSystem.AZURE_STORAGE_SHARED_KEY_CREDENTIAL, ENV.getPrimaryAccount().getCredential()); - provider.newFileSystem(getFileSystemUri(), config); + provider.newFileSystem(uri, config); - assertThrows(FileSystemAlreadyExistsException.class, () -> provider.newFileSystem(getFileSystemUri(), config)); + assertThrows(FileSystemAlreadyExistsException.class, () -> provider.newFileSystem(uri, config)); } @Test diff --git a/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/BlobNioTestBase.java b/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/BlobNioTestBase.java index b584ddd5936b..ef047df708c7 100644 --- a/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/BlobNioTestBase.java +++ b/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/BlobNioTestBase.java @@ -172,8 +172,13 @@ protected Map initializeConfigMap(HttpPipelinePolicy... policies } protected URI getFileSystemUri() { + return getFileSystemUri(null); + } + + protected URI getFileSystemUri(String uid) { try { - return new URI("azb://?endpoint=" + ENV.getPrimaryAccount().getBlobEndpoint()); + return new URI( + "azb://?endpoint=" + ENV.getPrimaryAccount().getBlobEndpoint() + (uid != null ? "&uid=" + uid : "")); } catch (URISyntaxException ex) { throw new RuntimeException(ex); } From c575ddf356414a3c9d26757d56697a04a2e2719a Mon Sep 17 00:00:00 2001 From: Mykhailo Nesterenko Date: Fri, 13 Mar 2026 20:36:19 +0200 Subject: [PATCH 2/3] fix file system close with a custom uid --- .../com/azure/storage/blob/nio/AzureFileSystem.java | 11 ++++++++++- .../storage/blob/nio/AzureFileSystemProvider.java | 5 ++++- .../blob/nio/AzureFileSystemProviderTests.java | 13 +++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystem.java b/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystem.java index 02cc40d3f458..71c85b13c31e 100644 --- a/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystem.java +++ b/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystem.java @@ -135,6 +135,11 @@ public final class AzureFileSystem extends FileSystem { */ public static final String AZURE_STORAGE_SKIP_INITIAL_CONTAINER_CHECK = "AzureStorageSkipInitialContainerCheck"; + /** + * Expected type: String + */ + public static final String AZURE_STORAGE_FILESYSTEM_UID = "AzureStorageFileSystemUid"; + static final String PATH_SEPARATOR = "/"; private static final Map PROPERTIES = CoreUtils.getProperties("azure-storage-blob-nio.properties"); @@ -161,6 +166,7 @@ public final class AzureFileSystem extends FileSystem { private final AzureSasCredential sasCredential; private FileStore defaultFileStore; private boolean closed; + private String uid; AzureFileSystem(AzureFileSystemProvider parentFileSystemProvider, String endpoint, Map config) throws IOException { @@ -178,6 +184,9 @@ public final class AzureFileSystem extends FileSystem { this.putBlobThreshold = (Long) config.get(AZURE_STORAGE_PUT_BLOB_THRESHOLD); this.maxConcurrencyPerRequest = (Integer) config.get(AZURE_STORAGE_MAX_CONCURRENCY_PER_REQUEST); this.sasCredential = (AzureSasCredential) config.get(AZURE_STORAGE_SAS_TOKEN_CREDENTIAL); + this.uid = config.containsKey(AZURE_STORAGE_FILESYSTEM_UID) + ? (String) config.get(AZURE_STORAGE_FILESYSTEM_UID) + : endpoint; // Initialize and ensure access to FileStores. this.fileStores = this.initializeFileStores(config); @@ -223,7 +232,7 @@ public FileSystemProvider provider() { @Override public void close() throws IOException { this.closed = true; - this.parentFileSystemProvider.closeFileSystem(this.getFileSystemUrl()); + this.parentFileSystemProvider.closeFileSystem(this.uid); } /** diff --git a/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystemProvider.java b/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystemProvider.java index 6daad5303e5e..0c08f45518ad 100644 --- a/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystemProvider.java +++ b/sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystemProvider.java @@ -224,7 +224,10 @@ public FileSystem newFileSystem(URI uri, Map config) throws IOExcepti new FileSystemAlreadyExistsException("Name: " + parameters.endpoint() + " UID: " + parameters.uid())); } - AzureFileSystem afs = new AzureFileSystem(this, parameters.endpoint(), config); + Map configCopy = new HashMap<>(config); + configCopy.put(AzureFileSystem.AZURE_STORAGE_FILESYSTEM_UID, parameters.uid()); + + AzureFileSystem afs = new AzureFileSystem(this, parameters.endpoint(), configCopy); this.openFileSystems.put(parameters.uid(), afs); return afs; diff --git a/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzureFileSystemProviderTests.java b/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzureFileSystemProviderTests.java index bf46aca67174..ea6712bd0648 100644 --- a/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzureFileSystemProviderTests.java +++ b/sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzureFileSystemProviderTests.java @@ -113,6 +113,19 @@ public void createFileSystem() throws IOException { verifyFileSystem((AzureFileSystem) provider.getFileSystem(uri)); } + @ParameterizedTest + @ValueSource(strings = { "", "test-uid" }) + public void getFileSystemNotFoundAfterFileSystemIsClosed(String uid) throws IOException { + URI uri = getFileSystemUri(uid == "" ? null : uid); + + config.put(AzureFileSystem.AZURE_STORAGE_SHARED_KEY_CREDENTIAL, ENV.getPrimaryAccount().getCredential()); + config.put(AzureFileSystem.AZURE_STORAGE_FILE_STORES, generateContainerName()); + provider.newFileSystem(uri, config); + + provider.getFileSystem(uri).close(); + assertThrows(FileSystemNotFoundException.class, () -> provider.getFileSystem(uri)); + } + @Test public void createFileSystemDifferentUid() throws IOException { config.put(AzureFileSystem.AZURE_STORAGE_SHARED_KEY_CREDENTIAL, ENV.getPrimaryAccount().getCredential()); From 9abee0aab31212cc727179cad609f50cbeebdf49 Mon Sep 17 00:00:00 2001 From: Ghost of the Code Date: Fri, 13 Mar 2026 20:36:53 +0200 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- sdk/storage/azure-storage-blob-nio/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/storage/azure-storage-blob-nio/CHANGELOG.md b/sdk/storage/azure-storage-blob-nio/CHANGELOG.md index cd00905de2c4..ba8f00b3d94b 100644 --- a/sdk/storage/azure-storage-blob-nio/CHANGELOG.md +++ b/sdk/storage/azure-storage-blob-nio/CHANGELOG.md @@ -1,9 +1,9 @@ # Release History ## 12.0.0-beta.37 (Unreleased) -- Extend azure nio filesystem url to support a unique identifer: `azb://?endpoint=&uid=` ### Features Added +- Extended Azure NIO filesystem URI support to allow a unique identifier: `azb://?endpoint=&uid=` ### Breaking Changes