2525import java .io .InputStream ;
2626import java .nio .file .Files ;
2727import java .nio .file .Path ;
28+ import java .nio .file .StandardCopyOption ;
2829import java .util .HashMap ;
2930import java .util .LinkedList ;
3031import java .util .List ;
3334import java .util .concurrent .Executors ;
3435import java .util .function .Supplier ;
3536import land .oras .exception .OrasException ;
37+ import land .oras .utils .ArchiveUtils ;
3638import land .oras .utils .Const ;
3739import land .oras .utils .JsonUtils ;
3840import land .oras .utils .SupportedAlgorithm ;
@@ -54,6 +56,14 @@ public final class OCILayout extends OCI<LayoutRef> {
5456 */
5557 private Path path ;
5658
59+ /**
60+ * When non-null the layout is backed by a tar file.
61+ * {@code path} then points to a temporary directory that holds the extracted contents.
62+ * Every mutating operation re-packs that temporary directory back into this tar file.
63+ */
64+ @ Nullable
65+ private Path tarPath ;
66+
5767 /**
5868 * Private constructor
5969 */
@@ -96,6 +106,7 @@ public boolean mountBlob(LayoutRef sourceRef, LayoutRef targetRef) {
96106 } catch (IOException e ) {
97107 throw new OrasException ("Failed to mount blob" , e );
98108 }
109+ packToTar ();
99110 return true ;
100111 }
101112
@@ -214,6 +225,7 @@ public Manifest pushManifest(LayoutRef layoutRef, Manifest manifest) {
214225 } catch (IOException e ) {
215226 throw new OrasException ("Failed to write manifest" , e );
216227 }
228+ packToTar ();
217229 return manifest ;
218230 }
219231
@@ -249,6 +261,7 @@ public Index pushIndex(LayoutRef layoutRef, Index index) {
249261 } catch (IOException e ) {
250262 throw new OrasException ("Failed to write manifest" , e );
251263 }
264+ packToTar ();
252265 return index ;
253266 }
254267
@@ -330,7 +343,9 @@ public Layer pushBlob(LayoutRef ref, Path blob, Map<String, String> annotations)
330343 return Layer .fromFile (blobPath , ref .getAlgorithm ()).withAnnotations (annotations );
331344 }
332345 Files .copy (blob , blobPath );
333- return Layer .fromFile (blobPath , ref .getAlgorithm ()).withAnnotations (annotations );
346+ Layer layer = Layer .fromFile (blobPath , ref .getAlgorithm ()).withAnnotations (annotations );
347+ packToTar ();
348+ return layer ;
334349 } catch (IOException e ) {
335350 throw new OrasException ("Failed to push blob" , e );
336351 }
@@ -357,7 +372,9 @@ public Layer pushBlob(LayoutRef ref, long size, Supplier<InputStream> stream, Ma
357372 Files .copy (is , blobPath );
358373 }
359374 ensureDigest (ref , blobPath );
360- return Layer .fromFile (blobPath , ref .getAlgorithm ()).withAnnotations (annotations );
375+ Layer layer = Layer .fromFile (blobPath , ref .getAlgorithm ()).withAnnotations (annotations );
376+ packToTar ();
377+ return layer ;
361378 } catch (IOException e ) {
362379 throw new OrasException ("Failed to push blob" , e );
363380 }
@@ -406,7 +423,9 @@ public Tags getTags(LayoutRef ref, int n, @Nullable String last) {
406423
407424 @ Override
408425 public Repositories getRepositories () {
409- return new Repositories (List .of (path .getFileName ().toString ()));
426+ // When tar-backed, report the tar file name rather than the temp-dir name
427+ Path reportPath = tarPath != null ? tarPath : path ;
428+ return new Repositories (List .of (reportPath .getFileName ().toString ()));
410429 }
411430
412431 @ Override
@@ -438,6 +457,29 @@ private void setPath(Path path) {
438457 this .path = path ;
439458 }
440459
460+ private void setTarPath (Path tarPath ) {
461+ this .tarPath = tarPath ;
462+ }
463+
464+ /**
465+ * Re-pack the working directory back into the backing tar file.
466+ * Called after every mutating operation when {@link #tarPath} is non-null.
467+ */
468+ private void packToTar () {
469+ if (tarPath == null ) {
470+ return ;
471+ }
472+ try {
473+ // Pack without directory-name prefix so entries sit at the root of the tar,
474+ // matching the OCI Image Layout tar format (blobs/, index.json, oci-layout …).
475+ LocalPath packed = ArchiveUtils .tar (LocalPath .of (path ), false );
476+ // Atomically replace the backing tar file
477+ Files .move (packed .getPath (), tarPath , StandardCopyOption .REPLACE_EXISTING );
478+ } catch (IOException e ) {
479+ throw new OrasException ("Failed to repack OCI layout tar: " + tarPath , e );
480+ }
481+ }
482+
441483 /**
442484 * Return the JSON representation of the referrers
443485 * @return The JSON string
@@ -674,14 +716,26 @@ public static OCILayout fromLayoutIndex(Path layoutPath) {
674716 }
675717
676718 /**
677- * Return the path to the OCI layout
719+ * Return the path to the OCI layout working directory.
720+ * <p>When the layout is tar-backed this is the temporary directory into which the tar
721+ * was extracted; use {@link #getTarPath()} to obtain the path of the backing tar file.</p>
678722 * @return The path to the OCI layout
679723 */
680724 @ JsonIgnore
681725 public Path getPath () {
682726 return path ;
683727 }
684728
729+ /**
730+ * Return the path to the backing tar file, or {@code null} if the layout is directory-backed.
731+ * @return The tar file path, or {@code null}
732+ */
733+ @ JsonIgnore
734+ @ Nullable
735+ public Path getTarPath () {
736+ return tarPath ;
737+ }
738+
685739 /**
686740 * Builder for the registry
687741 */
@@ -697,12 +751,30 @@ private Builder() {
697751 }
698752
699753 /**
700- * Return a new builder with default path
701- * @param path The path
754+ * Return a new builder with default path.
755+ * <p>If {@code path} ends with {@code .tar} the layout is considered to be tar-backed.
756+ * When the tar file already exists it is extracted to a temporary directory; that
757+ * temporary directory is used as the working {@code path} for all operations.
758+ * After every mutating operation the working directory is re-packed into the original
759+ * tar file. If the tar file does not yet exist a fresh, empty layout is created in
760+ * the temporary directory and packed once on the first mutation.</p>
761+ * @param path The path (directory or {@code .tar} file)
702762 * @return The builder
703763 */
704764 public OCILayout .Builder defaults (Path path ) {
705- layout .setPath (path );
765+ String name = path .getFileName ().toString ();
766+ if (name .endsWith (".tar" )) {
767+ // Tar-backed layout: work in a temp directory
768+ Path workDir = ArchiveUtils .createTempDir ();
769+ if (Files .exists (path )) {
770+ // Extract existing tar into the temp working directory
771+ ArchiveUtils .untar (path , workDir );
772+ }
773+ layout .setPath (workDir );
774+ layout .setTarPath (path );
775+ } else {
776+ layout .setPath (path );
777+ }
706778 return this ;
707779 }
708780
@@ -727,6 +799,7 @@ public OCILayout build() {
727799 }
728800 }
729801 layout .ensureMinimalLayout ();
802+ layout .packToTar ();
730803 return layout ;
731804 }
732805 }
0 commit comments