Skip to content

Commit 565ae2a

Browse files
authored
Nio skip container check (Azure#26850)
* Updated some docs and added convert from Uri method * Added option to skip container check * Wrote tests * Changelog * Added recordings * Added some javadocs and fixed a test
1 parent b0fce90 commit 565ae2a

16 files changed

+511
-16
lines changed

sdk/storage/azure-storage-blob-nio/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## 12.0.0-beta.15 (Unreleased)
44

55
### Features Added
6+
- Added `AzurePath.fromBlobUrl` to help convert from a blob url to an AzurePath
7+
- Added a configuration option `AZURE_STORAGE_SKIP_INITIAL_CONTAINER_CHECK` to skip the initial container check in cases where the authentication method used will not have necessary permissions.
68

79
### Breaking Changes
810

sdk/storage/azure-storage-blob-nio/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ committed or available to be read until the write stream is closed.
129129

130130
The following sections provide several code snippets covering some of the most common Azure Storage Blob NIO tasks, including:
131131

132+
- [URI format](#uri-format)
132133
- [Create a `FileSystem`](#create-a-filesystem)
133134
- [Create a directory](#create-a-directory)
134135
- [Iterate over directory contents](#iterate-over-directory-contents)
@@ -139,6 +140,19 @@ The following sections provide several code snippets covering some of the most c
139140
- [Read attributes on a file](#read-attributes-on-a-file)
140141
- [Write attributes to a file](#write-attributes-to-a-file)
141142

143+
### URI format
144+
URIs are the fundamental way of identifying a resource. This package defines its URI format as follows:
145+
146+
The scheme for this provider is `"azb"`, and the format of the URI to identify an `AzureFileSystem` is
147+
`"azb://?endpoint=<endpoint>"`. The endpoint of the Storage account is used to uniquely identify the filesystem.
148+
149+
The root component, if it is present, is the first element of the path and is denoted by a `':'` as the last character.
150+
Hence, only one instance of `':'` may appear in a path string, and it may only be the last character of the first
151+
element in the path. The root component is used to identify which container a path belongs to.
152+
153+
All other path elements, including separators, are considered as the blob name. `AzurePath#fromBlobUrl`
154+
may be used to convert a typical http url pointing to a blob into an `AzurePath` object pointing to the same resource.
155+
142156
### Create a `FileSystem`
143157

144158
Create a `FileSystem` using the [`shared key`](#get-credentials) retrieved above.

sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileStore.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public final class AzureFileStore extends FileStore {
2424
private final BlobContainerClient containerClient;
2525

2626

27-
AzureFileStore(AzureFileSystem parentFileSystem, String containerName) throws IOException {
27+
AzureFileStore(AzureFileSystem parentFileSystem, String containerName, Boolean skipConnectionCheck)
28+
throws IOException {
2829
// A FileStore should only ever be created by a FileSystem.
2930
if (Objects.isNull(parentFileSystem)) {
3031
throw LoggingUtility.logError(logger, new IllegalStateException("AzureFileStore cannot be instantiated "
@@ -33,14 +34,16 @@ public final class AzureFileStore extends FileStore {
3334
this.parentFileSystem = parentFileSystem;
3435
this.containerClient = this.parentFileSystem.getBlobServiceClient().getBlobContainerClient(containerName);
3536

36-
try {
37-
// This also serves as our connection check.
38-
if (!this.containerClient.exists()) {
39-
this.containerClient.create();
37+
if (skipConnectionCheck == null || !skipConnectionCheck) {
38+
try {
39+
// This also serves as our connection check.
40+
if (!this.containerClient.exists()) {
41+
this.containerClient.create();
42+
}
43+
} catch (Exception e) {
44+
throw LoggingUtility.logError(logger, new IOException("There was an error in establishing the existence of "
45+
+ "container: " + containerName, e));
4046
}
41-
} catch (Exception e) {
42-
throw LoggingUtility.logError(logger, new IOException("There was an error in establishing the existence of "
43-
+ "container: " + containerName, e));
4447
}
4548
}
4649

sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystem.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ public final class AzureFileSystem extends FileSystem {
130130
*/
131131
public static final String AZURE_STORAGE_FILE_STORES = "AzureStorageFileStores";
132132

133+
/**
134+
* Expected type: Boolean
135+
*/
136+
public static final String AZURE_STORAGE_SKIP_INITIAL_CONTAINER_CHECK = "AzureStorageSkipInitialContainerCheck";
137+
133138
static final String PATH_SEPARATOR = "/";
134139

135140
private static final Map<String, String> PROPERTIES =
@@ -432,9 +437,10 @@ private Map<String, FileStore> initializeFileStores(Map<String, ?> config) throw
432437
+ "null."));
433438
}
434439

440+
Boolean skipConnectionCheck = (Boolean) config.get(AZURE_STORAGE_SKIP_INITIAL_CONTAINER_CHECK);
435441
Map<String, FileStore> fileStores = new HashMap<>();
436442
for (String fileStoreName : fileStoreNames.split(",")) {
437-
FileStore fs = new AzureFileStore(this, fileStoreName);
443+
FileStore fs = new AzureFileStore(this, fileStoreName, skipConnectionCheck);
438444
if (this.defaultFileStore == null) {
439445
this.defaultFileStore = fs;
440446
}

sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzureFileSystemProvider.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
* {@link FileSystemProvider}.
8282
* <p>
8383
* The scheme for this provider is {@code "azb"}, and the format of the URI to identify an {@code AzureFileSystem} is
84-
* {@code "azb://?endpoint=<endpoing>"}. The endpoint of the Storage account is used to uniquely identify the
84+
* {@code "azb://?endpoint=<endpoint>"}. The endpoint of the Storage account is used to uniquely identify the
8585
* filesystem.
8686
* <p>
8787
* An {@link AzureFileSystem} is backed by an account. An {@link AzureFileStore} is backed by a container. Any number of
@@ -94,7 +94,8 @@
9494
* <p>
9595
* {@link #newFileSystem(URI, Map)} will check for the following keys in the configuration map and expect the named
9696
* types. Any entries not listed here will be ignored. Note that {@link AzureFileSystem} has public constants defined
97-
* for each of the keys for convenience.
97+
* for each of the keys for convenience. Most values are documented in the blob package. Any values which are unique to
98+
* nio will be documented here.
9899
* <ul>
99100
* <li>{@code AzureStorageSharedKeyCredential:}{@link com.azure.storage.common.StorageSharedKeyCredential}</li>
100101
* <li>{@code AzureStorageSasTokenCredential:}{@link com.azure.core.credential.AzureSasCredential}</li>
@@ -111,6 +112,9 @@
111112
* <li>{@code AzureStorageMaxConcurrencyPerRequest:}{@link Integer}</li>
112113
* <li>{@code AzureStorageDownloadResumeRetries:}{@link Integer}</li>
113114
* <li>{@code AzureStorageFileStores:}{@link String}</li>
115+
* <li>{@code AzureStorageSkipInitialContainerCheck:}{@link Boolean}. Indicates that the initial check which
116+
* confirms the existence of the containers meant to act as file stores should be skipped. This can be usesful in
117+
* cases where a sas token that is scoped to only one file is used to authenticate.</li>
114118
* </ul>
115119
* <p>
116120
* Either an account key or a sas token must be specified. If both are provided, the account key will be preferred. If
@@ -226,7 +230,7 @@ public FileSystem newFileSystem(URI uri, Map<String, ?> config) throws IOExcepti
226230
/**
227231
* Returns an existing FileSystem created by this provider.
228232
* <p>
229-
* The format of a {@code URI} identifying an file system is {@code "azb://?endpoint=&lt;endpoint&gt;"}.
233+
* The format of a {@code URI} identifying a file system is {@code "azb://?endpoint=&lt;endpoint&gt;"}.
230234
* <p>
231235
* Trying to retrieve a closed file system will throw a {@link FileSystemNotFoundException}. Once closed, a
232236
* file system with the same identifier may be reopened.

sdk/storage/azure-storage-blob-nio/src/main/java/com/azure/storage/blob/nio/AzurePath.java

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.azure.core.util.logging.ClientLogger;
77
import com.azure.storage.blob.BlobClient;
88
import com.azure.storage.blob.BlobContainerClient;
9+
import com.azure.storage.blob.BlobUrlParts;
910

1011
import java.io.File;
1112
import java.io.IOException;
@@ -33,8 +34,11 @@
3334
* An object that may be used to locate a file in a file system.
3435
* <p>
3536
* The root component, if it is present, is the first element of the path and is denoted by a {@code ':'} as the last
36-
* character. Hence, only one instance of {@code ':'} may appear in a path string and it may only be the last character
37-
* of the first element in the path. The root component is used to identify which container a path belongs to.
37+
* character. Hence, only one instance of {@code ':'} may appear in a path string, and it may only be the last character
38+
* of the first element in the path. The root component is used to identify which container a path belongs to. All other
39+
* path elements, including separators, are considered as the blob name. {@link AzurePath#fromBlobUrl} may
40+
* be used to convert a typical http url pointing to a blob into an {@code AzurePath} object pointing to the same
41+
* resource.
3842
* <p>
3943
* Constructing a syntactically valid path does not ensure a resource exists at the given path. An error will
4044
* not be thrown until trying to access an invalid resource, e.g. trying to access a resource that does not exist.
@@ -742,6 +746,36 @@ public BlobClient toBlobClient() throws IOException {
742746
return containerClient.getBlobClient(blobName);
743747
}
744748

749+
/**
750+
* A utility method to conveniently convert from a url to a storage resource to an {@code AzurePath} pointing to the
751+
* same resource.
752+
*
753+
* The url must be well formatted. There must be an open filesystem corresponding to the account which contains the
754+
* blob. Otherwise, a {@link java.nio.file.FileSystemNotFoundException} will be thrown.
755+
*
756+
* The url may point to either an account, container, or blob. If it points to an account, the path will be empty,
757+
* but it will have an internal reference to the file system containing it, meaning instance methods may be
758+
* performed on the path to construct a reference to another object. If it points to a container, there will be one
759+
* element, which is the root element. Everything after the container, that is the blob name, will then be appended
760+
* after the root element.
761+
*
762+
* IP style urls are not currently supported.
763+
*
764+
* The {@link AzureFileSystemProvider} can typically be obtained via {@link AzureFileSystem#provider()}.
765+
*
766+
* @param provider The installed {@link AzureFileSystemProvider} that manages open file systems for this jvm.
767+
* @param url The url to the desired resource.
768+
* @return An {@link AzurePath} which points to the resrouce identified by the url.
769+
* @throws URISyntaxException If the url contains elements which are not well formatted.
770+
*/
771+
public static AzurePath fromBlobUrl(AzureFileSystemProvider provider, String url) throws URISyntaxException {
772+
BlobUrlParts parts = BlobUrlParts.parse(url);
773+
URI fileSystemUri = hostToFileSystemUri(provider, parts.getScheme(), parts.getHost());
774+
FileSystem parentFileSystem = provider.getFileSystem(fileSystemUri);
775+
return new AzurePath((AzureFileSystem) parentFileSystem, fileStoreToRoot(parts.getBlobContainerName()),
776+
parts.getBlobName() == null ? "" : parts.getBlobName());
777+
}
778+
745779
/**
746780
* @return Whether this path consists of only a root component.
747781
*/
@@ -782,6 +816,18 @@ private String rootToFileStore(String root) {
782816
return root.substring(0, root.length() - 1); // Remove the ROOT_DIR_SUFFIX
783817
}
784818

819+
private static String fileStoreToRoot(String fileStore) {
820+
if (fileStore == null || "".equals(fileStore)) {
821+
return "";
822+
}
823+
return fileStore + ROOT_DIR_SUFFIX;
824+
}
825+
826+
private static URI hostToFileSystemUri(AzureFileSystemProvider provider, String scheme, String host)
827+
throws URISyntaxException {
828+
return new URI(provider.getScheme() + "://?endpoint=" + scheme + "://" + host);
829+
}
830+
785831
static void ensureFileSystemOpen(Path p) {
786832
if (!p.getFileSystem().isOpen()) {
787833
throw LoggingUtility.logError(((AzurePath) p).logger,

sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzureFileStoreTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class AzureFileStoreTest extends APISpec {
2626
def "Name"() {
2727
setup:
2828
def name = generateContainerName()
29-
def store = new AzureFileStore(fs, name)
29+
def store = new AzureFileStore(fs, name, false)
3030

3131
expect:
3232
store.name() == name

sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzureFileSystemTest.groovy

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ class AzureFileSystemTest extends APISpec {
100100
thrown(IOException)
101101
}
102102

103+
def "Create skip container check"() {
104+
setup:
105+
config[AzureFileSystem.AZURE_STORAGE_SAS_TOKEN_CREDENTIAL] = new AzureSasCredential(
106+
primaryBlobServiceClient.generateAccountSas(
107+
new AccountSasSignatureValues(OffsetDateTime.now().plusDays(2),
108+
AccountSasPermission.parse("d"), new AccountSasService().setBlobAccess(true),
109+
new AccountSasResourceType().setContainer(true))))
110+
config[AzureFileSystem.AZURE_STORAGE_FILE_STORES] = generateContainerName()
111+
config[AzureFileSystem.AZURE_STORAGE_SKIP_INITIAL_CONTAINER_CHECK] = true
112+
113+
when:
114+
// This would fail, but we skipped the check
115+
new AzureFileSystem(new AzureFileSystemProvider(), environment.primaryAccount.blobEndpoint, config)
116+
117+
then:
118+
notThrown(IOException)
119+
}
120+
103121
def "Close"() {
104122
setup:
105123
def provider = new AzureFileSystemProvider()

sdk/storage/azure-storage-blob-nio/src/test/java/com/azure/storage/blob/nio/AzurePathTest.groovy

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package com.azure.storage.blob.nio
66
import spock.lang.ResourceLock
77
import spock.lang.Unroll
88

9+
import java.nio.file.FileSystemNotFoundException
910
import java.nio.file.FileSystems
1011

1112
@ResourceLock("AzurePathTest")
@@ -17,7 +18,9 @@ class AzurePathTest extends APISpec {
1718
def config = initializeConfigMap()
1819
config[AzureFileSystem.AZURE_STORAGE_SHARED_KEY_CREDENTIAL] = environment.primaryAccount.credential
1920
config[AzureFileSystem.AZURE_STORAGE_FILE_STORES] = "jtcazurepath1,jtcazurepath2"
20-
fs = new AzureFileSystem(new AzureFileSystemProvider(), environment.primaryAccount.blobEndpoint, config)
21+
def provider = new AzureFileSystemProvider()
22+
fs = (AzureFileSystem) provider.newFileSystem(
23+
new URI("azb://?endpoint=" + environment.primaryAccount.blobEndpoint), config)
2124
}
2225

2326
def "GetFileSystem"() {
@@ -423,4 +426,39 @@ class AzurePathTest extends APISpec {
423426
then:
424427
thrown(IOException)
425428
}
429+
430+
@Unroll
431+
def "FromBlobUrl"() {
432+
setup:
433+
// Adjust the parameterized urls to point at real resources
434+
def scheme = environment.primaryAccount.blobEndpoint.startsWith("https") ? "https" : "http"
435+
url = scheme + url
436+
url = url.replace("myaccount", environment.primaryAccount.name)
437+
url = url.replace("containername", "jtcazurepath1")
438+
439+
path = path.replace("myaccount", environment.primaryAccount.name)
440+
path = path.replace("containername", "jtcazurepath1")
441+
442+
when:
443+
def resultPath = AzurePath.fromBlobUrl((AzureFileSystemProvider) fs.provider(), url)
444+
445+
then:
446+
resultPath.getFileSystem() == fs
447+
resultPath.toString() == path
448+
449+
where:
450+
url | path
451+
"://myaccount.blob.core.windows.net/containername/blobname" | "containername:/blobname"
452+
"://myaccount.blob.core.windows.net/containername/dirname/blobname" | "containername:/dirname/blobname"
453+
"://myaccount.blob.core.windows.net/containername" | "containername:"
454+
"://myaccount.blob.core.windows.net/" | ""
455+
}
456+
457+
def "FromBlobUrl no open file system"() {
458+
when:
459+
AzurePath.fromBlobUrl(new AzureFileSystemProvider(), "http://myaccount.blob.core.windows.net/container/blob")
460+
461+
then:
462+
thrown(FileSystemNotFoundException)
463+
}
426464
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"networkCallRecords" : [ ],
3+
"variables" : [ "0b3431c000b3431c056410375795eb7d50060472881a", "0b3431c010b3431c05647858174b0c8e62d9f46b7b0c" ]
4+
}

0 commit comments

Comments
 (0)