Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2d29a3b
add MemoryStore
brokkoli71 Nov 16, 2025
218f079
memorystore map string keys
brokkoli71 Nov 17, 2025
fa2626b
fix memorystore with List<String> keys
brokkoli71 Nov 21, 2025
4763778
Merge branch 'main' into memory-store
brokkoli71 Nov 21, 2025
36cb46a
remove code duplication and add v2.Group.setAttributes
brokkoli71 Nov 21, 2025
78fa912
add ConcurrentMemoryStore
brokkoli71 Nov 21, 2025
bd3bd7c
add tests
brokkoli71 Nov 21, 2025
e1f8e49
fix Group.writeMetadata
brokkoli71 Nov 21, 2025
50b40f1
add attributes to tests
brokkoli71 Nov 21, 2025
dbf7541
Merge branch 'main' into memory-store
brokkoli71 Nov 24, 2025
6094489
Array.create and Group.create with no store/path arguments should def…
brokkoli71 Nov 24, 2025
250daf1
Add Javadoc comments for Group methods
brokkoli71 Nov 24, 2025
261bc10
add ZipStore tests
brokkoli71 Nov 28, 2025
a6512de
make MemoryStore allways concurrent
brokkoli71 Nov 30, 2025
f650fd6
add ZipStore tests
brokkoli71 Nov 28, 2025
dc4dd7d
Merge remote-tracking branch 'origin/zip-store' into zip-store
brokkoli71 Dec 1, 2025
12a86b7
refactor and unify outputs of Store.list
brokkoli71 Dec 1, 2025
bce731e
read zip store
brokkoli71 Dec 1, 2025
135ca6d
end index validation in MemoryStore
brokkoli71 Dec 5, 2025
bb5bdf9
Merge branch 'main' into memory-store
brokkoli71 Dec 5, 2025
ff9ff99
end index validation in MemoryStore
brokkoli71 Dec 5, 2025
2fde2fb
Bump to 0.0.6 to trigger release
joshmoore Dec 4, 2025
013a417
Remove zarr-python setup from deploy workflow
normanrz Dec 4, 2025
6d25740
Bump to 0.0.7
joshmoore Dec 4, 2025
d422273
Bump to 0.0.8
joshmoore Dec 4, 2025
cf8d562
Create settings.xml
joshmoore Dec 4, 2025
f146d8d
Bump to 0.0.9
joshmoore Dec 4, 2025
f163b29
write buffer of zip store
brokkoli71 Dec 5, 2025
c525a63
use apache commons compress for zip file read and write
brokkoli71 Dec 5, 2025
c4448d5
set Zip64Mode.AsNeeded
brokkoli71 Dec 11, 2025
025265c
test Zipped OME-Zarr requirements
brokkoli71 Dec 11, 2025
7bc8b4a
Sort zarr.json files in breadth-first order within BufferedZipStore
brokkoli71 Dec 11, 2025
cb866a2
Merge branch 'main' into zip-store
brokkoli71 Dec 11, 2025
cb584e7
manually read zip comment
brokkoli71 Dec 12, 2025
071430f
refactor read zip comment
brokkoli71 Dec 12, 2025
5196562
test zip store with v2
brokkoli71 Dec 12, 2025
6737660
use com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream i…
brokkoli71 Dec 12, 2025
2625e13
add ReadOnlyZipStore
brokkoli71 Dec 12, 2025
7234f96
fix ReadOnlyZipStore for zips with
brokkoli71 Dec 12, 2025
6eb760c
Merge branch 'memory-store' into zip-store
brokkoli71 Dec 12, 2025
8b1e1ca
add BufferedZipStore parameter flushOnWrite
brokkoli71 Dec 12, 2025
a2a2c67
fix testMemoryStore
brokkoli71 Dec 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.28.0</version>
</dependency>
</dependencies>

<repositories>
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/dev/zarr/zarrjava/core/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,19 @@ public static Group open(String path) throws IOException, ZarrException {
}

@Nullable
public abstract Node get(String key) throws ZarrException;
public abstract Node get(String[] key) throws ZarrException, IOException;

@Nullable
public Node get(String key) throws ZarrException, IOException {
return get(new String[]{key});
}

public Stream<Node> list() {
return storeHandle.list()
.map(key -> {
try {
return get(key);
} catch (ZarrException e) {
} catch (Exception e) {
throw new RuntimeException(e);
}
})
Expand Down
297 changes: 297 additions & 0 deletions src/main/java/dev/zarr/zarrjava/store/BufferedZipStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
package dev.zarr.zarrjava.store;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream;
import org.apache.commons.compress.archivers.zip.*;

import java.util.zip.CRC32;
import java.util.zip.ZipEntry; // for STORED constant

import static dev.zarr.zarrjava.utils.ZipUtils.getZipCommentFromBuffer;


/** A Store implementation that buffers reads and writes and flushes them to an underlying Store as a zip file.
*/
public class BufferedZipStore implements Store, Store.ListableStore {

private final StoreHandle underlyingStore;
private final Store.ListableStore bufferStore;
private String archiveComment;
private boolean flushOnWrite;

private void writeBuffer() throws IOException{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine for now, but it creates copies of the entire zip archive in memory, so it could be expensive. Maybe we could add a streaming-write option the the store interface in the future.

// create zip file bytes from buffer store and write to underlying store
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(baos)) {
zos.setUseZip64(Zip64Mode.AsNeeded);
if (archiveComment != null) {
zos.setComment(archiveComment);
}
Stream<String[]> entries = bufferStore.list().sorted(
(a, b) -> {
boolean aIsZarr = a.length > 0 && a[a.length - 1].equals("zarr.json");
boolean bIsZarr = b.length > 0 && b[b.length - 1].equals("zarr.json");
// first all zarr.json files
if (aIsZarr && !bIsZarr) {
return -1;
} else if (!aIsZarr && bIsZarr) {
return 1;
} else if (aIsZarr && bIsZarr) {
// sort zarr.json in BFS order within same depth by lexicographical order
if (a.length != b.length) {
return Integer.compare(a.length, b.length);
} else {
return String.join("/", a).compareTo(String.join("/", b));
}
} else {
// then all other files in lexicographical order
return String.join("/", a).compareTo(String.join("/", b));
}
}
);

entries.forEach(keys -> {
try {
if (keys == null || keys.length == 0) {
// skip root entry
return;
}
String entryName = String.join("/", keys);
ByteBuffer bb = bufferStore.get(keys);
if (bb == null) {
// directory entry: ensure trailing slash
if (!entryName.endsWith("/")) {
entryName = entryName + "/";
}
ZipArchiveEntry dirEntry = new ZipArchiveEntry(entryName);
dirEntry.setMethod(ZipEntry.STORED);
dirEntry.setSize(0);
dirEntry.setCrc(0);
zos.putArchiveEntry(dirEntry);
zos.closeArchiveEntry();
} else {
// read bytes from ByteBuffer without modifying original
ByteBuffer dup = bb.duplicate();
int len = dup.remaining();
byte[] bytes = new byte[len];
dup.get(bytes);

// compute CRC and set size for STORED (no compression)
CRC32 crc = new CRC32();
crc.update(bytes, 0, bytes.length);
ZipArchiveEntry fileEntry = new ZipArchiveEntry(entryName);
fileEntry.setMethod(ZipEntry.STORED);
fileEntry.setSize(bytes.length);
fileEntry.setCrc(crc.getValue());

zos.putArchiveEntry(fileEntry);
zos.write(bytes);
zos.closeArchiveEntry();
}
} catch (IOException e) {
// wrap checked exception so it can be rethrown from stream for handling below
throw new RuntimeException(e);
}
});
zos.finish();
} catch (RuntimeException e) {
// unwrap and rethrow IOExceptions thrown inside the lambda
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
throw e;
}

byte[] zipBytes = baos.toByteArray();
// write zip bytes back to underlying store
underlyingStore.set(ByteBuffer.wrap(zipBytes));
}


private void loadBuffer() throws IOException{
// read zip file bytes from underlying store and populate buffer store
ByteBuffer buffer = underlyingStore.read();
if (buffer == null) {
return;
}
byte[] bufArray;
if (buffer.hasArray()) {
bufArray = buffer.array();
} else {
bufArray = new byte[buffer.remaining()];
buffer.duplicate().get(bufArray);
}
this.archiveComment = getZipCommentFromBuffer(bufArray);
try (ZipArchiveInputStream zis = new ZipArchiveInputStream(new ByteBufferBackedInputStream(buffer))) {
ZipArchiveEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] tmp = new byte[8192];
int read;
while ((read = zis.read(tmp)) != -1) {
baos.write(tmp, 0, read);
}
byte[] bytes = baos.toByteArray();
bufferStore.set(new String[]{entry.getName()}, ByteBuffer.wrap(bytes));
}
}
}

public BufferedZipStore(@Nonnull StoreHandle underlyingStore, @Nonnull Store.ListableStore bufferStore, @Nullable String archiveComment, boolean flushOnWrite) {
this.underlyingStore = underlyingStore;
this.bufferStore = bufferStore;
this.archiveComment = archiveComment;
this.flushOnWrite = flushOnWrite;
try {
loadBuffer();
} catch (IOException e) {
throw new RuntimeException("Failed to load buffer from underlying store", e);
}
}

public BufferedZipStore(@Nonnull StoreHandle underlyingStore, @Nonnull Store.ListableStore bufferStore, @Nullable String archiveComment) {
this(underlyingStore, bufferStore, archiveComment, true);
}

public BufferedZipStore(@Nonnull StoreHandle underlyingStore, @Nonnull Store.ListableStore bufferStore) {
this(underlyingStore, bufferStore, null);
}

public BufferedZipStore(@Nonnull StoreHandle underlyingStore, String archiveComment) {
this(underlyingStore, new MemoryStore(), archiveComment);
}

public BufferedZipStore(@Nonnull StoreHandle underlyingStore) {
this(underlyingStore, (String) null);
}

public BufferedZipStore(@Nonnull Path underlyingStore, String archiveComment) {
this(new FilesystemStore(underlyingStore.getParent()).resolve(underlyingStore.getFileName().toString()), archiveComment);
}

public BufferedZipStore(@Nonnull Path underlyingStore) {
this(underlyingStore, null);
}

public BufferedZipStore(@Nonnull String underlyingStorePath, String archiveComment) {
this(Paths.get(underlyingStorePath), archiveComment);
}

public BufferedZipStore(@Nonnull String underlyingStorePath) {
this(underlyingStorePath, null);
}

public BufferedZipStore(@Nonnull StoreHandle underlyingStore, @Nonnull Store.ListableStore bufferStore, boolean flushOnWrite) {
this(underlyingStore, bufferStore, null, flushOnWrite);
}

public BufferedZipStore(@Nonnull StoreHandle underlyingStore, String archiveComment, boolean flushOnWrite) {
this(underlyingStore, new MemoryStore(), archiveComment, flushOnWrite);
}

public BufferedZipStore(@Nonnull StoreHandle underlyingStore, boolean flushOnWrite) {
this(underlyingStore, (String) null, flushOnWrite);
}

public BufferedZipStore(@Nonnull Path underlyingStore, String archiveComment, boolean flushOnWrite) {
this(new FilesystemStore(underlyingStore.getParent()).resolve(underlyingStore.getFileName().toString()), archiveComment, flushOnWrite);
}

public BufferedZipStore(@Nonnull Path underlyingStore, boolean flushOnWrite) {
this(underlyingStore, null, flushOnWrite);
}

public BufferedZipStore(@Nonnull String underlyingStorePath, String archiveComment, boolean flushOnWrite) {
this(Paths.get(underlyingStorePath), archiveComment, flushOnWrite);
}

public BufferedZipStore(@Nonnull String underlyingStorePath, boolean flushOnWrite) {
this(underlyingStorePath, null, flushOnWrite);
}


/**
* Flushes the buffer and archiveComment to the underlying store as a zip file.
*/
public void flush() throws IOException {
writeBuffer();
}

public String getArchiveComment() {
return archiveComment;
}

@Override
public Stream<String[]> list(String[] keys) {
return bufferStore.list(keys);
}

@Override
public boolean exists(String[] keys) {
return bufferStore.exists(keys);
}

@Nullable
@Override
public ByteBuffer get(String[] keys) {
return bufferStore.get(keys);
}

@Nullable
@Override
public ByteBuffer get(String[] keys, long start) {
return bufferStore.get(keys, start);
}

@Nullable
@Override
public ByteBuffer get(String[] keys, long start, long end) {
return bufferStore.get(keys, start, end);
}

@Override
public void set(String[] keys, ByteBuffer bytes) {
bufferStore.set(keys, bytes);
if (flushOnWrite) {
try {
writeBuffer();
} catch (IOException e) {
throw new RuntimeException("Failed to flush buffer to underlying store after set operation", e);
}
}
}

@Override
public void delete(String[] keys) {
bufferStore.delete(keys);
if (flushOnWrite) {
try {
writeBuffer();
} catch (IOException e) {
throw new RuntimeException("Failed to flush buffer to underlying store after delete operation", e);
}
}
}

@Nonnull
@Override
public StoreHandle resolve(String... keys) {
return new StoreHandle(this, keys);
}

@Override
public String toString() {
return "BufferedZipStore(" + underlyingStore.toString() + ")";
}
}
12 changes: 9 additions & 3 deletions src/main/java/dev/zarr/zarrjava/store/FilesystemStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,16 @@ public void delete(String[] keys) {
throw new RuntimeException(e);
}
}

public Stream<String> list(String[] keys) {
public Stream<String[]> list(String[] keys) {
try {
return Files.list(resolveKeys(keys)).map(p -> p.toFile().getName());
return Files.list(resolveKeys(keys)).map(path -> {
Path relativePath = resolveKeys(keys).relativize(path);
String[] parts = new String[relativePath.getNameCount()];
for (int i = 0; i < relativePath.getNameCount(); i++) {
parts[i] = relativePath.getName(i).toString();
}
return parts;
});
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand Down
Loading
Loading