diff --git a/pom.xml b/pom.xml
index ca08d16..dadfe03 100644
--- a/pom.xml
+++ b/pom.xml
@@ -91,9 +91,11 @@
2.7.2
runtime
-
-
-
+
+ software.amazon.awssdk
+ s3
+ 2.13.8
+
diff --git a/src/loci/formats/S3FileSystemStore.java b/src/loci/formats/S3FileSystemStore.java
new file mode 100644
index 0000000..48cc817
--- /dev/null
+++ b/src/loci/formats/S3FileSystemStore.java
@@ -0,0 +1,306 @@
+package loci.formats;
+
+/*-
+ * #%L
+ * Implementation of Bio-Formats readers for the next-generation file formats
+ * %%
+ * Copyright (C) 2020 - 2022 Open Microscopy Environment
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import com.bc.zarr.ZarrConstants;
+import com.bc.zarr.ZarrUtils;
+import com.bc.zarr.storage.Store;
+
+import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.AwsCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.sync.ResponseTransformer;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.ListObjectsRequest;
+import software.amazon.awssdk.services.s3.model.ListObjectsResponse;
+import software.amazon.awssdk.services.s3.model.S3Object;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class S3FileSystemStore implements Store {
+
+ private Path root;
+ S3Client client;
+ protected static final Logger LOGGER =
+ LoggerFactory.getLogger(S3FileSystemStore.class);
+
+ public S3FileSystemStore(String path, FileSystem fileSystem) {
+ if (fileSystem == null) {
+ root = Paths.get(path);
+ } else {
+ root = fileSystem.getPath(path);
+ }
+ setupClient();
+ }
+
+ public void updateRoot(String path) {
+ root = Paths.get(path);
+ }
+
+ public String getRoot() {
+ return root.toString();
+ }
+
+ private void setupClient() {
+ String[] pathSplit = root.toString().split(File.separator);
+ String endpoint = "https://" + pathSplit[1] + File.separator;
+ URI endpoint_uri;
+ try {
+ endpoint_uri = new URI(endpoint);
+ final S3Configuration config = S3Configuration.builder()
+ .pathStyleAccessEnabled(true)
+ .build();
+ AwsCredentials credentials = AnonymousCredentialsProvider.create().resolveCredentials();
+ client = S3Client.builder()
+ .endpointOverride(endpoint_uri)
+ .serviceConfiguration(config)
+ .region(Region.EU_WEST_1) // Ignored but required by the client
+ .credentialsProvider(StaticCredentialsProvider.create(credentials)).build();
+
+ } catch (URISyntaxException e) {
+ LOGGER.info( "Syntax error generating URI from endpoint: " + endpoint);
+ e.printStackTrace();
+ } catch (Exception e) {
+ LOGGER.info("Exception caught while constructing S3 client");
+ e.printStackTrace();
+ }
+
+ }
+
+ public void close() {
+ if (client != null) {
+ client.close();
+ }
+ }
+
+ public S3FileSystemStore(Path rootPath) {
+ root = rootPath;
+ setupClient();
+ }
+
+ @Override
+ public InputStream getInputStream(String key) throws IOException {
+ String[] pathSplit = root.toString().split(File.separator);
+ String bucketName = pathSplit[2];
+ String key2 = root.toString().substring(root.toString().indexOf(pathSplit[3]), root.toString().length()) + File.separator + key;
+
+ try {
+ GetObjectRequest getRequest = GetObjectRequest.builder().bucket(bucketName).key(key2).build();
+ ResponseInputStream responseStream = client.getObject(getRequest, ResponseTransformer.toInputStream());
+ return responseStream;
+ } catch (Exception e) {
+ LOGGER.info( "Unable to locate or access key: " + key2);
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ @Override
+ public OutputStream getOutputStream(String key) throws IOException {
+ final Path filePath = root.resolve(key);
+ final Path dir = filePath.getParent();
+ Files.createDirectories(dir);
+ return Files.newOutputStream(filePath);
+ }
+
+ @Override
+ public void delete(String key) throws IOException {
+ final Path toBeDeleted = root.resolve(key);
+ if (Files.isDirectory(toBeDeleted)) {
+ ZarrUtils.deleteDirectoryTreeRecursively(toBeDeleted);
+ }
+ if (Files.exists(toBeDeleted)){
+ Files.delete(toBeDeleted);
+ }
+ if (Files.exists(toBeDeleted)|| Files.isDirectory(toBeDeleted)) {
+ throw new IOException("Unable to initialize " + toBeDeleted.toAbsolutePath().toString());
+ }
+ }
+
+ @Override
+ public TreeSet getArrayKeys() throws IOException {
+ return getKeysFor(ZarrConstants.FILENAME_DOT_ZARRAY);
+ }
+
+ @Override
+ public TreeSet getGroupKeys() throws IOException {
+ return getKeysFor(ZarrConstants.FILENAME_DOT_ZGROUP);
+ }
+
+ /**
+ * Copied from {@com.bc.zarr.storage.FileSystemStorage#getKeysEndingWith(String).
+ *
+ * @param suffix
+ * @return
+ * @throws IOException
+ */
+ public TreeSet getKeysEndingWith(String suffix) throws IOException {
+ return (TreeSet)Files.walk(this.root).filter((path) -> {
+ return path.toString().endsWith(suffix);
+ }).map((path) -> {
+ return this.root.relativize(path).toString();
+ }).collect(Collectors.toCollection(TreeSet::new));
+ }
+
+ /**
+ * Copied from {@com.bc.zarr.storage.FileSystemStorage#getRelativeLeafKeys(String).
+ *
+ * @param key
+ * @return
+ * @throws IOException
+ */
+ public Stream getRelativeLeafKeys(String key) throws IOException {
+ Path walkingRoot = this.root.resolve(key);
+ return Files.walk(walkingRoot).filter((path) -> {
+ return !Files.isDirectory(path, new LinkOption[0]);
+ }).map((path) -> {
+ return walkingRoot.relativize(path).toString();
+ }).map(ZarrUtils::normalizeStoragePath).filter((s) -> {
+ return s.trim().length() > 0;
+ });
+ }
+
+ private TreeSet getKeysFor(String suffix) throws IOException {
+ TreeSet keys = new TreeSet();
+
+ String[] pathSplit = root.toString().split(File.separator);
+
+ String bucketName = pathSplit[2];
+ String key2 = root.toString().substring(root.toString().indexOf(pathSplit[3]), root.toString().length());
+
+ ListObjectsRequest listObjectsRequest = ListObjectsRequest
+ .builder()
+ .bucket(bucketName)
+ .prefix(key2)
+ .build()
+ ;
+
+ ListObjectsResponse listObjectsResponse = null;
+ String lastKey = null;
+
+ do {
+ if ( listObjectsResponse != null ) {
+ listObjectsRequest = listObjectsRequest.toBuilder()
+ .marker(lastKey)
+ .build()
+ ;
+ }
+
+ listObjectsResponse = client.listObjects(listObjectsRequest);
+ List objects = listObjectsResponse.contents();
+
+ // Iterate over results
+ ListIterator iterVals = objects.listIterator();
+ while (iterVals.hasNext()) {
+ S3Object object = (S3Object) iterVals.next();
+ String k = object.key();
+ if (k.contains(suffix)) {
+ String key = k.substring(k.indexOf(key2) + key2.length() + 1, k.indexOf(suffix));
+ if (!key.isEmpty()) {
+ keys.add(key.substring(0, key.length()-1));
+ }
+ }
+ lastKey = k;
+ }
+ } while ( listObjectsResponse.isTruncated() );
+
+ return keys;
+ }
+
+ public ArrayList getFiles() throws IOException {
+ ArrayList keys = new ArrayList();
+
+ String[] pathSplit = root.toString().split(File.separator);
+ String bucketName = pathSplit[2];
+ String key2 = root.toString().substring(root.toString().indexOf(pathSplit[3]), root.toString().length());
+
+ ListObjectsRequest listObjectsRequest = ListObjectsRequest
+ .builder()
+ .bucket(bucketName)
+ .prefix(key2)
+ .build()
+ ;
+
+ ListObjectsResponse listObjectsResponse = null;
+ String lastKey = null;
+
+ do {
+ if ( listObjectsResponse != null ) {
+ listObjectsRequest = listObjectsRequest.toBuilder()
+ .marker(lastKey)
+ .build()
+ ;
+ }
+
+ listObjectsResponse = client.listObjects(listObjectsRequest);
+ List objects = listObjectsResponse.contents();
+
+ // Iterate over results
+ ListIterator iterVals = objects.listIterator();
+ while (iterVals.hasNext()) {
+ S3Object object = (S3Object) iterVals.next();
+ String k = object.key();
+ String key = k.substring(k.indexOf(key2) + key2.length() + 1, k.length());
+ if (!key.isEmpty()) {
+ keys.add(key.substring(0, key.length()-1));
+ }
+ lastKey = k;
+ }
+ } while ( listObjectsResponse.isTruncated() );
+ return keys;
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/loci/formats/in/ZarrReader.java b/src/loci/formats/in/ZarrReader.java
index 5bfc736..19c153c 100644
--- a/src/loci/formats/in/ZarrReader.java
+++ b/src/loci/formats/in/ZarrReader.java
@@ -97,6 +97,8 @@ public class ZarrReader extends FormatReader {
public static final String LIST_PIXELS_ENV_KEY = "OME_ZARR_LIST_PIXELS";
public static final String INCLUDE_LABELS_KEY = "omezarr.include_labels";
public static final boolean INCLUDE_LABELS_DEFAULT = false;
+ public static final String ALT_STORE_KEY = "omezarr.alt_store";
+ public static final String ALT_STORE_DEFAULT = null;
protected transient ZarrService zarrService;
private ArrayList arrayPaths = new ArrayList();
@@ -468,7 +470,7 @@ public void reopenFile() throws IOException {
}
protected void initializeZarrService(String rootPath) throws IOException, FormatException {
- zarrService = new JZarrServiceImpl(rootPath);
+ zarrService = new JZarrServiceImpl(altStore());
openZarr();
}
@@ -1200,6 +1202,19 @@ public boolean includeLabels() {
}
return INCLUDE_LABELS_DEFAULT;
}
+
+ /**
+ * Used to provide the location of an alternative file store where the data is located
+ * @return String representing the root path of the alternative file store or null if no alternative location exist
+ */
+ public String altStore() {
+ MetadataOptions options = getMetadataOptions();
+ if (options instanceof DynamicMetadataOptions) {
+ return ((DynamicMetadataOptions) options).get(
+ ALT_STORE_KEY, ALT_STORE_DEFAULT);
+ }
+ return ALT_STORE_DEFAULT;
+ }
private boolean systemEnvListPixels() {
String value = System.getenv(LIST_PIXELS_ENV_KEY);
diff --git a/src/loci/formats/services/JZarrServiceImpl.java b/src/loci/formats/services/JZarrServiceImpl.java
index dce0186..482315f 100644
--- a/src/loci/formats/services/JZarrServiceImpl.java
+++ b/src/loci/formats/services/JZarrServiceImpl.java
@@ -54,6 +54,7 @@
import loci.common.services.AbstractService;
import loci.formats.FormatException;
import loci.formats.FormatTools;
+import loci.formats.S3FileSystemStore;
import loci.formats.meta.IPyramidStore;
import loci.formats.meta.MetadataRetrieve;
import ucar.ma2.InvalidRangeException;
@@ -65,6 +66,7 @@ public class JZarrServiceImpl extends AbstractService
public static final String NO_ZARR_MSG = "JZARR is required to read Zarr files.";
// -- Fields --
+ S3FileSystemStore s3fs;
ZarrArray zarrArray;
String currentId;
Compressor zlibComp = CompressorFactory.create("zlib", "level", 8); // 8 = compression level .. valid values 0 .. 9
@@ -76,20 +78,20 @@ public class JZarrServiceImpl extends AbstractService
*/
public JZarrServiceImpl(String root) {
checkClassDependency(com.bc.zarr.ZarrArray.class);
- if (root != null && root.toLowerCase().contains("s3:")) {
- LOGGER.warn("S3 access currently not supported");
+ if (root != null && (root.toLowerCase().contains("s3:") || root.toLowerCase().contains("s3."))) {
+ s3fs = new S3FileSystemStore(Paths.get(root));
}
}
@Override
public void open(String file) throws IOException, FormatException {
currentId = file;
- // TODO: Update s3 location identification
- if (!file.toLowerCase().contains("s3:")) {
+ if (s3fs == null) {
zarrArray = ZarrArray.open(file);
}
- else {
- LOGGER.warn("S3 access currently not supported");
+ else {
+ s3fs.updateRoot(getZarrRoot(s3fs.getRoot()) + stripZarrRoot(file));
+ zarrArray = ZarrArray.open(s3fs);
}
}
@@ -100,48 +102,48 @@ public void open(String id, ZarrArray array) {
public Map getGroupAttr(String path) throws IOException, FormatException {
ZarrGroup group = null;
- if (!path.toLowerCase().contains("s3:")) {
+ if (s3fs == null) {
group = ZarrGroup.open(path);
}
else {
- LOGGER.warn("S3 access currently not supported");
- return null;
+ s3fs.updateRoot(getZarrRoot(s3fs.getRoot()) + stripZarrRoot(path));
+ group = ZarrGroup.open(s3fs);
}
return group.getAttributes();
}
public Map getArrayAttr(String path) throws IOException, FormatException {
ZarrArray array = null;
- if (!path.toLowerCase().contains("s3:")) {
+ if (s3fs == null) {
array = ZarrArray.open(path);
}
else {
- LOGGER.warn("S3 access currently not supported");
- return null;
+ s3fs.updateRoot(getZarrRoot(s3fs.getRoot()) + stripZarrRoot(path));
+ array = ZarrArray.open(s3fs);
}
return array.getAttributes();
}
public Set getGroupKeys(String path) throws IOException, FormatException {
ZarrGroup group = null;
- if (!path.toLowerCase().contains("s3:")) {
+ if (s3fs == null) {
group = ZarrGroup.open(path);
}
else {
- LOGGER.warn("S3 access currently not supported");
- return null;
+ s3fs.updateRoot(getZarrRoot(s3fs.getRoot()) + stripZarrRoot(path));
+ group = ZarrGroup.open(s3fs);
}
return group.getGroupKeys();
}
public Set getArrayKeys(String path) throws IOException, FormatException {
ZarrGroup group = null;
- if (!path.toLowerCase().contains("s3:")) {
+ if (s3fs == null) {
group = ZarrGroup.open(path);
}
else {
- LOGGER.warn("S3 access currently not supported");
- return null;
+ s3fs.updateRoot(getZarrRoot(s3fs.getRoot()) + stripZarrRoot(path));
+ group = ZarrGroup.open(s3fs);
}
return group.getArrayKeys();
}
@@ -247,6 +249,9 @@ public boolean isLittleEndian() {
public void close() throws IOException {
zarrArray = null;
currentId = null;
+ if (s3fs != null) {
+ s3fs.close();
+ }
}
@Override
@@ -358,5 +363,12 @@ public void create(String id, MetadataRetrieve meta, int[] chunks) throws IOExce
create(id, meta, chunks, Compression.NONE);
}
+ private String stripZarrRoot(String path) {
+ return path.substring(path.indexOf(".zarr")+5);
+ }
+
+ private String getZarrRoot(String path) {
+ return path.substring(0, path.indexOf(".zarr")+5);
+ }
}