11import contextlib
2+ import io
3+ import pathlib
24import sys
5+ import tarfile
36from os import PathLike
47from socket import socket
58from types import TracebackType
1821from testcontainers .core .exceptions import ContainerConnectException , ContainerStartException
1922from testcontainers .core .labels import LABEL_SESSION_ID , SESSION_ID
2023from testcontainers .core .network import Network
24+ from testcontainers .core .transferable import Transferable , TransferSpec , build_transfer_tar
2125from testcontainers .core .utils import is_arm , setup_logger
2226from testcontainers .core .wait_strategies import LogMessageWaitStrategy
2327from testcontainers .core .waiting_utils import WaitStrategy
@@ -69,6 +73,7 @@ def __init__(
6973 network : Optional [Network ] = None ,
7074 network_aliases : Optional [list [str ]] = None ,
7175 _wait_strategy : Optional [WaitStrategy ] = None ,
76+ transferables : Optional [list [TransferSpec ]] = None ,
7277 ** kwargs : Any ,
7378 ) -> None :
7479 self .env = env or {}
@@ -82,6 +87,8 @@ def __init__(
8287 for vol in volumes :
8388 self .with_volume_mapping (* vol )
8489
90+ self .tmpfs : dict [str , str ] = {}
91+
8592 self .image = image
8693 self ._docker = DockerClient (** (docker_client_kw or {}))
8794 self ._container : Optional [Container ] = None
@@ -98,6 +105,11 @@ def __init__(
98105 self ._kwargs = kwargs
99106 self ._wait_strategy : Optional [WaitStrategy ] = _wait_strategy
100107
108+ self ._transferable_specs : list [TransferSpec ] = []
109+ if transferables :
110+ for t in transferables :
111+ self .with_copy_into_container (* t )
112+
101113 def with_env (self , key : str , value : str ) -> Self :
102114 self .env [key ] = value
103115 return self
@@ -198,13 +210,18 @@ def start(self) -> Self:
198210 ports = cast ("dict[int, Optional[int]]" , self .ports ),
199211 name = self ._name ,
200212 volumes = self .volumes ,
213+ tmpfs = self .tmpfs ,
201214 ** {** network_kwargs , ** self ._kwargs },
202215 )
203216
204217 if self ._wait_strategy is not None :
205218 self ._wait_strategy .wait_until_ready (self )
206219
207220 logger .info ("Container started: %s" , self ._container .short_id )
221+
222+ for t in self ._transferable_specs :
223+ self ._transfer_into_container (* t )
224+
208225 return self
209226
210227 def stop (self , force : bool = True , delete_volume : bool = True ) -> None :
@@ -270,6 +287,16 @@ def with_volume_mapping(self, host: Union[str, PathLike[str]], container: str, m
270287 self .volumes [str (host )] = mapping
271288 return self
272289
290+ def with_tmpfs_mount (self , container_path : str , size : Optional [str ] = None ) -> Self :
291+ """Mount a tmpfs volume on the container.
292+
293+ :param container_path: Container path to mount tmpfs on (e.g., '/data')
294+ :param size: Optional size limit (e.g., '256m', '1g'). If None, unbounded.
295+ :return: Self for chaining
296+ """
297+ self .tmpfs [container_path ] = size or ""
298+ return self
299+
273300 def get_wrapped_container (self ) -> "Container" :
274301 return self ._container
275302
@@ -305,6 +332,35 @@ def _configure(self) -> None:
305332 # placeholder if subclasses want to define this and use the default start method
306333 pass
307334
335+ def with_copy_into_container (
336+ self , transferable : Transferable , destination_in_container : str , mode : int = 0o644
337+ ) -> Self :
338+ self ._transferable_specs .append ((transferable , destination_in_container , mode ))
339+ return self
340+
341+ def copy_into_container (self , transferable : Transferable , destination_in_container : str , mode : int = 0o644 ) -> None :
342+ return self ._transfer_into_container (transferable , destination_in_container , mode )
343+
344+ def _transfer_into_container (self , transferable : Transferable , destination_in_container : str , mode : int ) -> None :
345+ if not self ._container :
346+ raise ContainerStartException ("Container must be started before transferring files" )
347+
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 } " )
351+
352+ def copy_from_container (self , source_in_container : str , destination_on_host : pathlib .Path ) -> None :
353+ if not self ._container :
354+ raise ContainerStartException ("Container must be started before copying files" )
355+
356+ tar_stream , _ = self ._container .get_archive (source_in_container )
357+
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 ())
363+
308364
309365class Reaper :
310366 """
0 commit comments