diff --git a/src/main/java/land/oras/Annotations.java b/src/main/java/land/oras/Annotations.java index 07337c4f..ec6c8119 100644 --- a/src/main/java/land/oras/Annotations.java +++ b/src/main/java/land/oras/Annotations.java @@ -85,6 +85,27 @@ public Map getFileAnnotations(String key) { return this.filesAnnotations().getOrDefault(key, new HashMap<>()); } + /** + * Check if there are annotations for a file + * @param key The key + * @return True if there are annotations, false otherwise + */ + public boolean hasFileAnnotations(String key) { + return this.filesAnnotations().containsKey(key); + } + + /** + * Create a new annotations record with the given file annotations + * @param key The key of the file annotations + * @param annotations The file annotations + * @return The new annotations record + */ + public Annotations withFileAnnotations(String key, Map annotations) { + Map> newFilesAnnotations = new HashMap<>(this.filesAnnotations()); + newFilesAnnotations.put(key, annotations); + return new Annotations(this.configAnnotations(), this.manifestAnnotations(), newFilesAnnotations); + } + /** * Annotations file format */ diff --git a/src/main/java/land/oras/OCI.java b/src/main/java/land/oras/OCI.java index afb26311..4f140b5a 100644 --- a/src/main/java/land/oras/OCI.java +++ b/src/main/java/land/oras/OCI.java @@ -28,6 +28,7 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -156,7 +157,7 @@ protected List collectLayers(T ref, String contentType, boolean includeAl * @param paths The paths to the files * @return The layers */ - protected final List pushLayers(T ref, boolean withDigest, LocalPath... paths) { + protected final List pushLayers(T ref, Annotations annotations, boolean withDigest, LocalPath... paths) { List layers = new ArrayList<>(); for (LocalPath path : paths) { try { @@ -172,15 +173,20 @@ protected final List pushLayers(T ref, boolean withDigest, LocalPath... p ? path.getPath().getFileName().toString() : path.getPath().toString(); LOG.debug("Uploading directory as archive with title: {}", title); + + Map layerAnnotations = annotations.hasFileAnnotations(title) + ? annotations.getFileAnnotations(title) + : new LinkedHashMap<>(Map.of(Const.ANNOTATION_TITLE, title)); + + // Add oras digest/unpack + layerAnnotations.put( + Const.ANNOTATION_ORAS_CONTENT_DIGEST, + ref.getAlgorithm().digest(tempTar.getPath())); + layerAnnotations.put(Const.ANNOTATION_ORAS_UNPACK, "true"); + Layer layer = pushBlob(ref, is) .withMediaType(path.getMediaType()) - .withAnnotations(Map.of( - Const.ANNOTATION_TITLE, - title, - Const.ANNOTATION_ORAS_CONTENT_DIGEST, - ref.getAlgorithm().digest(tempTar.getPath()), - Const.ANNOTATION_ORAS_UNPACK, - "true")); + .withAnnotations(layerAnnotations); layers.add(layer); LOG.info("Uploaded directory: {}", layer.getDigest()); } @@ -190,11 +196,14 @@ protected final List pushLayers(T ref, boolean withDigest, LocalPath... p if (withDigest) { ref = ref.withDigest(ref.getAlgorithm().digest(path.getPath())); } + String title = path.getPath().getFileName().toString(); + Map layerAnnotations = annotations.hasFileAnnotations(title) + ? annotations.getFileAnnotations(title) + : Map.of(Const.ANNOTATION_TITLE, title); + Layer layer = pushBlob(ref, is) .withMediaType(path.getMediaType()) - .withAnnotations(Map.of( - Const.ANNOTATION_TITLE, - path.getPath().getFileName().toString())); + .withAnnotations(layerAnnotations); layers.add(layer); LOG.info("Uploaded: {}", layer.getDigest()); } @@ -413,7 +422,7 @@ public abstract Manifest pushArtifact( public Manifest attachArtifact(T ref, ArtifactType artifactType, Annotations annotations, LocalPath... paths) { // Push layers - List layers = pushLayers(ref, true, paths); + List layers = pushLayers(ref, annotations, true, paths); // Get the subject from the descriptor Descriptor descriptor = getDescriptor(ref); diff --git a/src/main/java/land/oras/OCILayout.java b/src/main/java/land/oras/OCILayout.java index b4fa6eca..70e676fe 100644 --- a/src/main/java/land/oras/OCILayout.java +++ b/src/main/java/land/oras/OCILayout.java @@ -83,7 +83,7 @@ public Manifest pushArtifact( } // Push layers - List layers = pushLayers(ref, true, paths); + List layers = pushLayers(ref, annotations, true, paths); // Push the config like any other blob Config configToPush = config != null ? config : Config.empty(); diff --git a/src/main/java/land/oras/Registry.java b/src/main/java/land/oras/Registry.java index b8a92baa..83a3c9f0 100644 --- a/src/main/java/land/oras/Registry.java +++ b/src/main/java/land/oras/Registry.java @@ -406,7 +406,7 @@ public Manifest pushArtifact( ContainerRef resolvedRef = containerRef.forRegistry(this).forRegistry(resolvedRegistry); // Push layers - List layers = pushLayers(resolvedRef, false, paths); + List layers = pushLayers(resolvedRef, annotations, false, paths); // Add layer and config manifest = manifest.withLayers(layers).withConfig(pushedConfig); diff --git a/src/test/java/land/oras/AnnotationsTest.java b/src/test/java/land/oras/AnnotationsTest.java index 261ccf8a..ed1982c0 100644 --- a/src/test/java/land/oras/AnnotationsTest.java +++ b/src/test/java/land/oras/AnnotationsTest.java @@ -21,6 +21,8 @@ package land.oras; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Map; import org.junit.jupiter.api.Test; @@ -51,6 +53,14 @@ public void nullAnnotations() { assertEquals(0, annotations.filesAnnotations().size()); } + @Test + public void shouldAddFileAnnotation() { + Annotations annotations = Annotations.empty().withFileAnnotations("cake.txt", Map.of("fun", "more cream")); + assertEquals("more cream", annotations.getFileAnnotations("cake.txt").get("fun")); + assertTrue(annotations.hasFileAnnotations("cake.txt")); + assertFalse(annotations.hasFileAnnotations("nonexistent.txt")); + } + @Test public void toJson() { Annotations annotations = new Annotations( diff --git a/src/test/java/land/oras/RegistryTest.java b/src/test/java/land/oras/RegistryTest.java index 555569f1..3391bfc4 100644 --- a/src/test/java/land/oras/RegistryTest.java +++ b/src/test/java/land/oras/RegistryTest.java @@ -1336,14 +1336,33 @@ void testShouldArtifactWithAnnotations() throws IOException { Files.writeString(pomFile, "my pom file"); // Push the main OCI artifact - Annotations annotations = Annotations.ofManifest(Map.of("foo", "bar")); + Annotations annotations = + Annotations.ofManifest(Map.of("foo", "bar")).withFileAnnotations("jenkins.png", Map.of("foo", "bar")); + + // Add image (without title (so it's not unpack) and specific annotation) Manifest manifest = registry.pushArtifact( - containerRef, ArtifactType.from(artifactType), annotations, LocalPath.of(pomFile, "application/xml")); + containerRef, + ArtifactType.from(artifactType), + annotations, + LocalPath.of(pomFile, "application/xml"), + LocalPath.of(Path.of("src/test/resources/img/jenkins.png"), "image/png")); - // Check annotations + // Check annotations (manifest) assertEquals(2, manifest.getAnnotations().size()); assertEquals("bar", manifest.getAnnotations().get("foo")); assertNotNull(manifest.getAnnotations().get(Const.ANNOTATION_CREATED)); + + // Check annotations (layer 0) + Layer layer = manifest.getLayers().get(0); + assertEquals(1, layer.getAnnotations().size()); + assertEquals( + "pom.xml", layer.getAnnotations().get(Const.ANNOTATION_TITLE), "Title annotation should be pom.xml"); + + // Check annotation (layer 1) + Layer layer2 = manifest.getLayers().get(1); + assertEquals(1, layer2.getAnnotations().size()); + assertNull(layer2.getAnnotations().get(Const.ANNOTATION_TITLE), "Title should not be added"); + assertEquals("bar", layer2.getAnnotations().get("foo"), "Custom annotation should be preserved"); } @Test