Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 104 additions & 4 deletions core/src/main/java/org/testcontainers/containers/ContainerState.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@
import com.github.dockerjava.api.model.PortBinding;
import com.github.dockerjava.api.model.Ports;
import com.google.common.base.Preconditions;
import java.nio.file.Path;
import java.nio.file.Paths;
import lombok.SneakyThrows;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -394,6 +398,77 @@ default void copyFileFromContainer(String containerPath, String destinationPath)
);
}


/**
* Copies a file or directory from a container to the host.
*
* @param containerPath the path within the container from which to copy
* @param hostPath the path on the host to which the contents will be copied
*/
default void copyPathFromContainer(String containerPath, String hostPath) {
if (getContainerId() == null) {
throw new IllegalStateException("copyPathFromContainer can only be used when the Container is created.");
}

copyArchiveFromContainer(containerPath, (inputStream) -> {
try(TarArchiveInputStream tarStream = new TarArchiveInputStream(inputStream)) {
// advance to first tar entry
TarArchiveEntry currentEntry = tarStream.getNextTarEntry();

// copy is file only (no directory copy)
boolean fileOnly = currentEntry.isFile();
// in case of coping a directory,
// we don't want to emit the root directory (would result in double nested directories)
String dirPrefixToRemove = currentEntry.isDirectory() ? currentEntry.getName() : "";

Path hostPathObj = Paths.get(hostPath);

while (currentEntry != null) {
File destFile;
if (fileOnly) {
// if we copy only a single file, we use the specified hostPath as destination
destFile = hostPathObj.toFile();
} else {
// if we copy a directory we have to resolve the path
destFile = hostPathObj.resolve(currentEntry.getName()
// remove the root directory of the copied TAR
.replaceFirst("^" + dirPrefixToRemove, "")
).toFile();
}

if (currentEntry.isFile()) {
// create parent directory if they do not exist yet
FileUtils.forceMkdirParent(destFile);
// copy file to destination
try (FileOutputStream output = new FileOutputStream(destFile)) {
IOUtils.copy(tarStream, output);
}
} else if (currentEntry.isDirectory()) {
if (destFile.exists() && !destFile.isDirectory()) {
// throw exception if directory would override already existing file
throw new IOException(
"copyPathFromContainer cannot create directory '" + destFile + "' as a file at this path already exists.");
}
// create a destination directory
FileUtils.forceMkdir(destFile);
} else {
// if we cannot handle the entry, we throw an exception
throw new UnsupportedOperationException(
"copyPathFromContainer can only copy files and directories. '" +
currentEntry.getName() +
"' is neither a file nor a directory.");
}

// jump to next tar entry
currentEntry = tarStream.getNextTarEntry();
}

return true;
}
});
}


/**
* Streams a file which resides inside the container
*
Expand All @@ -406,13 +481,38 @@ default <T> T copyFileFromContainer(String containerPath, ThrowingFunction<Input
throw new IllegalStateException("copyFileFromContainer can only be used when the Container is created.");
}

return copyArchiveFromContainer(containerPath, (stream) -> {
try(
TarArchiveInputStream tarStream = new TarArchiveInputStream(stream)
) {
tarStream.getNextTarEntry();
return function.apply(tarStream);
}
});
}

/**
* Copies an archive from the container at the specified path
* and processes it using the provided function.
* The path can be a file or directory and is automatically archived.
*
* @param containerPath the path inside the container to the file or directory to copy
* @param function a function that takes an {@link InputStream} of the copied
* archive and returns a result
* @return the result of applying the function to the InputStream
*/
@SneakyThrows
default <T> T copyArchiveFromContainer(String containerPath, ThrowingFunction<InputStream, T> function) {
if (getContainerId() == null) {
throw new IllegalStateException("copyArchiveFromContainer can only be used when the Container is created.");
}
DockerClient dockerClient = getDockerClient();
try (
InputStream inputStream = dockerClient.copyArchiveFromContainerCmd(getContainerId(), containerPath).exec();
TarArchiveInputStream tarInputStream = new TarArchiveInputStream(inputStream)
InputStream inputStream = dockerClient.copyArchiveFromContainerCmd(getContainerId(), containerPath).exec();
) {
tarInputStream.getNextTarEntry();
return function.apply(tarInputStream);
return function.apply(inputStream);
}
}


}
113 changes: 113 additions & 0 deletions core/src/test/java/org/testcontainers/junit/FileOperationsTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.testcontainers.junit;

import com.github.dockerjava.api.exception.NotFoundException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.CountingOutputStream;
Expand Down Expand Up @@ -185,4 +188,114 @@ public void shouldCopyFileFromExitedContainerTest() throws IOException {
);
}
}

@Test
public void copyPathFromContainer_singleFileTest() throws IOException {
try (
GenericContainer container = new GenericContainer(TestImages.ALPINE_IMAGE) //
.withCommand("top")
) {
container.start();
final MountableFile mountableFile = MountableFile.forClasspathResource("test_copy_to_container.txt");
container.copyFileToContainer(mountableFile, "/home/");

File actualFile = new File(temporaryFolder.getRoot().getAbsolutePath() + "/test_copy_from_container.txt");
container.copyPathFromContainer("/home/test_copy_to_container.txt", actualFile.getPath());

File expectedFile = new File(mountableFile.getResolvedPath());
assertThat(FileUtils.contentEquals(expectedFile, actualFile)).as("Files aren't same ").isTrue();
}
}

@Test
public void copyPathFromContainer_nestedDirectoryStructureWithoutFilesTest()
throws IOException, InterruptedException {
try (GenericContainer<?> container = new GenericContainer<>(TestImages.ALPINE_IMAGE)
.withCommand("sleep", "infinity")) {
container.start();

// create directory structure
container.execInContainer("mkdir", "-p", "/home/nested/directoryOne/inner");
container.execInContainer("mkdir", "-p", "/home/nested/directoryTwo/inner");

File hostPath = temporaryFolder.getRoot();
container.copyPathFromContainer("/home", hostPath.getPath());

// assert layer 1 structure
assertDirectoryContents(hostPath.toPath(), "nested");

// assert layer 2 structure (nested/)
assertDirectoryContents(Paths.get(hostPath.getPath(), "nested"), "directoryOne", "directoryTwo");

// assert layer 3 structure (nested/directoryOne/ and nested/directoryTwo/)
assertDirectoryContents(Paths.get(hostPath.getPath(), "nested", "directoryOne"), "inner");
assertDirectoryContents(Paths.get(hostPath.getPath(), "nested", "directoryTwo"), "inner");
}
}

@Test
public void copyPathFromContainer_filesInNestedDirectory()
throws IOException, InterruptedException {
try (GenericContainer<?> container = new GenericContainer<>(TestImages.ALPINE_IMAGE)
.withCommand("sleep", "infinity")) {
container.start();

// create directory and files
container.execInContainer("mkdir", "-p", "/home/test/nested");
container.execInContainer("sh","-c", "echo 'rootfile' > /home/test/root.txt ");
container.execInContainer("sh","-c", "echo 'nestedfile' > /home/test/nested/nested.txt");

Path actualRootPath = temporaryFolder.getRoot().toPath();
container.copyPathFromContainer("/home/test", actualRootPath.toString());

// assert correct file structure
assertDirectoryContents(actualRootPath, "nested", "root.txt");
assertDirectoryContents(actualRootPath.resolve("nested"), "nested.txt");

// assert correct file content
assertThat(actualRootPath.resolve("root.txt"))
.hasContent("rootfile");
assertThat(actualRootPath.resolve("nested/nested.txt"))
.hasContent("nestedfile");
}
}

@Test(expected = NotFoundException.class)
public void copyPathFromContainer_nonExistingDirectory_shouldFail()
throws IOException, InterruptedException {
try (GenericContainer<?> container = new GenericContainer<>(TestImages.ALPINE_IMAGE)
.withCommand("sleep", "infinity")) {
container.start();

Path actualRootPath = temporaryFolder.getRoot().toPath();
container.copyPathFromContainer("/home/test", actualRootPath.toString());
}
}

@Test
public void copyPathFromContainer_conflictingHostFile_shouldFail()
throws IOException, InterruptedException {
try (GenericContainer<?> container = new GenericContainer<>(TestImages.ALPINE_IMAGE)
.withCommand("sleep", "infinity")) {
container.start();

container.execInContainer("mkdir", "-p", "/home/test/conflict/dir");

Path actualRootPath = temporaryFolder.getRoot().toPath();
// create file `conflict` ... will conflict the copied directory `conflict/dir`
FileUtils.touch(actualRootPath.resolve("conflict").toFile());

assertThatThrownBy(() -> {
container.copyPathFromContainer("/home/test", actualRootPath.toString());
})
.isInstanceOf(IOException.class)
.hasMessageContaining("copyPathFromContainer cannot create directory")
.hasMessageContaining("as a file at this path already exists.");
}
}

private void assertDirectoryContents(Path directory, String... expectedContents) {
assertThat(directory.toFile().list()).containsExactlyInAnyOrder(expectedContents);
}

}