diff --git a/src/main/java/com/github/dockerjava/api/DockerClient.java b/src/main/java/com/github/dockerjava/api/DockerClient.java index 2f16f6608..49eafcfca 100644 --- a/src/main/java/com/github/dockerjava/api/DockerClient.java +++ b/src/main/java/com/github/dockerjava/api/DockerClient.java @@ -12,6 +12,8 @@ import com.github.dockerjava.api.command.BuildImageCmd; import com.github.dockerjava.api.command.CommitCmd; import com.github.dockerjava.api.command.ContainerDiffCmd; +import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; +import com.github.dockerjava.api.command.CopyArchiveToContainerCmd; import com.github.dockerjava.api.command.CopyFileFromContainerCmd; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateImageCmd; @@ -46,6 +48,7 @@ import com.github.dockerjava.api.exception.DockerException; import com.github.dockerjava.api.model.AuthConfig; import com.github.dockerjava.api.model.Identifier; +import com.github.dockerjava.core.RemoteApiVersion; // https://godoc.org/github.com/fsouza/go-dockerclient public interface DockerClient extends Closeable { @@ -121,8 +124,37 @@ public interface DockerClient extends Closeable { public LogContainerCmd logContainerCmd(@Nonnull String containerId); + /** + * Copy resource from container to local machine. + * + * @param containerId id of the container + * @param resource path to container's resource + * @return created command + * @since {@link RemoteApiVersion#VERSION_1_20} + */ + public CopyArchiveFromContainerCmd copyArchiveFromContainerCmd(@Nonnull String containerId, @Nonnull String resource); + + /** + * Copy resource from container to local machine. + * + * @param containerId id of the container + * @param resource path to container's resource + * @return created command + * @see #copyArchiveFromContainerCmd(String, String) + * @deprecated since docker API version 1.20, replaced by {@link #copyArchiveFromContainerCmd(String, String)} + */ + @Deprecated public CopyFileFromContainerCmd copyFileFromContainerCmd(@Nonnull String containerId, @Nonnull String resource); + /** + * Copy archive from local machine to remote container + * + * @param containerId id of the container + * @return created command + * @since {@link RemoteApiVersion#VERSION_1_20} + */ + public CopyArchiveToContainerCmd copyArchiveToContainerCmd(@Nonnull String containerId); + public ContainerDiffCmd containerDiffCmd(@Nonnull String containerId); public StopContainerCmd stopContainerCmd(@Nonnull String containerId); diff --git a/src/main/java/com/github/dockerjava/api/command/CopyArchiveFromContainerCmd.java b/src/main/java/com/github/dockerjava/api/command/CopyArchiveFromContainerCmd.java new file mode 100644 index 000000000..56b24d24d --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/command/CopyArchiveFromContainerCmd.java @@ -0,0 +1,38 @@ +package com.github.dockerjava.api.command; + +import java.io.InputStream; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +import com.github.dockerjava.api.exception.NotFoundException; + +public interface CopyArchiveFromContainerCmd extends SyncDockerCmd { + + @CheckForNull + public String getContainerId(); + + @CheckForNull + public String getHostPath(); + + @CheckForNull + public String getResource(); + + public CopyArchiveFromContainerCmd withContainerId(@Nonnull String containerId); + + public CopyArchiveFromContainerCmd withHostPath(String hostPath); + + public CopyArchiveFromContainerCmd withResource(@Nonnull String resource); + + /** + * Its the responsibility of the caller to consume and/or close the {@link InputStream} to prevent connection leaks. + * + * @throws NotFoundException + * No such container + */ + @Override + public InputStream exec() throws NotFoundException; + + public static interface Exec extends DockerCmdSyncExec { + } +} diff --git a/src/main/java/com/github/dockerjava/api/command/CopyArchiveToContainerCmd.java b/src/main/java/com/github/dockerjava/api/command/CopyArchiveToContainerCmd.java new file mode 100644 index 000000000..129526cfe --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/command/CopyArchiveToContainerCmd.java @@ -0,0 +1,67 @@ +package com.github.dockerjava.api.command; + +import java.io.InputStream; + +import com.github.dockerjava.api.exception.NotFoundException; + +public interface CopyArchiveToContainerCmd extends SyncDockerCmd { + + String getContainerId(); + + String getHostResource(); + + InputStream getTarInputStream(); + + boolean isNoOverwriteDirNonDir(); + + boolean isDirChildrenOnly(); + + /** + * Set container's id + * + * @param containerId id of the container to copy file to + */ + CopyArchiveToContainerCmd withContainerId(String containerId); + + /** + * Set path to the resource on the host machine + * + * @param resource path to the resource on the host machine + */ + CopyArchiveToContainerCmd withHostResource(String resource); + + /** + * Set the tar input stream that will be uploaded to the container. withHostResource or withTarInputStream can be defined but not both. + * + * @param tarInputStream the stream to upload to the container + */ + CopyArchiveToContainerCmd withTarInputStream(InputStream tarInputStream); + + /** + * If set to true then it will be an error if unpacking the given content would cause an existing directory to be replaced with a non-directory and vice versa + * + * @param noOverwriteDirNonDir flag to know if non directory can be overwritten + */ + CopyArchiveToContainerCmd withNoOverwriteDirNonDir(boolean noOverwriteDirNonDir); + + /** + * If this flag is set to true, all children of the local directory will be copied to the remote without the root directory. + * For ex: if I have root/titi and root/tata and the remote path is /var/data. + * dirChildrenOnly = true will create /var/data/titi and /var/data/tata + * dirChildrenOnly = false will create /var/data/root/titi and /var/data/root/tata + * + * @param dirChildrenOnly if root directory is ignored + */ + CopyArchiveToContainerCmd withDirChildrenOnly(boolean dirChildrenOnly); + + String getRemotePath(); + + CopyArchiveToContainerCmd withRemotePath(String remotePath); + + @Override + Void exec() throws NotFoundException; + + interface Exec extends DockerCmdSyncExec { + } + +} diff --git a/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java b/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java index fc750a743..f0b35f58f 100644 --- a/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java +++ b/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java @@ -57,6 +57,10 @@ public interface DockerCmdExecFactory extends Closeable { public CopyFileFromContainerCmd.Exec createCopyFileFromContainerCmdExec(); + public CopyArchiveFromContainerCmd.Exec createCopyArchiveFromContainerCmdExec(); + + public CopyArchiveToContainerCmd.Exec createCopyArchiveToContainerCmdExec(); + public StopContainerCmd.Exec createStopContainerCmdExec(); public ContainerDiffCmd.Exec createContainerDiffCmdExec(); @@ -83,5 +87,4 @@ public interface DockerCmdExecFactory extends Closeable { @Override public void close() throws IOException; - } \ No newline at end of file diff --git a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java index 9546335b1..f05a7a744 100644 --- a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java +++ b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java @@ -1,11 +1,19 @@ package com.github.dockerjava.core; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.AttachContainerCmd; import com.github.dockerjava.api.command.AuthCmd; import com.github.dockerjava.api.command.BuildImageCmd; import com.github.dockerjava.api.command.CommitCmd; import com.github.dockerjava.api.command.ContainerDiffCmd; +import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; import com.github.dockerjava.api.command.CopyFileFromContainerCmd; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateImageCmd; @@ -45,6 +53,9 @@ import com.github.dockerjava.core.command.BuildImageCmdImpl; import com.github.dockerjava.core.command.CommitCmdImpl; import com.github.dockerjava.core.command.ContainerDiffCmdImpl; +import com.github.dockerjava.core.command.CopyArchiveFromContainerCmdImpl; +import com.github.dockerjava.api.command.CopyArchiveToContainerCmd; +import com.github.dockerjava.core.command.CopyArchiveToContainerCmdImpl; import com.github.dockerjava.core.command.CopyFileFromContainerCmdImpl; import com.github.dockerjava.core.command.CreateContainerCmdImpl; import com.github.dockerjava.core.command.CreateImageCmdImpl; @@ -77,16 +88,8 @@ import com.github.dockerjava.core.command.VersionCmdImpl; import com.github.dockerjava.core.command.WaitContainerCmdImpl; -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; - -import static com.google.common.base.Preconditions.checkNotNull; - /** * @author Konstantin Pelykh (kpelykh@gmail.com) - * * @see "https://github.com/docker/docker/blob/master/api/client/commands.go" */ public class DockerClientImpl implements Closeable, DockerClient { @@ -304,6 +307,18 @@ public CopyFileFromContainerCmd copyFileFromContainerCmd(String containerId, Str containerId, resource); } + @Override + public CopyArchiveFromContainerCmd copyArchiveFromContainerCmd(String containerId, String resource) { + return new CopyArchiveFromContainerCmdImpl(getDockerCmdExecFactory().createCopyArchiveFromContainerCmdExec(), + containerId, resource); + } + + @Override + public CopyArchiveToContainerCmd copyArchiveToContainerCmd(String containerId) { + return new CopyArchiveToContainerCmdImpl(getDockerCmdExecFactory().createCopyArchiveToContainerCmdExec(), + containerId); + } + @Override public ContainerDiffCmd containerDiffCmd(String containerId) { return new ContainerDiffCmdImpl(getDockerCmdExecFactory().createContainerDiffCmdExec(), containerId); diff --git a/src/main/java/com/github/dockerjava/core/command/CopyArchiveFromContainerCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/CopyArchiveFromContainerCmdImpl.java new file mode 100644 index 000000000..dd03fc435 --- /dev/null +++ b/src/main/java/com/github/dockerjava/core/command/CopyArchiveFromContainerCmdImpl.java @@ -0,0 +1,71 @@ +package com.github.dockerjava.core.command; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.InputStream; + +import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; +import com.github.dockerjava.api.exception.NotFoundException; + +/** + * Copy files or folders from a container. + */ +public class CopyArchiveFromContainerCmdImpl extends AbstrDockerCmd implements + CopyArchiveFromContainerCmd { + + private String containerId; + + private String hostPath = "."; + + private String resource; + + public CopyArchiveFromContainerCmdImpl(CopyArchiveFromContainerCmd.Exec exec, String containerId, String resource) { + super(exec); + withContainerId(containerId); + withResource(resource); + } + + @Override + public String getContainerId() { + return containerId; + } + + @Override + public String getResource() { + return resource; + } + + @Override + public CopyArchiveFromContainerCmdImpl withContainerId(String containerId) { + checkNotNull(containerId, "containerId was not specified"); + this.containerId = containerId; + return this; + } + + @Override + public CopyArchiveFromContainerCmdImpl withResource(String resource) { + checkNotNull(resource, "resource was not specified"); + this.resource = resource; + return this; + } + + @Override + public String getHostPath() { + return hostPath; + } + + @Override + public CopyArchiveFromContainerCmdImpl withHostPath(String hostPath) { + checkNotNull(hostPath, "hostPath was not specified"); + this.hostPath = hostPath; + return this; + } + + /** + * @throws NotFoundException No such container + */ + @Override + public InputStream exec() throws NotFoundException { + return super.exec(); + } +} diff --git a/src/main/java/com/github/dockerjava/core/command/CopyArchiveToContainerCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/CopyArchiveToContainerCmdImpl.java new file mode 100644 index 000000000..a40fc72d9 --- /dev/null +++ b/src/main/java/com/github/dockerjava/core/command/CopyArchiveToContainerCmdImpl.java @@ -0,0 +1,142 @@ +package com.github.dockerjava.core.command; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.builder.ToStringBuilder; + +import com.github.dockerjava.api.command.CopyArchiveToContainerCmd; +import com.github.dockerjava.api.exception.BadRequestException; +import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.core.util.CompressArchiveUtil; + +public class CopyArchiveToContainerCmdImpl extends AbstrDockerCmd implements CopyArchiveToContainerCmd { + + private String containerId; + + private String remotePath = "."; + + private InputStream tarInputStream; + + private String hostResource; + + private boolean noOverwriteDirNonDir = false; + + private boolean dirChildrenOnly = false; + + public CopyArchiveToContainerCmdImpl(CopyArchiveToContainerCmd.Exec exec, String containerId) { + super(exec); + withContainerId(containerId); + } + + @Override + public CopyArchiveToContainerCmd withContainerId(String containerId) { + checkNotNull(containerId, "containerId was not specified"); + this.containerId = containerId; + return this; + } + + @Override + public CopyArchiveToContainerCmd withHostResource(String hostResource) { + checkNotNull(hostResource, "hostResource was not specified"); + this.hostResource = hostResource; + return this; + } + + @Override + public CopyArchiveToContainerCmd withNoOverwriteDirNonDir(boolean noOverwriteDirNonDir) { + this.noOverwriteDirNonDir = noOverwriteDirNonDir; + return this; + } + + @Override + public CopyArchiveToContainerCmd withRemotePath(String remotePath) { + checkNotNull(remotePath, "remotePath was not specified"); + this.remotePath = remotePath; + return this; + } + + @Override + public CopyArchiveToContainerCmd withTarInputStream(InputStream tarInputStream) { + checkNotNull(tarInputStream, "tarInputStream was not specified"); + this.tarInputStream = tarInputStream; + return this; + } + + @Override + public CopyArchiveToContainerCmd withDirChildrenOnly(boolean dirChildrenOnly) { + this.dirChildrenOnly = dirChildrenOnly; + return this; + } + + @Override + public InputStream getTarInputStream() { + return tarInputStream; + } + + @Override + public String getContainerId() { + return this.containerId; + } + + @Override + public String getHostResource() { + return this.hostResource; + } + + @Override + public boolean isNoOverwriteDirNonDir() { + return this.noOverwriteDirNonDir; + } + + @Override + public String getRemotePath() { + return this.remotePath; + } + + @Override + public boolean isDirChildrenOnly() { + return this.dirChildrenOnly; + } + + @Override + public String toString() { + return new ToStringBuilder(this).append("cp ").append(hostResource).append(" ").append(containerId).append(":").append(remotePath).toString(); + } + + private InputStream buildUploadStream(String hostResource, boolean dirChildrenOnly) throws IOException { + Path toUpload = Files.createTempFile("docker-java", ".tar.gz"); + CompressArchiveUtil.tar(Paths.get(hostResource), toUpload, true, dirChildrenOnly); + return Files.newInputStream(toUpload); + } + + /** + * @throws com.github.dockerjava.api.exception.NotFoundException No such container + */ + @Override + public Void exec() throws NotFoundException { + if (StringUtils.isNotEmpty(this.hostResource)) { + // User set host resource and not directly a stream + if (this.tarInputStream != null) { + throw new BadRequestException("Only one of host resource or tar input stream should be defined to perform the copy, not both"); + } + // We compress the given path, call exec so that the stream is consumed and then close it our self + try (InputStream uploadStream = buildUploadStream(this.hostResource, this.dirChildrenOnly)) { + this.tarInputStream = uploadStream; + return super.exec(); + } catch (IOException e) { + throw new BadRequestException("Unable to perform tar on host resource " + this.hostResource); + } + } else if (this.tarInputStream == null) { + throw new BadRequestException("One of host resource or tar input stream must be defined to perform the copy"); + } + // User set a stream, so we will just consume it and let the user close it by him self + return super.exec(); + } +} diff --git a/src/main/java/com/github/dockerjava/core/util/CompressArchiveUtil.java b/src/main/java/com/github/dockerjava/core/util/CompressArchiveUtil.java index df141df7b..8506a66e3 100644 --- a/src/main/java/com/github/dockerjava/core/util/CompressArchiveUtil.java +++ b/src/main/java/com/github/dockerjava/core/util/CompressArchiveUtil.java @@ -2,21 +2,79 @@ import static com.github.dockerjava.core.util.FilePathUtil.relativize; +import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.zip.GZIPOutputStream; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; import org.apache.commons.io.FileUtils; +import com.google.common.io.ByteStreams; + public class CompressArchiveUtil { private CompressArchiveUtil() { // utility class } + static void putTarEntry(TarArchiveOutputStream tarOutputStream, TarArchiveEntry tarEntry, Path file) throws IOException { + tarEntry.setSize(Files.size(file)); + tarOutputStream.putArchiveEntry(tarEntry); + try (InputStream input = new BufferedInputStream(Files.newInputStream(file))) { + ByteStreams.copy(input, tarOutputStream); + tarOutputStream.closeArchiveEntry(); + } + } + + private static TarArchiveOutputStream buildTarStream(Path outputPath, boolean gZipped) throws IOException { + OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(outputPath)); + if (gZipped) { + outputStream = new GzipCompressorOutputStream(outputStream); + } + TarArchiveOutputStream tarArchiveOutputStream = new TarArchiveOutputStream(outputStream); + tarArchiveOutputStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU); + return tarArchiveOutputStream; + } + + /** + * Recursively tar file + * + * @param inputPath file path can be directory + * @param outputPath where to put the archived file + * @param childrenOnly if inputPath is directory and if childrenOnly is true, the archive will contain all of its children, else the archive contains unique + * entry which is the inputPath itself + * @param gZipped compress with gzip algorithm + */ + public static void tar(Path inputPath, Path outputPath, boolean gZipped, boolean childrenOnly) throws IOException { + if (!Files.exists(inputPath)) { + throw new FileNotFoundException("File not found " + inputPath); + } + FileUtils.touch(outputPath.toFile()); + + try (TarArchiveOutputStream tarArchiveOutputStream = buildTarStream(outputPath, gZipped)) { + if (!Files.isDirectory(inputPath)) { + putTarEntry(tarArchiveOutputStream, new TarArchiveEntry(inputPath.getFileName().toString()), inputPath); + } else { + Path sourcePath = inputPath; + if (!childrenOnly) { + // In order to have the dossier as the root entry + sourcePath = inputPath.getParent(); + } + Files.walkFileTree(inputPath, new TarDirWalker(sourcePath, tarArchiveOutputStream)); + } + tarArchiveOutputStream.flush(); + } + } + public static File archiveTARFiles(File base, Iterable files, String archiveNameWithOutExtension) throws IOException { File tarFile = new File(FileUtils.getTempDirectoryPath(), archiveNameWithOutExtension + ".tar"); diff --git a/src/main/java/com/github/dockerjava/core/util/FilePathUtil.java b/src/main/java/com/github/dockerjava/core/util/FilePathUtil.java index 9655bc502..d5e14ed0d 100644 --- a/src/main/java/com/github/dockerjava/core/util/FilePathUtil.java +++ b/src/main/java/com/github/dockerjava/core/util/FilePathUtil.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Path; import com.github.dockerjava.api.exception.DockerClientException; @@ -12,7 +13,7 @@ private FilePathUtil() { /** * Return the relative path. Path elements are separated with / char. - * + * * @param baseDir * a parent directory of {@code file} * @param file @@ -29,4 +30,23 @@ public static String relativize(File baseDir, File file) { throw new DockerClientException(e.getMessage(), e); } } + + /** + * Return the relative path. Path elements are separated with / char. + * + * @param baseDir + * a parent directory of {@code file} + * @param file + * the file to get the relative path + * @return the relative path + */ + public static String relativize(Path baseDir, Path file) { + String path = baseDir.toUri().relativize(file.toUri()).getPath(); + if (!"/".equals(baseDir.getFileSystem().getSeparator())) { + // For windows + return path.replace(baseDir.getFileSystem().getSeparator(), "/"); + } else { + return path; + } + } } diff --git a/src/main/java/com/github/dockerjava/core/util/TarDirWalker.java b/src/main/java/com/github/dockerjava/core/util/TarDirWalker.java new file mode 100644 index 000000000..927736af5 --- /dev/null +++ b/src/main/java/com/github/dockerjava/core/util/TarDirWalker.java @@ -0,0 +1,44 @@ +package com.github.dockerjava.core.util; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; + +import com.google.common.io.Closeables; + +public class TarDirWalker extends SimpleFileVisitor { + + private Path basePath; + + private TarArchiveOutputStream tarArchiveOutputStream; + + public TarDirWalker(Path basePath, TarArchiveOutputStream tarArchiveOutputStream) { + this.basePath = basePath; + this.tarArchiveOutputStream = tarArchiveOutputStream; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (!dir.equals(basePath)) { + tarArchiveOutputStream.putArchiveEntry(new TarArchiveEntry(FilePathUtil.relativize(basePath, dir))); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + CompressArchiveUtil.putTarEntry(tarArchiveOutputStream, new TarArchiveEntry(FilePathUtil.relativize(basePath, file)), file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + Closeables.close(tarArchiveOutputStream, true); + throw exc; + } +} diff --git a/src/main/java/com/github/dockerjava/jaxrs/CopyArchiveFromContainerCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/CopyArchiveFromContainerCmdExec.java new file mode 100644 index 000000000..c3fe175ee --- /dev/null +++ b/src/main/java/com/github/dockerjava/jaxrs/CopyArchiveFromContainerCmdExec.java @@ -0,0 +1,36 @@ +package com.github.dockerjava.jaxrs; + +import java.io.InputStream; + +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.jaxrs.util.WrappedResponseInputStream; + +public class CopyArchiveFromContainerCmdExec extends AbstrSyncDockerCmdExec + implements CopyArchiveFromContainerCmd.Exec { + + private static final Logger LOGGER = LoggerFactory.getLogger(CopyArchiveFromContainerCmdExec.class); + + public CopyArchiveFromContainerCmdExec(WebTarget baseResource, DockerClientConfig dockerClientConfig) { + super(baseResource, dockerClientConfig); + } + + @Override + protected InputStream execute(CopyArchiveFromContainerCmd command) { + WebTarget webResource = getBaseResource().path("/containers/{id}/archive").resolveTemplate("id", + command.getContainerId()); + + LOGGER.trace("Get: " + webResource.toString()); + + Response response = webResource.queryParam("path", command.getResource()).request().accept("application/x-tar").get(); + + return new WrappedResponseInputStream(response); + } + +} diff --git a/src/main/java/com/github/dockerjava/jaxrs/CopyArchiveToContainerCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/CopyArchiveToContainerCmdExec.java new file mode 100644 index 000000000..c77d50709 --- /dev/null +++ b/src/main/java/com/github/dockerjava/jaxrs/CopyArchiveToContainerCmdExec.java @@ -0,0 +1,39 @@ +package com.github.dockerjava.jaxrs; + +import static javax.ws.rs.client.Entity.entity; + +import java.io.InputStream; + +import javax.ws.rs.client.WebTarget; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.command.CopyArchiveToContainerCmd; +import com.github.dockerjava.core.DockerClientConfig; + +public class CopyArchiveToContainerCmdExec extends AbstrSyncDockerCmdExec implements CopyArchiveToContainerCmd.Exec { + + private static final Logger LOGGER = LoggerFactory.getLogger(CopyArchiveFromContainerCmdExec.class); + + public CopyArchiveToContainerCmdExec(WebTarget baseResource, DockerClientConfig dockerClientConfig) { + super(baseResource, dockerClientConfig); + } + + @Override + protected Void execute(CopyArchiveToContainerCmd command) { + WebTarget webResource = getBaseResource().path("/containers/{id}/archive").resolveTemplate("id", + command.getContainerId()); + + LOGGER.trace("PUT: " + webResource.toString()); + InputStream streamToUpload = command.getTarInputStream(); + webResource + .queryParam("path", command.getRemotePath()) + .queryParam("noOverwriteDirNonDir", command.isNoOverwriteDirNonDir()) + .request() + .put(entity(streamToUpload, "application/x-tar")) + .close(); + return null; + + } +} diff --git a/src/main/java/com/github/dockerjava/jaxrs/CopyFileFromContainerCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/CopyFileFromContainerCmdExec.java index 15185a8d8..10a719cbc 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/CopyFileFromContainerCmdExec.java +++ b/src/main/java/com/github/dockerjava/jaxrs/CopyFileFromContainerCmdExec.java @@ -35,4 +35,4 @@ protected InputStream execute(CopyFileFromContainerCmd command) { return new WrappedResponseInputStream(response); } -} +} \ No newline at end of file diff --git a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java index e3d57442a..6e2591f86 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java +++ b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java @@ -1,12 +1,37 @@ package com.github.dockerjava.jaxrs; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.IOException; +import java.net.URI; + +import javax.net.ssl.SSLContext; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.client.WebTarget; + +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.apache.connector.ApacheClientProperties; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; -import com.github.dockerjava.api.exception.DockerClientException; import com.github.dockerjava.api.command.AttachContainerCmd; import com.github.dockerjava.api.command.AuthCmd; import com.github.dockerjava.api.command.BuildImageCmd; import com.github.dockerjava.api.command.CommitCmd; import com.github.dockerjava.api.command.ContainerDiffCmd; +import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; +import com.github.dockerjava.api.command.CopyArchiveToContainerCmd; import com.github.dockerjava.api.command.CopyFileFromContainerCmd; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateImageCmd; @@ -39,33 +64,12 @@ import com.github.dockerjava.api.command.UnpauseContainerCmd; import com.github.dockerjava.api.command.VersionCmd; import com.github.dockerjava.api.command.WaitContainerCmd; +import com.github.dockerjava.api.exception.DockerClientException; import com.github.dockerjava.core.DockerClientConfig; import com.github.dockerjava.jaxrs.connector.ApacheConnectorProvider; import com.github.dockerjava.jaxrs.filter.JsonClientFilter; import com.github.dockerjava.jaxrs.filter.ResponseStatusExceptionFilter; import com.github.dockerjava.jaxrs.filter.SelectiveLoggingFilter; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.glassfish.jersey.CommonProperties; -import org.glassfish.jersey.apache.connector.ApacheClientProperties; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.net.ssl.SSLContext; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.ClientRequestFilter; -import javax.ws.rs.client.ClientResponseFilter; -import javax.ws.rs.client.WebTarget; -import java.io.IOException; -import java.net.URI; - -import static com.google.common.base.Preconditions.checkNotNull; //import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; // see https://github.com/docker-java/docker-java/issues/196 @@ -309,11 +313,21 @@ public LogContainerCmd.Exec createLogContainerCmdExec() { return new LogContainerCmdExec(getBaseResource(), getDockerClientConfig()); } + @Override + public CopyArchiveFromContainerCmd.Exec createCopyArchiveFromContainerCmdExec() { + return new CopyArchiveFromContainerCmdExec(getBaseResource(), getDockerClientConfig()); + } + @Override public CopyFileFromContainerCmd.Exec createCopyFileFromContainerCmdExec() { return new CopyFileFromContainerCmdExec(getBaseResource(), getDockerClientConfig()); } + @Override + public CopyArchiveToContainerCmd.Exec createCopyArchiveToContainerCmdExec() { + return new CopyArchiveToContainerCmdExec(getBaseResource(), getDockerClientConfig()); + } + @Override public StopContainerCmd.Exec createStopContainerCmdExec() { return new StopContainerCmdExec(getBaseResource(), getDockerClientConfig()); diff --git a/src/test/java/com/github/dockerjava/client/AbstractDockerClientTest.java b/src/test/java/com/github/dockerjava/client/AbstractDockerClientTest.java index 2fd228b9f..8f8cf0502 100644 --- a/src/test/java/com/github/dockerjava/client/AbstractDockerClientTest.java +++ b/src/test/java/com/github/dockerjava/client/AbstractDockerClientTest.java @@ -38,7 +38,7 @@ public abstract class AbstractDockerClientTest extends Assert { public static final Logger LOG = LoggerFactory.getLogger(AbstractDockerClientTest.class); - private String apiVersion = "1.19"; + private String apiVersion = "1.20"; protected DockerClient dockerClient; diff --git a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java index fdc582083..d1ff920b0 100644 --- a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java +++ b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java @@ -11,6 +11,8 @@ import com.github.dockerjava.api.command.BuildImageCmd; import com.github.dockerjava.api.command.CommitCmd; import com.github.dockerjava.api.command.ContainerDiffCmd; +import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; +import com.github.dockerjava.api.command.CopyArchiveToContainerCmd; import com.github.dockerjava.api.command.CopyFileFromContainerCmd; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateContainerResponse; @@ -52,7 +54,6 @@ * for the purpose of automatically cleanup. * * @author marcus - * */ public class TestDockerCmdExecFactory implements DockerCmdExecFactory { @@ -242,6 +243,16 @@ public CopyFileFromContainerCmd.Exec createCopyFileFromContainerCmdExec() { return delegate.createCopyFileFromContainerCmdExec(); } + @Override + public CopyArchiveFromContainerCmd.Exec createCopyArchiveFromContainerCmdExec() { + return delegate.createCopyArchiveFromContainerCmdExec(); + } + + @Override + public CopyArchiveToContainerCmd.Exec createCopyArchiveToContainerCmdExec() { + return delegate.createCopyArchiveToContainerCmdExec(); + } + @Override public StopContainerCmd.Exec createStopContainerCmdExec() { return delegate.createStopContainerCmdExec(); diff --git a/src/test/java/com/github/dockerjava/core/command/CopyArchiveFromContainerCmdImplTest.java b/src/test/java/com/github/dockerjava/core/command/CopyArchiveFromContainerCmdImplTest.java new file mode 100644 index 000000000..0f9da6058 --- /dev/null +++ b/src/test/java/com/github/dockerjava/core/command/CopyArchiveFromContainerCmdImplTest.java @@ -0,0 +1,73 @@ +package com.github.dockerjava.core.command; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; + +import java.io.InputStream; +import java.lang.reflect.Method; + +import org.testng.ITestResult; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.client.AbstractDockerClientTest; + +@Test(groups = "integration") +public class CopyArchiveFromContainerCmdImplTest extends AbstractDockerClientTest { + + @BeforeTest + public void beforeTest() throws Exception { + super.beforeTest(); + } + + @AfterTest + public void afterTest() { + super.afterTest(); + } + + @BeforeMethod + public void beforeMethod(Method method) { + super.beforeMethod(method); + } + + @AfterMethod + public void afterMethod(ITestResult result) { + super.afterMethod(result); + } + + @Test + public void copyFromContainer() throws Exception { + // TODO extract this into a shared method + CreateContainerResponse container = dockerClient.createContainerCmd("busybox") + .withName("docker-java-itest-copyFromContainer").withCmd("touch", "/copyFromContainer").exec(); + + LOG.info("Created container: {}", container); + assertThat(container.getId(), not(isEmptyOrNullString())); + + dockerClient.startContainerCmd(container.getId()).exec(); + + InputStream response = dockerClient.copyArchiveFromContainerCmd(container.getId(), "/copyFromContainer").exec(); + Boolean bytesAvailable = response.available() > 0; + assertTrue(bytesAvailable, "The file was not copied from the container."); + + // read the stream fully. Otherwise, the underlying stream will not be closed. + String responseAsString = asString(response); + assertNotNull(responseAsString); + assertTrue(responseAsString.length() > 0); + } + + @Test + public void copyFromNonExistingContainer() throws Exception { + try { + dockerClient.copyArchiveFromContainerCmd("non-existing", "/test").exec(); + fail("expected NotFoundException"); + } catch (NotFoundException ignored) { + } + } +} diff --git a/src/test/java/com/github/dockerjava/core/command/CopyArchiveToContainerCmdImplTest.java b/src/test/java/com/github/dockerjava/core/command/CopyArchiveToContainerCmdImplTest.java new file mode 100644 index 000000000..3e5cb6702 --- /dev/null +++ b/src/test/java/com/github/dockerjava/core/command/CopyArchiveToContainerCmdImplTest.java @@ -0,0 +1,90 @@ +package com.github.dockerjava.core.command; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.testng.ITestResult; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.client.AbstractDockerClientTest; +import com.github.dockerjava.core.util.CompressArchiveUtil; + +public class CopyArchiveToContainerCmdImplTest extends AbstractDockerClientTest { + @BeforeTest + public void beforeTest() throws Exception { + super.beforeTest(); + } + + @AfterTest + public void afterTest() { + super.afterTest(); + } + + @BeforeMethod + public void beforeMethod(Method method) { + super.beforeMethod(method); + } + + @AfterMethod + public void afterMethod(ITestResult result) { + super.afterMethod(result); + } + + @Test + public void copyFileToContainer() throws Exception { + CreateContainerResponse container = prepareContainerForCopy(); + Path temp = Files.createTempFile("", ".tar.gz"); + CompressArchiveUtil.tar(Paths.get("src/test/resources/testReadFile"), temp, true, false); + try (InputStream uploadStream = Files.newInputStream(temp)) { + dockerClient.copyArchiveToContainerCmd(container.getId()).withTarInputStream(uploadStream).exec(); + assertFileCopied(container); + } + } + + @Test + public void copyStreamToContainer() throws Exception { + CreateContainerResponse container = prepareContainerForCopy(); + dockerClient.copyArchiveToContainerCmd(container.getId()).withHostResource("src/test/resources/testReadFile").exec(); + assertFileCopied(container); + } + + private CreateContainerResponse prepareContainerForCopy() { + CreateContainerResponse container = dockerClient.createContainerCmd("busybox").withName("docker-java-itest-copyToContainer").exec(); + LOG.info("Created container: {}", container); + assertThat(container.getId(), not(isEmptyOrNullString())); + dockerClient.startContainerCmd(container.getId()).exec(); + // Copy a folder to the container + return container; + } + + private void assertFileCopied(CreateContainerResponse container) throws IOException { + try (InputStream response = dockerClient.copyArchiveFromContainerCmd(container.getId(), "testReadFile").exec()) { + boolean bytesAvailable = response.available() > 0; + assertTrue(bytesAvailable, "The file was not copied to the container."); + } + } + + @Test + public void copyToNonExistingContainer() throws Exception { + try { + dockerClient.copyArchiveToContainerCmd("non-existing").withHostResource("src/test/resources/testReadFile").exec(); + fail("expected NotFoundException"); + } catch (NotFoundException ignored) { + } + } + +} diff --git a/src/test/java/com/github/dockerjava/core/command/ExecStartCmdImplTest.java b/src/test/java/com/github/dockerjava/core/command/ExecStartCmdImplTest.java index 43aa49f0e..5144f2d4f 100644 --- a/src/test/java/com/github/dockerjava/core/command/ExecStartCmdImplTest.java +++ b/src/test/java/com/github/dockerjava/core/command/ExecStartCmdImplTest.java @@ -56,7 +56,7 @@ public void execStart() throws Exception { .withAttachStdout(true).withCmd("touch", "/execStartTest.log").exec(); dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(); - InputStream response = dockerClient.copyFileFromContainerCmd(container.getId(), "/execStartTest.log").exec(); + InputStream response = dockerClient.copyArchiveFromContainerCmd(container.getId(), "/execStartTest.log").exec(); Boolean bytesAvailable = response.available() > 0; assertTrue(bytesAvailable, "The file was not copied from the container."); @@ -81,7 +81,7 @@ public void execStartAttached() throws Exception { .withAttachStdout(true).withCmd("touch", "/execStartTest.log").exec(); dockerClient.execStartCmd(execCreateCmdResponse.getId()).withDetach(false).withTty(true).exec(); - InputStream response = dockerClient.copyFileFromContainerCmd(container.getId(), "/execStartTest.log").exec(); + InputStream response = dockerClient.copyArchiveFromContainerCmd(container.getId(), "/execStartTest.log").exec(); Boolean bytesAvailable = response.available() > 0; assertTrue(bytesAvailable, "The file was not copied from the container.");