Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support copying directories to container #735

Merged
merged 1 commit into from
Sep 24, 2024
Merged
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
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(())
}
Loading