@@ -10,6 +10,7 @@ import (
1010 "io"
1111 "os"
1212 "os/exec"
13+ "path"
1314 "path/filepath"
1415 "regexp"
1516 "strings"
@@ -186,6 +187,47 @@ func (dc *DockerClient) CopyFile(containerName, sourceFile, targetPath string) e
186187 return dc .copyToContainer (containerID , sourceFile , targetPath )
187188}
188189
190+ // CopyFromContainer copies files from a container path and returns a tar archive stream.
191+ func (dc * DockerClient ) CopyFromContainer (containerName , sourcePath string ) (io.ReadCloser , container.PathStat , error ) {
192+ return dc .CopyFromContainerWithContext (context .Background (), containerName , sourcePath )
193+ }
194+
195+ // CopyFromContainerWithContext copies files from a container path and returns a tar archive stream.
196+ func (dc * DockerClient ) CopyFromContainerWithContext (ctx context.Context , containerName , sourcePath string ) (io.ReadCloser , container.PathStat , error ) {
197+ containerID , err := dc .findContainerIDByName (ctx , containerName )
198+ if err != nil {
199+ return nil , container.PathStat {}, fmt .Errorf ("failed to find container ID by name: %s" , containerName )
200+ }
201+ reader , stat , err := dc .cli .CopyFromContainer (ctx , containerID , sourcePath )
202+ if err != nil {
203+ return nil , container.PathStat {}, fmt .Errorf ("could not copy from container %s path %s: %w" , containerName , sourcePath , err )
204+ }
205+ return reader , stat , nil
206+ }
207+
208+ // CopyFromContainerToHost copies files from a container path and extracts them into hostDir.
209+ // If sourcePath points to a directory, only its contents are placed inside hostDir.
210+ func (dc * DockerClient ) CopyFromContainerToHost (containerName , sourcePath , hostDir string ) error {
211+ return dc .CopyFromContainerToHostWithContext (context .Background (), containerName , sourcePath , hostDir )
212+ }
213+
214+ // CopyFromContainerToHostWithContext copies files from a container path and extracts them into hostDir.
215+ // If sourcePath points to a directory, only its contents are placed inside hostDir.
216+ func (dc * DockerClient ) CopyFromContainerToHostWithContext (ctx context.Context , containerName , sourcePath , hostDir string ) error {
217+ reader , _ , err := dc .CopyFromContainerWithContext (ctx , containerName , sourcePath )
218+ if err != nil {
219+ return err
220+ }
221+ defer reader .Close ()
222+
223+ if err := os .MkdirAll (hostDir , 0o755 ); err != nil {
224+ return fmt .Errorf ("failed to create host destination directory %s: %w" , hostDir , err )
225+ }
226+
227+ stripTopDir := path .Base (path .Clean (sourcePath ))
228+ return extractTarArchiveToHostDir (reader , hostDir , stripTopDir )
229+ }
230+
189231// findContainerIDByName finds a container ID by its name
190232func (dc * DockerClient ) findContainerIDByName (ctx context.Context , containerName string ) (string , error ) {
191233 containers , err := dc .cli .ContainerList (ctx , container.ListOptions {
@@ -245,6 +287,89 @@ func (dc *DockerClient) copyToContainer(containerID, sourceFile, targetPath stri
245287 return nil
246288}
247289
290+ func extractTarArchiveToHostDir (reader io.Reader , hostDir , stripTopDir string ) error {
291+ tarReader := tar .NewReader (reader )
292+ for {
293+ header , err := tarReader .Next ()
294+ if err == io .EOF {
295+ return nil
296+ }
297+ if err != nil {
298+ return fmt .Errorf ("failed reading tar stream: %w" , err )
299+ }
300+
301+ relativePath , ok := normalizeTarEntryPath (header .Name , stripTopDir )
302+ if ! ok {
303+ continue
304+ }
305+ targetPath := filepath .Join (hostDir , filepath .FromSlash (relativePath ))
306+ if err := ensureSubpath (hostDir , targetPath ); err != nil {
307+ return err
308+ }
309+
310+ switch header .Typeflag {
311+ case tar .TypeDir :
312+ if err := os .MkdirAll (targetPath , 0o755 ); err != nil {
313+ return fmt .Errorf ("failed to create dir %s: %w" , targetPath , err )
314+ }
315+ case tar .TypeReg , tar .TypeRegA :
316+ if err := os .MkdirAll (filepath .Dir (targetPath ), 0o755 ); err != nil {
317+ return fmt .Errorf ("failed to create parent dir for %s: %w" , targetPath , err )
318+ }
319+ file , createErr := os .OpenFile (targetPath , os .O_CREATE | os .O_TRUNC | os .O_WRONLY , os .FileMode (header .Mode ))
320+ if createErr != nil {
321+ return fmt .Errorf ("failed to create file %s: %w" , targetPath , createErr )
322+ }
323+ if _ , copyErr := io .Copy (file , tarReader ); copyErr != nil {
324+ _ = file .Close ()
325+ return fmt .Errorf ("failed to write file %s: %w" , targetPath , copyErr )
326+ }
327+ if closeErr := file .Close (); closeErr != nil {
328+ return fmt .Errorf ("failed to close file %s: %w" , targetPath , closeErr )
329+ }
330+ }
331+ }
332+ }
333+
334+ func normalizeTarEntryPath (entryName , stripTopDir string ) (string , bool ) {
335+ normalized := strings .TrimPrefix (entryName , "./" )
336+ normalized = path .Clean (normalized )
337+ if normalized == "." || normalized == "/" {
338+ return "" , false
339+ }
340+
341+ if stripTopDir != "" {
342+ stripTopDir = path .Clean (stripTopDir )
343+ if normalized == stripTopDir {
344+ return "" , false
345+ }
346+ prefix := stripTopDir + "/"
347+ normalized = strings .TrimPrefix (normalized , prefix )
348+ }
349+
350+ normalized = strings .TrimPrefix (normalized , "/" )
351+ if normalized == "" {
352+ return "" , false
353+ }
354+ return normalized , true
355+ }
356+
357+ func ensureSubpath (baseDir , targetPath string ) error {
358+ baseAbs , err := filepath .Abs (baseDir )
359+ if err != nil {
360+ return fmt .Errorf ("failed to resolve absolute path for %s: %w" , baseDir , err )
361+ }
362+ targetAbs , err := filepath .Abs (targetPath )
363+ if err != nil {
364+ return fmt .Errorf ("failed to resolve absolute path for %s: %w" , targetPath , err )
365+ }
366+ prefix := baseAbs + string (filepath .Separator )
367+ if targetAbs != baseAbs && ! strings .HasPrefix (targetAbs , prefix ) {
368+ return fmt .Errorf ("unsafe path detected outside destination: %s" , targetPath )
369+ }
370+ return nil
371+ }
372+
248373// SearchLogFile searches logfile using regex and return matches or error
249374func SearchLogFile (fp string , regex string ) ([]string , error ) {
250375 file , err := os .Open (fp )
0 commit comments