Skip to content

Commit

Permalink
feat: Support copying directories to container
Browse files Browse the repository at this point in the history
  • Loading branch information
guenhter committed Sep 17, 2024
1 parent 3168f25 commit d6fe872
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 58 deletions.
1 change: 1 addition & 0 deletions testcontainers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
5 changes: 1 addition & 4 deletions testcontainers/src/core/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};

Expand Down
132 changes: 80 additions & 52 deletions testcontainers/src/core/copy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{
io,
path::{self, Path, PathBuf},
path::{Path, PathBuf},
};

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -31,75 +31,103 @@ impl CopyToContainer {
}
}

pub(crate) fn target_directory(&self) -> Result<String, CopyToContaienrError> {
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<bytes::Bytes, CopyToContaienrError> {
self.source.tar(&self.target).await
}
}

impl From<&Path> for CopyDataSource {
fn from(value: &Path) -> Self {
CopyDataSource::File(value.to_path_buf())
}
}
impl From<PathBuf> for CopyDataSource {
fn from(value: PathBuf) -> Self {
CopyDataSource::File(value)
}
}
impl From<Vec<u8>> for CopyDataSource {
fn from(value: Vec<u8>) -> Self {
CopyDataSource::Data(value)
}
}

impl CopyDataSource {
pub(crate) async fn tar(
&self,
target_path: impl Into<String>,
) -> Result<bytes::Bytes, CopyToContaienrError> {
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<Vec<u8>, 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<PathBuf> for CopyDataSource {
fn from(value: PathBuf) -> Self {
CopyDataSource::File(value)
}

async fn tar_bytes(data: &Vec<u8>, target_path: &str) -> Result<Vec<u8>, 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<Vec<u8>> for CopyDataSource {
fn from(value: Vec<u8>) -> 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()
}
}
33 changes: 32 additions & 1 deletion testcontainers/tests/async_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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(())
}
32 changes: 31 additions & 1 deletion testcontainers/tests/sync_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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(())
}

0 comments on commit d6fe872

Please sign in to comment.