Skip to content

Commit 3da03c3

Browse files
committed
Push blob to OCI layout
Signed-off-by: Valentin Delaye <[email protected]>
1 parent 648c916 commit 3da03c3

File tree

8 files changed

+201
-75
lines changed

8 files changed

+201
-75
lines changed

src/main/java/land/oras/ContainerRef.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,7 @@ public ContainerRef withDigest(String digest) {
141141
return new ContainerRef(registry, getNamespace(), repository, tag, digest);
142142
}
143143

144-
/**
145-
* Get the algorithm for this container ref
146-
* @return The algorithm
147-
*/
144+
@Override
148145
public SupportedAlgorithm getAlgorithm() {
149146
// Default if not set
150147
if (digest == null) {

src/main/java/land/oras/LayoutRef.java

+11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.nio.file.Path;
2424
import java.util.regex.Pattern;
2525
import land.oras.exception.OrasException;
26+
import land.oras.utils.SupportedAlgorithm;
2627
import org.jspecify.annotations.NullMarked;
2728

2829
/**
@@ -68,4 +69,14 @@ public static LayoutRef parse(String name) {
6869
String tag = matcher.group(2) != null ? matcher.group(2) : matcher.group(3); // Tag or digest
6970
return new LayoutRef(path, tag);
7071
}
72+
73+
@Override
74+
public SupportedAlgorithm getAlgorithm() {
75+
// Default if not set
76+
if (tag == null) {
77+
return SupportedAlgorithm.getDefault();
78+
}
79+
// See https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests
80+
return SupportedAlgorithm.fromDigest(tag);
81+
}
7182
}

src/main/java/land/oras/OCI.java

+47
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222

2323
import java.io.InputStream;
2424
import java.nio.file.Path;
25+
import java.util.Map;
2526
import org.jspecify.annotations.Nullable;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
2629

2730
/**
2831
* Abstract class for OCI operation on remote registry or layout
@@ -31,6 +34,11 @@
3134
*/
3235
public abstract sealed class OCI<T extends Ref> permits Registry, OCILayout {
3336

37+
/**
38+
* The logger
39+
*/
40+
protected static final Logger LOG = LoggerFactory.getLogger(OCI.class);
41+
3442
/**
3543
* Default constructor
3644
*/
@@ -69,6 +77,28 @@ public Manifest pushArtifact(T ref, ArtifactType artifactType, Annotations annot
6977
return pushArtifact(ref, artifactType, annotations, Config.empty(), paths);
7078
}
7179

80+
/**
81+
* Push a blob from file
82+
* @param ref The ref
83+
* @param blob The blob
84+
* @return The layer
85+
*/
86+
public Layer pushBlob(T ref, Path blob) {
87+
return pushBlob(ref, blob, Map.of());
88+
}
89+
90+
/**
91+
* Push config
92+
* @param ref The ref
93+
* @param config The config
94+
* @return The config
95+
*/
96+
public Config pushConfig(T ref, Config config) {
97+
Layer layer = pushBlob(ref, config.getDataBytes());
98+
LOG.debug("Config pushed: {}", layer.getDigest());
99+
return config;
100+
}
101+
72102
/**
73103
* Push an artifact
74104
* @param ref The container
@@ -109,4 +139,21 @@ public abstract Manifest pushArtifact(
109139
* @return The input stream
110140
*/
111141
public abstract InputStream fetchBlob(T ref);
142+
143+
/**
144+
* Push a blob from file
145+
* @param ref The container
146+
* @param blob The blob
147+
* @param annotations The annotations
148+
* @return The layer
149+
*/
150+
public abstract Layer pushBlob(T ref, Path blob, Map<String, String> annotations);
151+
152+
/**
153+
* Push the blob for the given layer
154+
* @param ref The container ref
155+
* @param data The data
156+
* @return The layer
157+
*/
158+
public abstract Layer pushBlob(T ref, byte[] data);
112159
}

src/main/java/land/oras/OCILayout.java

+71-18
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,12 @@
3131
import land.oras.utils.JsonUtils;
3232
import land.oras.utils.SupportedAlgorithm;
3333
import org.jspecify.annotations.Nullable;
34-
import org.slf4j.Logger;
35-
import org.slf4j.LoggerFactory;
3634

3735
/**
3836
* Index from an OCI layout
3937
*/
4038
public final class OCILayout extends OCI<LayoutRef> {
4139

42-
/**
43-
* The logger
44-
*/
45-
private static final Logger LOG = LoggerFactory.getLogger(OCILayout.class);
46-
4740
private final String imageLayoutVersion = "1.0.0";
4841

4942
/**
@@ -120,6 +113,41 @@ public InputStream fetchBlob(LayoutRef ref) {
120113
}
121114
}
122115

116+
@Override
117+
public Layer pushBlob(LayoutRef ref, Path blob, Map<String, String> annotations) {
118+
ensureDigest(ref);
119+
ensureMinimalLayout();
120+
Path blobPath = getBlobPath(ref);
121+
String digest = ref.getAlgorithm().digest(blob);
122+
ensureAlgorithmPath(digest);
123+
LOG.debug("Digest: {}", digest);
124+
try {
125+
if (Files.exists(blobPath)) {
126+
LOG.debug("Blob already exists: {}", blobPath);
127+
return Layer.fromFile(blobPath).withAnnotations(annotations);
128+
}
129+
Files.copy(blob, blobPath);
130+
return Layer.fromFile(blobPath).withAnnotations(annotations);
131+
} catch (IOException e) {
132+
throw new OrasException("Failed to push blob", e);
133+
}
134+
}
135+
136+
@Override
137+
public Layer pushBlob(LayoutRef ref, byte[] data) {
138+
ensureDigest(ref);
139+
ensureMinimalLayout();
140+
String digest = ref.getAlgorithm().digest(data);
141+
ensureAlgorithmPath(digest);
142+
try {
143+
Path path = Files.createTempFile("oras", "blob");
144+
Files.write(path, data);
145+
return pushBlob(ref, path, Map.of());
146+
} catch (IOException e) {
147+
throw new OrasException("Failed to push blob to OCI layout", e);
148+
}
149+
}
150+
123151
private void setPath(Path path) {
124152
this.path = path;
125153
}
@@ -158,6 +186,31 @@ public void copy(Registry registry, ContainerRef containerRef) {
158186
copy(registry, containerRef, false);
159187
}
160188

189+
private void ensureMinimalLayout() {
190+
try {
191+
Files.createDirectories(getBlobPath());
192+
if (!Files.exists(getOciLayoutPath())) {
193+
Files.writeString(getOciLayoutPath(), toJson());
194+
}
195+
if (!Files.exists(getIndexPath())) {
196+
Files.writeString(getIndexPath(), Index.fromManifests(List.of()).toJson());
197+
}
198+
} catch (IOException e) {
199+
throw new OrasException("Failed to create layout", e);
200+
}
201+
}
202+
203+
private void ensureAlgorithmPath(String digest) {
204+
Path prefixDirectory = getBlobAlgorithmPath(digest);
205+
try {
206+
if (!Files.exists(prefixDirectory)) {
207+
Files.createDirectory(prefixDirectory);
208+
}
209+
} catch (IOException e) {
210+
throw new OrasException("Failed to create algorithm path", e);
211+
}
212+
}
213+
161214
/**
162215
* Copy the container ref from registry into oci-layout
163216
* @param registry The registry
@@ -168,13 +221,7 @@ public void copy(Registry registry, ContainerRef containerRef, boolean recursive
168221

169222
try {
170223

171-
// Create blobs directory if needed
172-
Files.createDirectories(getBlobPath());
173-
174-
// Write oci layout JSON
175-
if (!Files.exists(getOciLayoutPath())) {
176-
Files.writeString(getOciLayoutPath(), toJson());
177-
}
224+
ensureMinimalLayout();
178225

179226
Map<String, String> headers = registry.getHeaders(containerRef);
180227
String contentType = headers.get(Const.CONTENT_TYPE_HEADER.toLowerCase());
@@ -235,10 +282,7 @@ else if (registry.isIndexMediaType(contentType)) {
235282
for (Layer layer : registry.collectLayers(containerRef, contentType, true)) {
236283
try (InputStream is = registry.fetchBlob(containerRef.withDigest(layer.getDigest()))) {
237284

238-
Path prefixDirectory = getBlobAlgorithmPath(layer.getDigest());
239-
if (!Files.exists(prefixDirectory)) {
240-
Files.createDirectory(prefixDirectory);
241-
}
285+
ensureAlgorithmPath(layer.getDigest());
242286

243287
Path blobFile = getBlobPath(layer);
244288

@@ -256,6 +300,15 @@ else if (registry.isIndexMediaType(contentType)) {
256300
}
257301
}
258302

303+
private void ensureDigest(LayoutRef ref) {
304+
if (ref.getTag() == null) {
305+
throw new OrasException("Missing ref");
306+
}
307+
if (!SupportedAlgorithm.isSupported(ref.getTag())) {
308+
throw new OrasException("Unsupported digest: %s".formatted(ref.getTag()));
309+
}
310+
}
311+
259312
private Path getOciLayoutPath() {
260313
return path.resolve(Const.OCI_LAYOUT_FILE);
261314
}

src/main/java/land/oras/Ref.java

+7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package land.oras;
2222

23+
import land.oras.utils.SupportedAlgorithm;
2324
import org.jspecify.annotations.NullMarked;
2425
import org.jspecify.annotations.Nullable;
2526

@@ -48,4 +49,10 @@ protected Ref(String tag) {
4849
public @Nullable String getTag() {
4950
return tag;
5051
}
52+
53+
/**
54+
* Get the algorithm
55+
* @return The algorithm
56+
*/
57+
public abstract SupportedAlgorithm getAlgorithm();
5158
}

src/main/java/land/oras/Registry.java

+2-43
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,13 @@
4545
import land.oras.utils.SupportedAlgorithm;
4646
import org.jspecify.annotations.NullMarked;
4747
import org.jspecify.annotations.Nullable;
48-
import org.slf4j.Logger;
49-
import org.slf4j.LoggerFactory;
5048

5149
/**
5250
* A registry is the main entry point for interacting with a container registry
5351
*/
5452
@NullMarked
5553
public final class Registry extends OCI<ContainerRef> {
5654

57-
/**
58-
* The logger
59-
*/
60-
private static final Logger LOG = LoggerFactory.getLogger(Registry.class);
61-
6255
/**
6356
* The HTTP client
6457
*/
@@ -398,23 +391,7 @@ public Manifest attachArtifact(
398391
manifest);
399392
}
400393

401-
/**
402-
* Push a blob from file
403-
* @param containerRef The container
404-
* @param blob The blob
405-
* @return The layer
406-
*/
407-
public Layer pushBlob(ContainerRef containerRef, Path blob) {
408-
return pushBlob(containerRef, blob, Map.of());
409-
}
410-
411-
/**
412-
* Push a blob from file
413-
* @param containerRef The container
414-
* @param blob The blob
415-
* @param annotations The annotations
416-
* @return The layer
417-
*/
394+
@Override
418395
public Layer pushBlob(ContainerRef containerRef, Path blob, Map<String, String> annotations) {
419396
String digest = containerRef.getAlgorithm().digest(blob);
420397
LOG.debug("Digest: {}", digest);
@@ -465,25 +442,7 @@ public Layer pushBlob(ContainerRef containerRef, Path blob, Map<String, String>
465442
return Layer.fromFile(blob).withAnnotations(annotations);
466443
}
467444

468-
/**
469-
* Push config
470-
* @param containerRef The container
471-
* @param config The config
472-
* @return The config
473-
*/
474-
public Config pushConfig(ContainerRef containerRef, Config config) {
475-
Layer layer = pushBlob(containerRef, config.getDataBytes());
476-
LOG.debug("Config pushed: {}", layer.getDigest());
477-
return config;
478-
}
479-
480-
/**
481-
* Push the blob for the given layer in a single post request. Might not be supported by all registries
482-
* Fallback to POST/then PUT (end-4a) if not supported
483-
* @param containerRef The container ref
484-
* @param data The data
485-
* @return The layer
486-
*/
445+
@Override
487446
public Layer pushBlob(ContainerRef containerRef, byte[] data) {
488447
String digest = containerRef.getAlgorithm().digest(data);
489448
if (hasBlob(containerRef.withDigest(digest))) {

src/test/java/land/oras/LayoutRefTest.java

+10
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,14 @@ void shouldParseFolderNameOnly() {
6262
void shouldFailWithInvalidRef() {
6363
assertThrows(OrasException.class, () -> LayoutRef.parse(""));
6464
}
65+
66+
@Test
67+
void shouldGetAlgorithm() {
68+
LayoutRef layoutRef = LayoutRef.parse("foo");
69+
assertEquals("sha256", layoutRef.getAlgorithm().getPrefix());
70+
layoutRef = LayoutRef.parse("foo@sha256:12345");
71+
assertEquals("sha256", layoutRef.getAlgorithm().getPrefix());
72+
layoutRef = LayoutRef.parse("foo@sha512:12345");
73+
assertEquals("sha512", layoutRef.getAlgorithm().getPrefix());
74+
}
6575
}

0 commit comments

Comments
 (0)