|
21 | 21 | from testcontainers.core.exceptions import ContainerConnectException, ContainerStartException |
22 | 22 | from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID |
23 | 23 | from testcontainers.core.network import Network |
24 | | -from testcontainers.core.transferable import Transferable, TransferSpec |
| 24 | +from testcontainers.core.transferable import Transferable, TransferSpec, build_transfer_tar |
25 | 25 | from testcontainers.core.utils import is_arm, setup_logger |
26 | 26 | from testcontainers.core.wait_strategies import LogMessageWaitStrategy |
27 | 27 | from testcontainers.core.waiting_utils import WaitStrategy |
@@ -289,7 +289,7 @@ def with_volume_mapping(self, host: Union[str, PathLike[str]], container: str, m |
289 | 289 |
|
290 | 290 | def with_tmpfs_mount(self, container_path: str, size: Optional[str] = None) -> Self: |
291 | 291 | """Mount a tmpfs volume on the container. |
292 | | - |
| 292 | +
|
293 | 293 | :param container_path: Container path to mount tmpfs on (e.g., '/data') |
294 | 294 | :param size: Optional size limit (e.g., '256m', '1g'). If None, unbounded. |
295 | 295 | :return: Self for chaining |
@@ -342,57 +342,24 @@ def copy_into_container(self, transferable: Transferable, destination_in_contain |
342 | 342 | return self._transfer_into_container(transferable, destination_in_container, mode) |
343 | 343 |
|
344 | 344 | def _transfer_into_container(self, transferable: Transferable, destination_in_container: str, mode: int) -> None: |
345 | | - if isinstance(transferable, bytes): |
346 | | - self._transfer_file_content_into_container(transferable, destination_in_container, mode) |
347 | | - elif isinstance(transferable, pathlib.Path): |
348 | | - if transferable.is_file(): |
349 | | - self._transfer_file_content_into_container(transferable.read_bytes(), destination_in_container, mode) |
350 | | - elif transferable.is_dir(): |
351 | | - self._transfer_directory_into_container(transferable, destination_in_container, mode) |
352 | | - else: |
353 | | - raise TypeError(f"Path {transferable} is neither a file nor directory") |
354 | | - else: |
355 | | - raise TypeError("source must be bytes or PathLike") |
356 | | - |
357 | | - def _transfer_file_content_into_container( |
358 | | - self, file_content: bytes, destination_in_container: str, mode: int |
359 | | - ) -> None: |
360 | | - fileobj = io.BytesIO() |
361 | | - with tarfile.open(fileobj=fileobj, mode="w") as tar: |
362 | | - tarinfo = tarfile.TarInfo(name=destination_in_container) |
363 | | - tarinfo.size = len(file_content) |
364 | | - tarinfo.mode = mode |
365 | | - tar.addfile(tarinfo, io.BytesIO(file_content)) |
366 | | - fileobj.seek(0) |
367 | | - assert self._container is not None |
368 | | - rv = self._container.put_archive(path="/", data=fileobj.getvalue()) |
369 | | - assert rv is True |
370 | | - |
371 | | - def _transfer_directory_into_container( |
372 | | - self, source_directory: pathlib.Path, destination_in_container: str, mode: int |
373 | | - ) -> None: |
374 | | - assert self._container is not None |
375 | | - result = self._container.exec_run(["mkdir", "-p", destination_in_container]) |
376 | | - assert result.exit_code == 0 |
| 345 | + if not self._container: |
| 346 | + raise ContainerStartException("Container must be started before transferring files") |
377 | 347 |
|
378 | | - fileobj = io.BytesIO() |
379 | | - with tarfile.open(fileobj=fileobj, mode="w") as tar: |
380 | | - tar.add(source_directory, arcname=source_directory.name) |
381 | | - fileobj.seek(0) |
382 | | - rv = self._container.put_archive(path=destination_in_container, data=fileobj.getvalue()) |
383 | | - assert rv is True |
| 348 | + data = build_transfer_tar(transferable, destination_in_container, mode) |
| 349 | + if not self._container.put_archive(path="/", data=data): |
| 350 | + raise OSError(f"Failed to put archive into container at {destination_in_container}") |
384 | 351 |
|
385 | 352 | def copy_from_container(self, source_in_container: str, destination_on_host: pathlib.Path) -> None: |
386 | | - assert self._container is not None |
| 353 | + if not self._container: |
| 354 | + raise ContainerStartException("Container must be started before copying files") |
| 355 | + |
387 | 356 | tar_stream, _ = self._container.get_archive(source_in_container) |
388 | 357 |
|
389 | | - for chunk in tar_stream: |
390 | | - with tarfile.open(fileobj=io.BytesIO(chunk)) as tar: |
391 | | - for member in tar.getmembers(): |
392 | | - with open(destination_on_host, "wb") as f: |
393 | | - fileobj = tar.extractfile(member) |
394 | | - assert fileobj is not None |
395 | | - f.write(fileobj.read()) |
| 358 | + with tarfile.open(fileobj=io.BytesIO(b"".join(tar_stream))) as tar: |
| 359 | + for member in tar.getmembers(): |
| 360 | + extracted = tar.extractfile(member) |
| 361 | + if extracted is not None: |
| 362 | + destination_on_host.write_bytes(extracted.read()) |
396 | 363 |
|
397 | 364 |
|
398 | 365 | class Reaper: |
|
0 commit comments