Skip to content

Commit ece5002

Browse files
authored
feat: support copying directories to container (#735)
1 parent 6c7019b commit ece5002

File tree

5 files changed

+145
-58
lines changed

5 files changed

+145
-58
lines changed

testcontainers/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,6 @@ properties-config = ["serde-java-properties"]
5353
anyhow = "1.0.86"
5454
pretty_env_logger = "0.5"
5555
reqwest = { version = "0.12.4", features = ["blocking"], default-features = false }
56+
temp-dir = "0.1.13"
5657
testimages.workspace = true
5758
tokio = { version = "1", features = ["macros"] }

testcontainers/src/core/client.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,12 +293,9 @@ impl Client {
293293
copy_to_container: &CopyToContainer,
294294
) -> Result<(), ClientError> {
295295
let container_id: String = container_id.into();
296-
let target_directory = copy_to_container
297-
.target_directory()
298-
.map_err(ClientError::CopyToContaienrError)?;
299296

300297
let options = UploadToContainerOptions {
301-
path: target_directory,
298+
path: "/".to_string(),
302299
no_overwrite_dir_non_dir: "false".into(),
303300
};
304301

testcontainers/src/core/copy.rs

Lines changed: 80 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{
22
io,
3-
path::{self, Path, PathBuf},
3+
path::{Path, PathBuf},
44
};
55

66
#[derive(Debug, Clone)]
@@ -31,75 +31,103 @@ impl CopyToContainer {
3131
}
3232
}
3333

34-
pub(crate) fn target_directory(&self) -> Result<String, CopyToContaienrError> {
35-
path::Path::new(&self.target)
36-
.parent()
37-
.map(path::Path::display)
38-
.map(|dir| dir.to_string())
39-
.ok_or_else(|| CopyToContaienrError::PathNameError(self.target.clone()))
40-
}
41-
4234
pub(crate) async fn tar(&self) -> Result<bytes::Bytes, CopyToContaienrError> {
4335
self.source.tar(&self.target).await
4436
}
4537
}
4638

39+
impl From<&Path> for CopyDataSource {
40+
fn from(value: &Path) -> Self {
41+
CopyDataSource::File(value.to_path_buf())
42+
}
43+
}
44+
impl From<PathBuf> for CopyDataSource {
45+
fn from(value: PathBuf) -> Self {
46+
CopyDataSource::File(value)
47+
}
48+
}
49+
impl From<Vec<u8>> for CopyDataSource {
50+
fn from(value: Vec<u8>) -> Self {
51+
CopyDataSource::Data(value)
52+
}
53+
}
54+
4755
impl CopyDataSource {
4856
pub(crate) async fn tar(
4957
&self,
5058
target_path: impl Into<String>,
5159
) -> Result<bytes::Bytes, CopyToContaienrError> {
5260
let target_path: String = target_path.into();
53-
let mut ar = tokio_tar::Builder::new(Vec::new());
54-
55-
match self {
56-
CopyDataSource::File(file_path) => {
57-
let f = &mut tokio::fs::File::open(file_path)
58-
.await
59-
.map_err(CopyToContaienrError::IoError)?;
60-
ar.append_file(&target_path, f)
61-
.await
62-
.map_err(CopyToContaienrError::IoError)?;
63-
}
64-
CopyDataSource::Data(data) => {
65-
let path = path::Path::new(&target_path);
66-
let file_name = match path.file_name() {
67-
Some(v) => v,
68-
None => return Err(CopyToContaienrError::PathNameError(target_path)),
69-
};
70-
71-
let mut header = tokio_tar::Header::new_gnu();
72-
header.set_size(data.len() as u64);
73-
header.set_mode(0o0644);
74-
header.set_cksum();
75-
76-
ar.append_data(&mut header, file_name, data.as_slice())
77-
.await
78-
.map_err(CopyToContaienrError::IoError)?;
79-
}
80-
}
8161

82-
let bytes = ar
83-
.into_inner()
84-
.await
85-
.map_err(CopyToContaienrError::IoError)?;
62+
let bytes = match self {
63+
CopyDataSource::File(source_file_path) => {
64+
tar_file(source_file_path, &target_path).await?
65+
}
66+
CopyDataSource::Data(data) => tar_bytes(data, &target_path).await?,
67+
};
8668

8769
Ok(bytes::Bytes::copy_from_slice(bytes.as_slice()))
8870
}
8971
}
9072

91-
impl From<&Path> for CopyDataSource {
92-
fn from(value: &Path) -> Self {
93-
CopyDataSource::File(value.to_path_buf())
94-
}
73+
async fn tar_file(
74+
source_file_path: &Path,
75+
target_path: &str,
76+
) -> Result<Vec<u8>, CopyToContaienrError> {
77+
let target_path = make_path_relative(&target_path);
78+
let meta = tokio::fs::metadata(source_file_path)
79+
.await
80+
.map_err(CopyToContaienrError::IoError)?;
81+
82+
let mut ar = tokio_tar::Builder::new(Vec::new());
83+
if meta.is_dir() {
84+
ar.append_dir_all(target_path, source_file_path)
85+
.await
86+
.map_err(CopyToContaienrError::IoError)?;
87+
} else {
88+
let f = &mut tokio::fs::File::open(source_file_path)
89+
.await
90+
.map_err(CopyToContaienrError::IoError)?;
91+
92+
ar.append_file(target_path, f)
93+
.await
94+
.map_err(CopyToContaienrError::IoError)?;
95+
};
96+
97+
let res = ar
98+
.into_inner()
99+
.await
100+
.map_err(CopyToContaienrError::IoError)?;
101+
102+
Ok(res)
95103
}
96-
impl From<PathBuf> for CopyDataSource {
97-
fn from(value: PathBuf) -> Self {
98-
CopyDataSource::File(value)
99-
}
104+
105+
async fn tar_bytes(data: &Vec<u8>, target_path: &str) -> Result<Vec<u8>, CopyToContaienrError> {
106+
let relative_target_path = make_path_relative(&target_path);
107+
108+
let mut header = tokio_tar::Header::new_gnu();
109+
header.set_size(data.len() as u64);
110+
header.set_mode(0o0644);
111+
header.set_cksum();
112+
113+
let mut ar = tokio_tar::Builder::new(Vec::new());
114+
ar.append_data(&mut header, relative_target_path, data.as_slice())
115+
.await
116+
.map_err(CopyToContaienrError::IoError)?;
117+
118+
let res = ar
119+
.into_inner()
120+
.await
121+
.map_err(CopyToContaienrError::IoError)?;
122+
123+
Ok(res)
100124
}
101-
impl From<Vec<u8>> for CopyDataSource {
102-
fn from(value: Vec<u8>) -> Self {
103-
CopyDataSource::Data(value)
125+
126+
fn make_path_relative(path: &str) -> String {
127+
// TODO support also absolute windows paths like "C:\temp\foo.txt"
128+
if path.starts_with("/") {
129+
path.trim_start_matches("/").to_string()
130+
} else {
131+
path.to_string()
104132
}
105133
}

testcontainers/tests/async_runner.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ async fn async_run_with_log_consumer() -> anyhow::Result<()> {
201201
}
202202

203203
#[tokio::test]
204-
async fn async_copy_files_to_container() -> anyhow::Result<()> {
204+
async fn async_copy_bytes_to_container() -> anyhow::Result<()> {
205205
let container = GenericImage::new("alpine", "latest")
206206
.with_wait_for(WaitFor::seconds(2))
207207
.with_copy_to("/tmp/somefile", "foobar".to_string().into_bytes())
@@ -216,3 +216,34 @@ async fn async_copy_files_to_container() -> anyhow::Result<()> {
216216

217217
Ok(())
218218
}
219+
220+
#[tokio::test]
221+
async fn async_copy_files_to_container() -> anyhow::Result<()> {
222+
let temp_dir = temp_dir::TempDir::new()?;
223+
let f1 = temp_dir.child("foo.txt");
224+
225+
let sub_dir = temp_dir.child("subdir");
226+
std::fs::create_dir(&sub_dir)?;
227+
let mut f2 = sub_dir.clone();
228+
f2.push("bar.txt");
229+
230+
std::fs::write(&f1, "foofoofoo")?;
231+
std::fs::write(&f2, "barbarbar")?;
232+
233+
let container = GenericImage::new("alpine", "latest")
234+
.with_wait_for(WaitFor::seconds(2))
235+
.with_copy_to("/tmp/somefile", f1)
236+
.with_copy_to("/", temp_dir.path())
237+
.with_cmd(vec!["cat", "/tmp/somefile", "&&", "cat", "/subdir/bar.txt"])
238+
.start()
239+
.await?;
240+
241+
let mut out = String::new();
242+
container.stdout(false).read_to_string(&mut out).await?;
243+
244+
println!("{}", out);
245+
assert!(out.contains("foofoofoo"));
246+
assert!(out.contains("barbarbar"));
247+
248+
Ok(())
249+
}

testcontainers/tests/sync_runner.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ fn sync_run_with_log_consumer() -> anyhow::Result<()> {
225225
}
226226

227227
#[test]
228-
fn sync_copy_files_to_container() -> anyhow::Result<()> {
228+
fn sync_copy_bytes_to_container() -> anyhow::Result<()> {
229229
let _ = pretty_env_logger::try_init();
230230

231231
let container = GenericImage::new("alpine", "latest")
@@ -241,3 +241,33 @@ fn sync_copy_files_to_container() -> anyhow::Result<()> {
241241

242242
Ok(())
243243
}
244+
245+
#[test]
246+
fn sync_copy_files_to_container() -> anyhow::Result<()> {
247+
let temp_dir = temp_dir::TempDir::new()?;
248+
let f1 = temp_dir.child("foo.txt");
249+
250+
let sub_dir = temp_dir.child("subdir");
251+
std::fs::create_dir(&sub_dir)?;
252+
let mut f2 = sub_dir.clone();
253+
f2.push("bar.txt");
254+
255+
std::fs::write(&f1, "foofoofoo")?;
256+
std::fs::write(&f2, "barbarbar")?;
257+
258+
let container = GenericImage::new("alpine", "latest")
259+
.with_wait_for(WaitFor::seconds(2))
260+
.with_copy_to("/tmp/somefile", f1)
261+
.with_copy_to("/", temp_dir.path())
262+
.with_cmd(vec!["cat", "/tmp/somefile", "&&", "cat", "/subdir/bar.txt"])
263+
.start()?;
264+
265+
let mut out = String::new();
266+
container.stdout(false).read_to_string(&mut out)?;
267+
268+
println!("{}", out);
269+
assert!(out.contains("foofoofoo"));
270+
assert!(out.contains("barbarbar"));
271+
272+
Ok(())
273+
}

0 commit comments

Comments
 (0)