@@ -6,10 +6,12 @@ import (
66 "context"
77 "fmt"
88 "io"
9+ "os"
910 "path/filepath"
1011 "slices"
1112 "strconv"
1213 "strings"
14+ "text/tabwriter"
1315
1416 "github.com/containers/image/v5/pkg/blobinfocache/none"
1517 "github.com/containers/image/v5/pkg/compression"
@@ -25,6 +27,16 @@ var topCommand = cli.Command{
2527 Flags : []cli.Flag {
2628 & cli.BoolFlag {Name : "include-pseudo" , Usage : "Include pseudo-filesystems (/dev, /proc, /sys)" },
2729 & cli.BoolFlag {Name : "follow-symlinks" , Usage : "Follow symbolic links" },
30+ & cli.BoolFlag {
31+ Name : "human-readable" ,
32+ Usage : "Show file sizes in human readable format" ,
33+ Aliases : []string {"h" },
34+ },
35+ & cli.StringSliceFlag {
36+ Name : "layer" ,
37+ Usage : "Filter results to specific layer(s) by SHA256 digest. If not specified, all layers are included (not an empty result)." ,
38+ Aliases : []string {"l" },
39+ },
2840 },
2941 Arguments : []cli.Argument {
3042 & cli.StringArg {Name : "image" , UsageText : "Container image ref" },
@@ -35,17 +47,25 @@ var topCommand = cli.Command{
3547 return fmt .Errorf ("image URL is required" )
3648 }
3749
50+ humanReadable := c .Bool ("human-readable" )
51+ layers := c .StringSlice ("layer" )
52+ if c .IsSet ("layer" ) && len (layers ) == 0 {
53+ return fmt .Errorf ("--layer flag provided but no layer digest specified; please provide at least one layer digest" )
54+ }
55+
3856 sysCtx := types.SystemContext {}
39- return analyzeLayers (image , ctx , & sysCtx )
57+
58+ return analyzeLayers (ctx , & sysCtx , image , layers , humanReadable )
4059 },
4160}
4261
4362const defaultFileLimit = 10
4463
4564type FileInfo struct {
46- Path string
47- Size int64
48- Layer string // layer ID this file belongs to
65+ Path string
66+ Size int64
67+ HumanReadableSize string
68+ Layer string // layer ID this file belongs to
4969}
5070
5171type FileHeap []FileInfo
@@ -66,9 +86,73 @@ func (h *FileHeap) Pop() interface{} {
6686 return item
6787}
6888
89+ // HumanReadableSize converts a byte count to a human readable string
90+ // From https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
91+ func HumanReadableSize (b int64 ) string {
92+ const unit = 1000
93+ if b < unit {
94+ return fmt .Sprintf ("%d B" , b )
95+ }
96+ div , exp := int64 (unit ), 0
97+ for n := b / unit ; n >= unit ; n /= unit {
98+ div *= unit
99+ exp ++
100+ }
101+ return fmt .Sprintf ("%.1f %cB" ,
102+ float64 (b )/ float64 (div ), "kMGTPE" [exp ])
103+ }
104+
105+ // getFilteredLayers returns only the layers that needs to be
106+ // processed/extracted e.g. after user specifies specific layer(s)
107+ // using --layer, we shouldn't be processing all the layers.
108+ // If layers is empty, returns all layers in the image.
109+ func getFilteredLayers (img types.Image , layers []string ) ([]types.BlobInfo , error ) {
110+ allLayers := img .LayerInfos ()
111+
112+ if len (layers ) == 0 {
113+ return allLayers , nil // No filtering needed
114+ }
115+
116+ // Build a map for efficient layer lookup
117+ layerMap := make (map [string ]types.BlobInfo )
118+ for _ , layer := range allLayers {
119+ layerMap [layer .Digest .Encoded ()] = layer
120+ }
121+
122+ var filteredLayers []types.BlobInfo
123+ seenLayers := make (map [string ]bool ) // Track which layers we've already added
124+
125+ for _ , filter := range layers {
126+ var matchedLayersDigests []string
127+ for layerDigest := range layerMap {
128+ if layerDigest == filter || strings .HasPrefix (layerDigest , filter ) {
129+ matchedLayersDigests = append (matchedLayersDigests , layerDigest )
130+ }
131+ }
132+
133+ if len (matchedLayersDigests ) == 0 {
134+ return nil , fmt .Errorf ("layer %s not found in image" , filter )
135+ }
136+ if len (matchedLayersDigests ) > 1 {
137+ return nil , fmt .Errorf ("multiple layers match shortened digest %s" , filter )
138+ }
139+
140+ matchedLayerDigest := matchedLayersDigests [0 ]
141+ layer := layerMap [matchedLayerDigest ]
142+
143+ // Only add if we haven't seen this layer before
144+ if ! seenLayers [layer .Digest .String ()] {
145+ filteredLayers = append (filteredLayers , layer )
146+ seenLayers [layer .Digest .String ()] = true
147+ }
148+ }
149+
150+ return filteredLayers , nil
151+ }
152+
69153// analyzeLayers fetches layers for a given image reference
70154// reads the associated layer archives and lists file info
71- func analyzeLayers (uri string , ctx context.Context , sysCtx * types.SystemContext ) error {
155+ func analyzeLayers (ctx context.Context , sysCtx * types.SystemContext , uri string , layers [] string , humanReadable bool ) error {
72156 img , _ , err := skiff .ImageAndLayersFromURI (ctx , sysCtx , uri )
73157 if err != nil {
74158 return err
@@ -83,9 +167,7 @@ func analyzeLayers(uri string, ctx context.Context, sysCtx *types.SystemContext)
83167 h := & FileHeap {}
84168 heap .Init (h )
85169
86- files := make ([]FileInfo , h .Len ())
87-
88- layerInfos , err := skiff .BlobInfoFromImage (img , ctx , sysCtx )
170+ layerInfos , err := getFilteredLayers (img , layers )
89171 if err != nil {
90172 return err
91173 }
@@ -113,48 +195,49 @@ func analyzeLayers(uri string, ctx context.Context, sysCtx *types.SystemContext)
113195 return fmt .Errorf ("failed to read tar header: %w" , err )
114196 }
115197
116- // TODO: follow symlinks
198+ // TODO(danishprakash) : follow symlinks
117199 // if hdr.Typeflag == tar.TypeSymlink
118200
119201 path , err := filepath .Abs (filepath .Join ("/" , hdr .Name ))
120202 if err != nil {
121- // TODO: perhaps just log and not error out
122- return fmt .Errorf ("error generating absolute representation of path: %w" , err )
203+ // Log the error but continue processing other files
204+ fmt .Fprintf (os .Stderr , "warning: error generating absolute representation of path %s: %v\n " , hdr .Name , err )
205+ continue
123206 }
124207
125208 if hdr .Typeflag == tar .TypeReg {
126- heap .Push (h , FileInfo {Path : path , Size : hdr .Size , Layer : layer .Digest .Encoded ()})
209+ heap .Push (h , FileInfo {
210+ Path : path ,
211+ Size : hdr .Size ,
212+ HumanReadableSize : HumanReadableSize (hdr .Size ),
213+ Layer : layer .Digest .Encoded (),
214+ })
127215 if h .Len () > defaultFileLimit {
128216 heap .Pop (h )
129217 }
130218 }
131219 }
132-
133220 }
134221
135- for i := 0 ; h .Len () > 0 ; i ++ {
222+ // Extract files from heap in reverse order (largest first)
223+ var files []FileInfo
224+ for h .Len () > 0 {
136225 files = append (files , heap .Pop (h ).(FileInfo ))
137226 }
138227
139- maxPathLen , maxSizeLen , maxLayerLen := 0 , 0 , 12
140- for _ , f := range files {
141- if len (f .Path ) > maxPathLen {
142- maxPathLen = len (f .Path )
143- }
144- sizeStr := strconv .FormatInt (f .Size , 10 )
145- if len (sizeStr ) > maxSizeLen {
146- maxSizeLen = len (sizeStr )
147- }
148- }
149-
150- fmt .Printf ("%-*s %*s %-*s\n " , maxPathLen , "File Path" , maxSizeLen , "Size" , maxLayerLen , "Layer ID" )
151- fmt .Println (strings .Repeat ("-" , maxPathLen + maxSizeLen + maxLayerLen + 15 )) // also consider two tab chars
228+ w := tabwriter .NewWriter (os .Stdout , 0 , 0 , 2 , ' ' , tabwriter .TabIndent )
229+ defer w .Flush ()
230+ fmt .Fprintln (w , "FILE PATH\t SIZE\t LAYER ID" )
152231
153232 slices .Reverse (files )
154233 for _ , f := range files {
155- sizeStr := strconv .FormatInt (f .Size , 10 )
156- fmt .Printf ("%-*s %*s %-*s\n " , maxPathLen , f .Path , maxSizeLen , sizeStr , maxLayerLen , f .Layer [:12 ])
234+ var size string
235+ if humanReadable {
236+ size = f .HumanReadableSize
237+ } else {
238+ size = strconv .FormatInt (f .Size , 10 )
239+ }
240+ fmt .Fprintf (w , "%s\t %s\t %s\n " , f .Path , size , f .Layer [:12 ])
157241 }
158-
159242 return nil
160243}
0 commit comments