diff --git a/CHANGELOG.md b/CHANGELOG.md index 19bb758c45..1302b6fde2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Usage: * Fix #1690: Base images based on ubi9 * Fix #2390: support for all missing Chart.yaml fields * Fix #2444: Add support for Spring Boot application properties placeholders +* Fix #2456: Add utility class to decompress archive files ### 1.15.0 (2023-11-10) * Fix #2138: Support for Spring Boot Native Image diff --git a/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/archive/JKubeArchiveDecompressor.java b/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/archive/JKubeArchiveDecompressor.java new file mode 100644 index 0000000000..412375e318 --- /dev/null +++ b/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/archive/JKubeArchiveDecompressor.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.common.archive; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.eclipse.jkube.kit.common.util.FileUtil; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class JKubeArchiveDecompressor { + private JKubeArchiveDecompressor() { } + + /** + * Extracts a given archive file to specified target directory + * + * @param inputArchiveFile input archive file + * @param targetDirectory target folder where you want to extract + * @throws IOException in case of failure while trying to create any directory + */ + public static void extractArchive(File inputArchiveFile, File targetDirectory) throws IOException { + if (targetDirectory.exists()) { + FileUtil.cleanDirectory(targetDirectory); + } + Files.createDirectory(targetDirectory.toPath()); + if (inputArchiveFile.getName().endsWith(".tgz")) { + extractTarArchive(inputArchiveFile, targetDirectory.toPath()); + } else if (inputArchiveFile.getName().endsWith(".zip")) { + extractZipArchive(inputArchiveFile, targetDirectory.toPath()); + } + } + + private static void extractTarArchive(File downloadedArchive, Path targetExtractionDir) throws IOException { + try (BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(downloadedArchive.toPath())); + TarArchiveInputStream tar = new TarArchiveInputStream(new GzipCompressorInputStream(inputStream))) { + ArchiveEntry entry; + while ((entry = tar.getNextEntry()) != null) { + Path extractTo = targetExtractionDir.resolve(entry.getName()); + if (entry.isDirectory()) { + Files.createDirectories(extractTo); + } else { + Files.copy(tar, extractTo); + } + } + } + } + + private static void extractZipArchive(File downloadedArchive, Path targetExtractionDir) throws IOException { + byte[] buffer = new byte[1024]; + try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(downloadedArchive.toPath()))) { + ZipEntry zipEntry = zis.getNextEntry(); + while (zipEntry != null) { + File newFile = new File(targetExtractionDir.toFile(), zipEntry.getName()); + if (zipEntry.isDirectory()) { + if (!newFile.isDirectory() && !newFile.mkdirs()) { + throw new IOException("Failed to create directory " + newFile); + } + } else { + // fix for Windows-created archives + File parent = newFile.getParentFile(); + if (!parent.isDirectory() && !parent.mkdirs()) { + throw new IOException("Failed to create directory " + parent); + } + + try (FileOutputStream fos = new FileOutputStream(newFile)) { + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + } + zipEntry = zis.getNextEntry(); + } + zis.closeEntry(); + } + } +} diff --git a/jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/archive/JKubeArchiveDecompressorTest.java b/jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/archive/JKubeArchiveDecompressorTest.java new file mode 100644 index 0000000000..551d3b5a93 --- /dev/null +++ b/jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/archive/JKubeArchiveDecompressorTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.common.archive; + +import org.eclipse.jkube.kit.common.assertj.FileAssertions; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.io.File; +import java.io.IOException; + +class JKubeArchiveDecompressorTest { + @TempDir + private File temporaryFolder; + + @ParameterizedTest + @CsvSource({ + "/archives/pack-v0.31.0-linux.tgz,pack", + "/archives/pack-v0.31.0-windows.zip,pack.exe" + }) + void extractArchive_whenArchiveWithSingleFileProvided_thenExtractToSpecifiedDir(String filePath, String expectedFileInExtractedArchiveName) throws IOException { + // Given + File input = new File(getClass().getResource(filePath).getFile()); + + // When + JKubeArchiveDecompressor.extractArchive(input, temporaryFolder); + + // Then + FileAssertions.assertThat(temporaryFolder) + .exists() + .fileTree() + .containsExactlyInAnyOrder(expectedFileInExtractedArchiveName); + } + + @ParameterizedTest + @CsvSource({ + "/archives/nested-archive.tgz,nested,nested/folder,nested/folder/artifact", + "/archives/nested-archive.zip,nested,nested/folder,nested/folder/artifact.exe" + }) + void extractArchive_whenArchiveWithNestedDir_thenExtractToSpecifiedDir(String filePath, String parentDir, String artifactParentDir, String artifact) throws IOException { + // Given + File input = new File(getClass().getResource(filePath).getFile()); + + // When + JKubeArchiveDecompressor.extractArchive(input, temporaryFolder); + + // Then + FileAssertions.assertThat(temporaryFolder) + .exists() + .fileTree() + .containsExactlyInAnyOrder(parentDir, artifactParentDir, artifact); + } +} diff --git a/jkube-kit/common/src/test/resources/archives/nested-archive.tgz b/jkube-kit/common/src/test/resources/archives/nested-archive.tgz new file mode 100644 index 0000000000..60dbb7eef1 Binary files /dev/null and b/jkube-kit/common/src/test/resources/archives/nested-archive.tgz differ diff --git a/jkube-kit/common/src/test/resources/archives/nested-archive.zip b/jkube-kit/common/src/test/resources/archives/nested-archive.zip new file mode 100644 index 0000000000..ba902aca2d Binary files /dev/null and b/jkube-kit/common/src/test/resources/archives/nested-archive.zip differ diff --git a/jkube-kit/common/src/test/resources/archives/pack-v0.31.0-linux.tgz b/jkube-kit/common/src/test/resources/archives/pack-v0.31.0-linux.tgz new file mode 100644 index 0000000000..79ab02b785 Binary files /dev/null and b/jkube-kit/common/src/test/resources/archives/pack-v0.31.0-linux.tgz differ diff --git a/jkube-kit/common/src/test/resources/archives/pack-v0.31.0-windows.zip b/jkube-kit/common/src/test/resources/archives/pack-v0.31.0-windows.zip new file mode 100644 index 0000000000..06cc3ed7c5 Binary files /dev/null and b/jkube-kit/common/src/test/resources/archives/pack-v0.31.0-windows.zip differ