@@ -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"
@@ -31,6 +33,7 @@ func main() {
3133 Action : func (c * cli.Context ) error {
3234 cfg := & config {
3335 ref : c .Args ().First (),
36+ out : os .Stdout ,
3437 }
3538 if err := inspect (cfg ); err != nil {
3639 return cli .Exit (c .Command .Name + ": " + err .Error (), 1 )
@@ -46,6 +49,7 @@ func main() {
4649 ref : c .Args ().First (),
4750 layerIDs : c .Args ().Tail (),
4851 sort : c .Bool ("sort" ),
52+ out : os .Stdout ,
4953 }
5054
5155 if err := ls (cfg ); err != nil {
@@ -61,6 +65,28 @@ func main() {
6165 },
6266 },
6367 },
68+ {
69+ Name : "extract" ,
70+ Usage : "extract files from an image and print to stdout" ,
71+ Action : func (c * cli.Context ) error {
72+ cfg := & config {
73+ ref : c .Args ().First (),
74+ files : c .Args ().Tail (),
75+ outputDir : c .String ("output_dir" ),
76+ out : os .Stdout ,
77+ }
78+ if err := extract (cfg ); err != nil {
79+ return cli .Exit (c .Command .Name + ": " + err .Error (), 1 )
80+ }
81+ return nil
82+ },
83+ Flags : []cli.Flag {
84+ & cli.StringFlag {
85+ Name : "output_dir" ,
86+ Usage : "Write extracted files to this directory instead of stdout" ,
87+ },
88+ },
89+ },
6490 },
6591 }
6692
@@ -75,6 +101,12 @@ type config struct {
75101 layerIDs []string
76102 // sort is true if the output should be sorted by size.
77103 sort bool
104+ // files is the list of file paths to extract.
105+ files []string
106+ // outputDir is the directory to write extracted files to.
107+ outputDir string
108+ // out is the writer for output.
109+ out io.Writer
78110}
79111
80112// makeOptions returns the options for crane.
@@ -148,13 +180,17 @@ func inspect(cfg *config) error {
148180 if err != nil {
149181 return err
150182 }
183+ return inspectImage (cfg , image )
184+ }
151185
186+ // inspectImage prints info about the layers of an image.
187+ func inspectImage (cfg * config , image v1.Image ) error {
152188 layers , err := image .Layers ()
153189 if err != nil {
154190 return err
155191 }
156192
157- tw := tabwriter .NewWriter (os . Stdout , 0 , 0 , 2 , ' ' , 0 )
193+ tw := tabwriter .NewWriter (cfg . out , 0 , 0 , 2 , ' ' , 0 )
158194 tw .Write ([]byte ("N\t Layer\t Size\n " ))
159195 for i , layer := range layers {
160196 hash , err := layer .DiffID ()
@@ -178,7 +214,11 @@ func ls(cfg *config) error {
178214 if err != nil {
179215 return err
180216 }
217+ return lsImage (cfg , image )
218+ }
181219
220+ // lsImage lists files in the layers of an image.
221+ func lsImage (cfg * config , image v1.Image ) error {
182222 layers , err := image .Layers ()
183223 if err != nil {
184224 return fmt .Errorf ("getting layers: %w" , err )
@@ -237,8 +277,8 @@ func files(cfg *config, layer v1.Layer) error {
237277
238278 tarReader := tar .NewReader (uncompressed )
239279
240- fmt .Printf ( "\n --- %s ---\n " , hash )
241- tw := tabwriter .NewWriter (os . Stdout , 0 , 0 , 2 , ' ' , 0 )
280+ fmt .Fprintf ( cfg . out , "\n --- %s ---\n " , hash )
281+ tw := tabwriter .NewWriter (cfg . out , 0 , 0 , 2 , ' ' , 0 )
242282 tw .Write ([]byte ("Mode\t Size\t Name\n " ))
243283
244284 headers := make ([]* tar.Header , 0 )
@@ -271,3 +311,115 @@ func files(cfg *config, layer v1.Layer) error {
271311
272312 return tw .Flush ()
273313}
314+
315+ // extract extracts files from an image and writes them to stdout or a directory.
316+ func extract (cfg * config ) error {
317+ if len (cfg .files ) == 0 {
318+ return fmt .Errorf ("no files specified" )
319+ }
320+
321+ image , err := getImage (cfg .ref )
322+ if err != nil {
323+ return err
324+ }
325+
326+ return extractFromImage (cfg , image )
327+ }
328+
329+ // extractFromImage extracts files from the given image.
330+ func extractFromImage (cfg * config , image v1.Image ) error {
331+ layers , err := image .Layers ()
332+ if err != nil {
333+ return fmt .Errorf ("getting layers: %w" , err )
334+ }
335+
336+ // Build a set of wanted files for quick lookup.
337+ // Normalize by stripping leading slash.
338+ wanted := make (map [string ]bool , len (cfg .files ))
339+ for _ , f := range cfg .files {
340+ wanted [strings .TrimPrefix (f , "/" )] = true
341+ }
342+
343+ found := make (map [string ]bool , len (cfg .files ))
344+
345+ // Search layers in reverse order (last wins) to match container runtime behavior.
346+ for i := len (layers ) - 1 ; i >= 0 ; i -- {
347+ layer := layers [i ]
348+
349+ uncompressed , err := layer .Uncompressed ()
350+ if err != nil {
351+ return fmt .Errorf ("getting layer: %w" , err )
352+ }
353+
354+ tarReader := tar .NewReader (uncompressed )
355+ for {
356+ header , err := tarReader .Next ()
357+ if err == io .EOF {
358+ break
359+ }
360+ if err != nil {
361+ uncompressed .Close ()
362+ return fmt .Errorf ("reading tar: %w" , err )
363+ }
364+
365+ name := strings .TrimPrefix (header .Name , "./" )
366+ name = strings .TrimPrefix (name , "/" )
367+
368+ if ! wanted [name ] || found [name ] {
369+ continue
370+ }
371+
372+ if err := extractFile (cfg , name , tarReader ); err != nil {
373+ uncompressed .Close ()
374+ return err
375+ }
376+ found [name ] = true
377+
378+ // Stop early if all files found.
379+ if len (found ) == len (wanted ) {
380+ uncompressed .Close ()
381+ return nil
382+ }
383+ }
384+ uncompressed .Close ()
385+ }
386+
387+ // Report any files not found.
388+ var missing []string
389+ for _ , f := range cfg .files {
390+ name := strings .TrimPrefix (f , "/" )
391+ if ! found [name ] {
392+ missing = append (missing , f )
393+ }
394+ }
395+ if len (missing ) > 0 {
396+ return fmt .Errorf ("files not found: %s" , strings .Join (missing , ", " ))
397+ }
398+
399+ return nil
400+ }
401+
402+ // extractFile writes the contents of a tar entry to stdout or to a file under outputDir.
403+ func extractFile (cfg * config , name string , r io.Reader ) error {
404+ if cfg .outputDir == "" {
405+ _ , err := io .Copy (cfg .out , r )
406+ return err
407+ }
408+
409+ outPath := filepath .Join (cfg .outputDir , name )
410+ if err := os .MkdirAll (filepath .Dir (outPath ), 0o755 ); err != nil {
411+ return fmt .Errorf ("creating directory for %s: %w" , name , err )
412+ }
413+
414+ f , err := os .Create (outPath )
415+ if err != nil {
416+ return fmt .Errorf ("creating file %s: %w" , outPath , err )
417+ }
418+ defer f .Close ()
419+
420+ if _ , err := io .Copy (f , r ); err != nil {
421+ return fmt .Errorf ("writing file %s: %w" , outPath , err )
422+ }
423+
424+ return nil
425+ }
0 commit comments