From 3c025cce77f5a169b8187a7e120cb203809c5ea0 Mon Sep 17 00:00:00 2001 From: guenhter Date: Tue, 17 Sep 2024 09:53:54 +0200 Subject: [PATCH] feat: support copying directories to container --- testcontainers/Cargo.toml | 1 + testcontainers/src/core/client.rs | 5 +- testcontainers/src/core/copy.rs | 132 ++++++++++++++++----------- testcontainers/tests/async_runner.rs | 33 ++++++- testcontainers/tests/sync_runner.rs | 32 ++++++- 5 files changed, 145 insertions(+), 58 deletions(-) diff --git a/testcontainers/Cargo.toml b/testcontainers/Cargo.toml index 0895290e..418f6c79 100644 --- a/testcontainers/Cargo.toml +++ b/testcontainers/Cargo.toml @@ -53,5 +53,6 @@ properties-config = ["serde-java-properties"] anyhow = "1.0.86" pretty_env_logger = "0.5" reqwest = { version = "0.12.4", features = ["blocking"], default-features = false } +temp-dir = "0.1.13" testimages.workspace = true tokio = { version = "1", features = ["macros"] } diff --git a/testcontainers/src/core/client.rs b/testcontainers/src/core/client.rs index b4f8c916..e39dcda8 100644 --- a/testcontainers/src/core/client.rs +++ b/testcontainers/src/core/client.rs @@ -293,12 +293,9 @@ impl Client { copy_to_container: &CopyToContainer, ) -> Result<(), ClientError> { let container_id: String = container_id.into(); - let target_directory = copy_to_container - .target_directory() - .map_err(ClientError::CopyToContaienrError)?; let options = UploadToContainerOptions { - path: target_directory, + path: "/".to_string(), no_overwrite_dir_non_dir: "false".into(), }; diff --git a/testcontainers/src/core/copy.rs b/testcontainers/src/core/copy.rs index cc6f67b5..3dc19ce8 100644 --- a/testcontainers/src/core/copy.rs +++ b/testcontainers/src/core/copy.rs @@ -1,6 +1,6 @@ use std::{ io, - path::{self, Path, PathBuf}, + path::{Path, PathBuf}, }; #[derive(Debug, Clone)] @@ -31,75 +31,103 @@ impl CopyToContainer { } } - pub(crate) fn target_directory(&self) -> Result { - path::Path::new(&self.target) - .parent() - .map(path::Path::display) - .map(|dir| dir.to_string()) - .ok_or_else(|| CopyToContaienrError::PathNameError(self.target.clone())) - } - pub(crate) async fn tar(&self) -> Result { self.source.tar(&self.target).await } } +impl From<&Path> for CopyDataSource { + fn from(value: &Path) -> Self { + CopyDataSource::File(value.to_path_buf()) + } +} +impl From for CopyDataSource { + fn from(value: PathBuf) -> Self { + CopyDataSource::File(value) + } +} +impl From> for CopyDataSource { + fn from(value: Vec) -> Self { + CopyDataSource::Data(value) + } +} + impl CopyDataSource { pub(crate) async fn tar( &self, target_path: impl Into, ) -> Result { let target_path: String = target_path.into(); - let mut ar = tokio_tar::Builder::new(Vec::new()); - - match self { - CopyDataSource::File(file_path) => { - let f = &mut tokio::fs::File::open(file_path) - .await - .map_err(CopyToContaienrError::IoError)?; - ar.append_file(&target_path, f) - .await - .map_err(CopyToContaienrError::IoError)?; - } - CopyDataSource::Data(data) => { - let path = path::Path::new(&target_path); - let file_name = match path.file_name() { - Some(v) => v, - None => return Err(CopyToContaienrError::PathNameError(target_path)), - }; - - let mut header = tokio_tar::Header::new_gnu(); - header.set_size(data.len() as u64); - header.set_mode(0o0644); - header.set_cksum(); - - ar.append_data(&mut header, file_name, data.as_slice()) - .await - .map_err(CopyToContaienrError::IoError)?; - } - } - let bytes = ar - .into_inner() - .await - .map_err(CopyToContaienrError::IoError)?; + let bytes = match self { + CopyDataSource::File(source_file_path) => { + tar_file(source_file_path, &target_path).await? + } + CopyDataSource::Data(data) => tar_bytes(data, &target_path).await?, + }; Ok(bytes::Bytes::copy_from_slice(bytes.as_slice())) } } -impl From<&Path> for CopyDataSource { - fn from(value: &Path) -> Self { - CopyDataSource::File(value.to_path_buf()) - } +async fn tar_file( + source_file_path: &Path, + target_path: &str, +) -> Result, CopyToContaienrError> { + let target_path = make_path_relative(&target_path); + let meta = tokio::fs::metadata(source_file_path) + .await + .map_err(CopyToContaienrError::IoError)?; + + let mut ar = tokio_tar::Builder::new(Vec::new()); + if meta.is_dir() { + ar.append_dir_all(target_path, source_file_path) + .await + .map_err(CopyToContaienrError::IoError)?; + } else { + let f = &mut tokio::fs::File::open(source_file_path) + .await + .map_err(CopyToContaienrError::IoError)?; + + ar.append_file(target_path, f) + .await + .map_err(CopyToContaienrError::IoError)?; + }; + + let res = ar + .into_inner() + .await + .map_err(CopyToContaienrError::IoError)?; + + Ok(res) } -impl From for CopyDataSource { - fn from(value: PathBuf) -> Self { - CopyDataSource::File(value) - } + +async fn tar_bytes(data: &Vec, target_path: &str) -> Result, CopyToContaienrError> { + let relative_target_path = make_path_relative(&target_path); + + let mut header = tokio_tar::Header::new_gnu(); + header.set_size(data.len() as u64); + header.set_mode(0o0644); + header.set_cksum(); + + let mut ar = tokio_tar::Builder::new(Vec::new()); + ar.append_data(&mut header, relative_target_path, data.as_slice()) + .await + .map_err(CopyToContaienrError::IoError)?; + + let res = ar + .into_inner() + .await + .map_err(CopyToContaienrError::IoError)?; + + Ok(res) } -impl From> for CopyDataSource { - fn from(value: Vec) -> Self { - CopyDataSource::Data(value) + +fn make_path_relative(path: &str) -> String { + // TODO support also absolute windows paths like "C:\temp\foo.txt" + if path.starts_with("/") { + path.trim_start_matches("/").to_string() + } else { + path.to_string() } } diff --git a/testcontainers/tests/async_runner.rs b/testcontainers/tests/async_runner.rs index 8033438a..62a2e45d 100644 --- a/testcontainers/tests/async_runner.rs +++ b/testcontainers/tests/async_runner.rs @@ -201,7 +201,7 @@ async fn async_run_with_log_consumer() -> anyhow::Result<()> { } #[tokio::test] -async fn async_copy_files_to_container() -> anyhow::Result<()> { +async fn async_copy_bytes_to_container() -> anyhow::Result<()> { let container = GenericImage::new("alpine", "latest") .with_wait_for(WaitFor::seconds(2)) .with_copy_to("/tmp/somefile", "foobar".to_string().into_bytes()) @@ -216,3 +216,34 @@ async fn async_copy_files_to_container() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn async_copy_files_to_container() -> anyhow::Result<()> { + let temp_dir = temp_dir::TempDir::new()?; + let f1 = temp_dir.child("foo.txt"); + + let sub_dir = temp_dir.child("subdir"); + std::fs::create_dir(&sub_dir)?; + let mut f2 = sub_dir.clone(); + f2.push("bar.txt"); + + std::fs::write(&f1, "foofoofoo")?; + std::fs::write(&f2, "barbarbar")?; + + let container = GenericImage::new("alpine", "latest") + .with_wait_for(WaitFor::seconds(2)) + .with_copy_to("/tmp/somefile", f1) + .with_copy_to("/", temp_dir.path()) + .with_cmd(vec!["cat", "/tmp/somefile", "&&", "cat", "/subdir/bar.txt"]) + .start() + .await?; + + let mut out = String::new(); + container.stdout(false).read_to_string(&mut out).await?; + + println!("{}", out); + assert!(out.contains("foofoofoo")); + assert!(out.contains("barbarbar")); + + Ok(()) +} diff --git a/testcontainers/tests/sync_runner.rs b/testcontainers/tests/sync_runner.rs index 709f6f90..caa98951 100644 --- a/testcontainers/tests/sync_runner.rs +++ b/testcontainers/tests/sync_runner.rs @@ -225,7 +225,7 @@ fn sync_run_with_log_consumer() -> anyhow::Result<()> { } #[test] -fn sync_copy_files_to_container() -> anyhow::Result<()> { +fn sync_copy_bytes_to_container() -> anyhow::Result<()> { let _ = pretty_env_logger::try_init(); let container = GenericImage::new("alpine", "latest") @@ -241,3 +241,33 @@ fn sync_copy_files_to_container() -> anyhow::Result<()> { Ok(()) } + +#[test] +fn sync_copy_files_to_container() -> anyhow::Result<()> { + let temp_dir = temp_dir::TempDir::new()?; + let f1 = temp_dir.child("foo.txt"); + + let sub_dir = temp_dir.child("subdir"); + std::fs::create_dir(&sub_dir)?; + let mut f2 = sub_dir.clone(); + f2.push("bar.txt"); + + std::fs::write(&f1, "foofoofoo")?; + std::fs::write(&f2, "barbarbar")?; + + let container = GenericImage::new("alpine", "latest") + .with_wait_for(WaitFor::seconds(2)) + .with_copy_to("/tmp/somefile", f1) + .with_copy_to("/", temp_dir.path()) + .with_cmd(vec!["cat", "/tmp/somefile", "&&", "cat", "/subdir/bar.txt"]) + .start()?; + + let mut out = String::new(); + container.stdout(false).read_to_string(&mut out)?; + + println!("{}", out); + assert!(out.contains("foofoofoo")); + assert!(out.contains("barbarbar")); + + Ok(()) +}