@@ -5,8 +5,10 @@ import (
55 "fmt"
66 "io"
77 "os"
8+ "path/filepath"
89 "sort"
910 "strconv"
11+ "strings"
1012 "text/tabwriter"
1113
1214 "github.com/dustin/go-humanize"
@@ -61,6 +63,27 @@ func main() {
6163 },
6264 },
6365 },
66+ {
67+ Name : "extract" ,
68+ Usage : "extract files from an image and print to stdout" ,
69+ Action : func (c * cli.Context ) error {
70+ cfg := & config {
71+ ref : c .Args ().First (),
72+ files : c .Args ().Tail (),
73+ outputDir : c .String ("output_dir" ),
74+ }
75+ if err := extract (cfg ); err != nil {
76+ return cli .Exit (c .Command .Name + ": " + err .Error (), 1 )
77+ }
78+ return nil
79+ },
80+ Flags : []cli.Flag {
81+ & cli.StringFlag {
82+ Name : "output_dir" ,
83+ Usage : "Write extracted files to this directory instead of stdout" ,
84+ },
85+ },
86+ },
6487 },
6588 }
6689
@@ -75,6 +98,10 @@ type config struct {
7598 layerIDs []string
7699 // sort is true if the output should be sorted by size.
77100 sort bool
101+ // files is the list of file paths to extract.
102+ files []string
103+ // outputDir is the directory to write extracted files to.
104+ outputDir string
78105}
79106
80107// makeOptions returns the options for crane.
@@ -271,3 +298,110 @@ func files(cfg *config, layer v1.Layer) error {
271298
272299 return tw .Flush ()
273300}
301+
302+ // extract extracts files from an image and writes them to stdout or a directory.
303+ func extract (cfg * config ) error {
304+ if len (cfg .files ) == 0 {
305+ return fmt .Errorf ("no files specified" )
306+ }
307+
308+ image , err := getImage (cfg .ref )
309+ if err != nil {
310+ return err
311+ }
312+
313+ layers , err := image .Layers ()
314+ if err != nil {
315+ return fmt .Errorf ("getting layers: %w" , err )
316+ }
317+
318+ // Build a set of wanted files for quick lookup.
319+ // Normalize by stripping leading slash.
320+ wanted := make (map [string ]bool , len (cfg .files ))
321+ for _ , f := range cfg .files {
322+ wanted [strings .TrimPrefix (f , "/" )] = true
323+ }
324+
325+ found := make (map [string ]bool , len (cfg .files ))
326+
327+ // Search layers in reverse order (last wins) to match container runtime behavior.
328+ for i := len (layers ) - 1 ; i >= 0 ; i -- {
329+ layer := layers [i ]
330+
331+ uncompressed , err := layer .Uncompressed ()
332+ if err != nil {
333+ return fmt .Errorf ("getting layer: %w" , err )
334+ }
335+
336+ tarReader := tar .NewReader (uncompressed )
337+ for {
338+ header , err := tarReader .Next ()
339+ if err == io .EOF {
340+ break
341+ }
342+ if err != nil {
343+ uncompressed .Close ()
344+ return fmt .Errorf ("reading tar: %w" , err )
345+ }
346+
347+ name := strings .TrimPrefix (header .Name , "./" )
348+ name = strings .TrimPrefix (name , "/" )
349+
350+ if ! wanted [name ] || found [name ] {
351+ continue
352+ }
353+
354+ if err := extractFile (cfg , name , tarReader ); err != nil {
355+ uncompressed .Close ()
356+ return err
357+ }
358+ found [name ] = true
359+
360+ // Stop early if all files found.
361+ if len (found ) == len (wanted ) {
362+ uncompressed .Close ()
363+ return nil
364+ }
365+ }
366+ uncompressed .Close ()
367+ }
368+
369+ // Report any files not found.
370+ var missing []string
371+ for _ , f := range cfg .files {
372+ name := strings .TrimPrefix (f , "/" )
373+ if ! found [name ] {
374+ missing = append (missing , f )
375+ }
376+ }
377+ if len (missing ) > 0 {
378+ return fmt .Errorf ("files not found: %s" , strings .Join (missing , ", " ))
379+ }
380+
381+ return nil
382+ }
383+
384+ // extractFile writes the contents of a tar entry to stdout or to a file under outputDir.
385+ func extractFile (cfg * config , name string , r io.Reader ) error {
386+ if cfg .outputDir == "" {
387+ _ , err := io .Copy (os .Stdout , r )
388+ return err
389+ }
390+
391+ outPath := filepath .Join (cfg .outputDir , name )
392+ if err := os .MkdirAll (filepath .Dir (outPath ), 0o755 ); err != nil {
393+ return fmt .Errorf ("creating directory for %s: %w" , name , err )
394+ }
395+
396+ f , err := os .Create (outPath )
397+ if err != nil {
398+ return fmt .Errorf ("creating file %s: %w" , outPath , err )
399+ }
400+ defer f .Close ()
401+
402+ if _ , err := io .Copy (f , r ); err != nil {
403+ return fmt .Errorf ("writing file %s: %w" , outPath , err )
404+ }
405+
406+ return nil
407+ }
0 commit comments