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
@@ -99,6 +106,11 @@ def __init__(
99106 self ._wait_strategy : Optional [WaitStrategy ] = _wait_strategy
100107 self ._cached_container_info : Optional [ContainerInspectInfo ] = None
101108
109+ self ._transferable_specs : list [TransferSpec ] = []
110+ if transferables :
111+ for t in transferables :
112+ self .with_copy_into_container (* t )
113+
102114 def with_env (self , key : str , value : str ) -> Self :
103115 self .env [key ] = value
104116 return self
@@ -199,13 +211,18 @@ def start(self) -> Self:
199211 ports = cast ("dict[int, Optional[int]]" , self .ports ),
200212 name = self ._name ,
201213 volumes = self .volumes ,
214+ tmpfs = self .tmpfs ,
202215 ** {** network_kwargs , ** self ._kwargs },
203216 )
204217
205218 if self ._wait_strategy is not None :
206219 self ._wait_strategy .wait_until_ready (self )
207220
208221 logger .info ("Container started: %s" , self ._container .short_id )
222+
223+ for t in self ._transferable_specs :
224+ self ._transfer_into_container (* t )
225+
209226 return self
210227
211228 def stop (self , force : bool = True , delete_volume : bool = True ) -> None :
@@ -271,6 +288,16 @@ def with_volume_mapping(self, host: Union[str, PathLike[str]], container: str, m
271288 self .volumes [str (host )] = mapping
272289 return self
273290
291+ def with_tmpfs_mount (self , container_path : str , size : Optional [str ] = None ) -> Self :
292+ """Mount a tmpfs volume on the container.
293+
294+ :param container_path: Container path to mount tmpfs on (e.g., '/data')
295+ :param size: Optional size limit (e.g., '256m', '1g'). If None, unbounded.
296+ :return: Self for chaining
297+ """
298+ self .tmpfs [container_path ] = size or ""
299+ return self
300+
274301 def get_wrapped_container (self ) -> "Container" :
275302 return self ._container
276303
@@ -327,6 +354,35 @@ def _configure(self) -> None:
327354 # placeholder if subclasses want to define this and use the default start method
328355 pass
329356
357+ def with_copy_into_container (
358+ self , transferable : Transferable , destination_in_container : str , mode : int = 0o644
359+ ) -> Self :
360+ self ._transferable_specs .append ((transferable , destination_in_container , mode ))
361+ return self
362+
363+ def copy_into_container (self , transferable : Transferable , destination_in_container : str , mode : int = 0o644 ) -> None :
364+ return self ._transfer_into_container (transferable , destination_in_container , mode )
365+
366+ def _transfer_into_container (self , transferable : Transferable , destination_in_container : str , mode : int ) -> None :
367+ if not self ._container :
368+ raise ContainerStartException ("Container must be started before transferring files" )
369+
370+ data = build_transfer_tar (transferable , destination_in_container , mode )
371+ if not self ._container .put_archive (path = "/" , data = data ):
372+ raise OSError (f"Failed to put archive into container at { destination_in_container } " )
373+
374+ def copy_from_container (self , source_in_container : str , destination_on_host : pathlib .Path ) -> None :
375+ if not self ._container :
376+ raise ContainerStartException ("Container must be started before copying files" )
377+
378+ tar_stream , _ = self ._container .get_archive (source_in_container )
379+
380+ with tarfile .open (fileobj = io .BytesIO (b"" .join (tar_stream ))) as tar :
381+ for member in tar .getmembers ():
382+ extracted = tar .extractfile (member )
383+ if extracted is not None :
384+ destination_on_host .write_bytes (extracted .read ())
385+
330386
331387class Reaper :
332388 """
0 commit comments