├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── go.mod ├── go.sum └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /docker-show-context_* 2 | .git 3 | .gitignore 4 | *.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docker-show-context_* 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11 AS builder 2 | WORKDIR /app 3 | COPY . . 4 | ENV GO111MODULE=on 5 | RUN go build -o docker-show-context . 6 | 7 | FROM alpine:latest 8 | COPY --from=builder /app/docker-show-context /usr/local/bin 9 | RUN apk add --no-cache libc6-compat 10 | WORKDIR /data 11 | ENTRYPOINT ["docker-show-context"] 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Peter Waller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `docker-show-context` 2 | 3 | Ever wonder why docker pauses when you do `docker build`, and what you can do 4 | about it? You know, when it says `Sending build context to Docker daemon`? 5 | 6 | This program shows where time and bytes are spent when building a docker context. 7 | 8 | It is based directly on the same logic that Docker itself uses to build the 9 | context. 10 | 11 | ## Getting started (docker): 12 | 13 | ```sh 14 | docker build -t docker-show-context https://github.com/pwaller/docker-show-context.git 15 | docker run --rm -v $PWD:/data docker-show-context 16 | ``` 17 | 18 | ## Getting started (binaries): 19 | 20 | Binaries are available on the 21 | [releases page](https://github.com/pwaller/docker-show-context/releases). 22 | Just grab the binary and put it in your path, then invoke it as 23 | `docker-show-context`. Use at your own risk. 24 | 25 | ## Getting started (building from source): 26 | 27 | You will need go 1.11 or more recent, which can be obtained from 28 | [the go website](https://golang.org/dl). 29 | 30 | Then run: 31 | 32 | ``` 33 | git clone https://github.com/pwaller/docker-show-context 34 | cd docker-show-context 35 | GO111MODULE=on go install -v 36 | ``` 37 | 38 | # What the output looks like 39 | 40 | The output looks something like this. It's easy to see now that I accidentally 41 | included some large binary content (`*.deb` and `*.pdf` files in particular), 42 | so I can now go and add those to my `.dockerignore` or delete them. 43 | 44 | ``` 45 | $ cd ~/path/to/project/using/docker 46 | $ docker-show-context 47 | Scanning local directory (in tar / on disk): 48 | 24 / 1057 (62 / 216 MiB) (0.0s elapsed) .. completed 49 | 50 | Excluded by .dockerignore: 1033 files totalling 153.98 MiB 51 | 52 | Final .tar: 53 | 24 files totalling 61.83 MiB (+ 0.02 MiB tar overhead) 54 | Took 0.04 seconds to build 55 | 56 | Top 10 directories by time spent: 57 | 40 ms: . 58 | 1 ms: example 59 | 60 | Top 10 directories by storage: 61 | 61.83 MiB: . 62 | 0.00 MiB: example 63 | 64 | Top 10 directories by file count: 65 | 23: . 66 | 1: example 67 | 68 | Top 10 file extensions by storage: 69 | 57.10 MiB: 70 | 4.71 MiB: .exe 71 | 0.01 MiB: .pprof 72 | 0.01 MiB: .md 73 | 0.01 MiB: .go 74 | 0.00 MiB: .sum 75 | 0.00 MiB: .mod 76 | 0.00 MiB: .sh 77 | 0.00 MiB: .gitignore 78 | 0.00 MiB: .dockerignore 79 | ``` 80 | 81 | # Notes about the current behaviour 82 | 83 | This documents the current behaviour, which may not be ideal, but it is what it 84 | is for now. Pull requests welcome. 85 | 86 | * The amounts shown don't show recursive usage, they just show a single level 87 | of the directory. (Otherwise, the root would always be the biggest thing). 88 | 89 | * Time records the amount of time between `tarFile.Next()` calls. I assume that 90 | this approximates the amount of time `docker/pkg/archive` spent constructing 91 | one tar entry. It might not be precise. 92 | 93 | * "Total content" shows the uncompressed bytes inside files inside the tar. 94 | The total amount sent to the docker daemon is this amount plus the tar 95 | overhead. 96 | 97 | * At this moment, only running with the build context root as the current 98 | working directory is supported, with a dockerfile named `Dockerfile`. 99 | Pull requests welcome to add parameters, so long as the existing default 100 | behaviour is preserved. 101 | 102 | # How can I use this to make building faster? 103 | 104 | Frequently, I find that `docker build` suddenly takes longer than I expect. It 105 | is often the case that I have accidentally included some binaries or something 106 | which I did not intend to include. This the purpose of this tool is to give 107 | visibility into this, taking into account your existing `.dockerignore`, 108 | so that you can improve your `.dockerfile` or delete assets you don't need. 109 | 110 | It scratches an itch. 111 | 112 | # License 113 | 114 | > The MIT License (MIT) 115 | > 116 | > Copyright (c) 2016-2018 Peter Waller 117 | > 118 | > Permission is hereby granted, free of charge, to any person obtaining a copy 119 | > of this software and associated documentation files (the "Software"), to deal 120 | > in the Software without restriction, including without limitation the rights 121 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 122 | > copies of the Software, and to permit persons to whom the Software is 123 | > furnished to do so, subject to the following conditions: 124 | > 125 | > The above copyright notice and this permission notice shall be included in all 126 | > copies or substantial portions of the Software. 127 | > 128 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 129 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 130 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 131 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 132 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 133 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 134 | > SOFTWARE. 135 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pwaller/docker-show-context 2 | 3 | require ( 4 | github.com/Microsoft/go-winio v0.4.14 // indirect 5 | github.com/Microsoft/hcsshim v0.8.6 // indirect 6 | github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 // indirect 7 | github.com/docker/docker v1.4.2-0.20191108004417-1a88e0255496 8 | github.com/docker/go-units v0.4.0 // indirect 9 | github.com/golang/protobuf v1.3.2 // indirect 10 | github.com/google/go-cmp v0.3.1 // indirect 11 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 12 | github.com/opencontainers/go-digest v1.0.0-rc1 // indirect 13 | github.com/opencontainers/image-spec v1.0.1 // indirect 14 | github.com/opencontainers/runc v0.1.1 // indirect 15 | github.com/sirupsen/logrus v1.4.2 // indirect 16 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect 17 | golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd // indirect 18 | gotest.tools v2.2.0+incompatible // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= 2 | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= 3 | github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA= 4 | github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= 5 | github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= 6 | github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/docker/docker v0.0.0-20191017212428-1e000435e60d h1:0QAxgI4pvRtTcgcvIo5xSSJPTn0kgEg2PqdcvP2hBJ8= 9 | github.com/docker/docker v0.0.0-20191017212428-1e000435e60d/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 10 | github.com/docker/docker v1.4.2-0.20191108004417-1a88e0255496 h1:X3W/mc2aCB8NcDE7HsMSw+/VTW3hzAO8PAP7nTyB3ec= 11 | github.com/docker/docker v1.4.2-0.20191108004417-1a88e0255496/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 12 | github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= 13 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 14 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 16 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 17 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 18 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 19 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 20 | github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= 21 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 22 | github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= 23 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 24 | github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= 25 | github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 26 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 27 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 30 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 31 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 32 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 34 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 35 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20191018095205-727590c5006e h1:ZtoklVMHQy6BFRHkbG6JzK+S6rX82//Yeok1vMlizfQ= 40 | golang.org/x/sys v0.0.0-20191018095205-727590c5006e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII= 42 | golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT License (MIT) 2 | // Copyright (c) 2016 Peter Waller 3 | 4 | package main 5 | 6 | import ( 7 | "archive/tar" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "sort" 14 | "strings" 15 | "time" 16 | 17 | "github.com/docker/docker/builder/dockerignore" 18 | "github.com/docker/docker/pkg/archive" 19 | "github.com/docker/docker/pkg/fileutils" 20 | ) 21 | 22 | // getArchive returns the tarfile io.ReadCloser. It is a direct copy of the 23 | // logic found in the official docker client. 24 | // See . 25 | func getArchive(contextDir, relDockerfile string) (io.ReadCloser, error) { 26 | var err error 27 | 28 | // And canonicalize dockerfile name to a platform-independent one 29 | relDockerfile = archive.CanonicalTarNameForPath(relDockerfile) 30 | 31 | f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) 32 | if err != nil && !os.IsNotExist(err) { 33 | return nil, err 34 | } 35 | 36 | var excludes []string 37 | if err == nil { 38 | excludes, err = dockerignore.ReadAll(f) 39 | if err != nil { 40 | return nil, err 41 | } 42 | } 43 | 44 | // If .dockerignore mentions .dockerignore or the Dockerfile 45 | // then make sure we send both files over to the daemon 46 | // because Dockerfile is, obviously, needed no matter what, and 47 | // .dockerignore is needed to know if either one needs to be 48 | // removed. The daemon will remove them for us, if needed, after it 49 | // parses the Dockerfile. Ignore errors here, as they will have been 50 | // caught by validateContextDirectory above. 51 | var includes = []string{"."} 52 | keepThem1, _ := fileutils.Matches(".dockerignore", excludes) 53 | keepThem2, _ := fileutils.Matches(relDockerfile, excludes) 54 | if keepThem1 || keepThem2 { 55 | includes = append(includes, ".dockerignore", relDockerfile) 56 | } 57 | 58 | return archive.TarWithOptions(contextDir, &archive.TarOptions{ 59 | Compression: archive.Uncompressed, 60 | ExcludePatterns: excludes, 61 | IncludeFiles: includes, 62 | }) 63 | } 64 | 65 | // WriteCounter counts the bytes written to it. 66 | type WriteCounter int 67 | 68 | func (w *WriteCounter) Write(bs []byte) (int, error) { 69 | *w += WriteCounter(len(bs)) 70 | return len(bs), nil 71 | } 72 | 73 | func main() { 74 | log.SetFlags(0) 75 | 76 | // Take a quick and dirty file count. This should be an over-estimate, 77 | // since it doesn't currently attempt to re-implement or reuse the 78 | // dockerignore logic. 79 | totalCount := 0 80 | totalStorage := int64(0) 81 | filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 82 | totalCount++ 83 | totalStorage += info.Size() 84 | return nil 85 | }) 86 | 87 | // TODO(pwaller): Make these parameters? 88 | r, err := getArchive(".", "Dockerfile") 89 | if err != nil { 90 | log.Fatalf("Failed to make context: %v", err) 91 | } 92 | defer r.Close() 93 | 94 | // Keep mappings of paths/extensions to bytes/counts/times. 95 | dirStorage := map[string]int64{} 96 | dirFiles := map[string]int64{} 97 | dirTime := map[string]int64{} 98 | extStorage := map[string]int64{} 99 | 100 | // Counts of amounts seen so far. 101 | currentCount := 0 102 | currentStorage := int64(0) 103 | 104 | // Update the progress indicator at some frequency. 105 | const updateFrequency = 50 // Hz 106 | ticker := time.NewTicker(time.Second / updateFrequency) 107 | defer ticker.Stop() 108 | tick := ticker.C 109 | 110 | start := time.Now() 111 | last := time.Now() 112 | 113 | // Keep a count of how many bytes of Tar file were seen. 114 | writeCounter := WriteCounter(0) 115 | tf := tar.NewReader(io.TeeReader(r, &writeCounter)) 116 | 117 | cr := []byte("\r") 118 | showUpdate := func(w io.Writer) { 119 | os.Stderr.Write(cr) // always to Stderr. 120 | fmt.Fprintf(w, 121 | " %v / %v (%.0f / %.0f MiB) "+ 122 | "(%.1fs elapsed)", 123 | currentCount, 124 | totalCount, 125 | float64(currentStorage)/1024/1024, 126 | float64(totalStorage)/1024/1024, 127 | time.Since(start).Seconds(), 128 | ) 129 | } 130 | 131 | fmt.Println() 132 | fmt.Println("Scanning local directory (in tar / on disk):") 133 | entries: 134 | for { 135 | header, err := tf.Next() 136 | switch err { 137 | case io.EOF: 138 | showUpdate(os.Stdout) 139 | fmt.Println(" .. completed") 140 | fmt.Println() 141 | break entries 142 | default: 143 | log.Fatalf("Error reading archive: %v", err) 144 | return 145 | case nil: 146 | } 147 | 148 | duration := time.Since(last).Nanoseconds() 149 | last = time.Now() 150 | 151 | dir := filepath.Dir(header.Name) 152 | size := header.FileInfo().Size() 153 | 154 | currentCount++ 155 | currentStorage += size 156 | 157 | dirStorage[dir] += size 158 | dirTime[dir] += duration 159 | dirFiles[dir]++ 160 | 161 | if !header.FileInfo().IsDir() { 162 | ext := filepath.Ext(strings.ToLower(header.Name)) 163 | extStorage[ext] += size 164 | } 165 | 166 | select { 167 | case <-tick: 168 | showUpdate(os.Stderr) 169 | default: 170 | } 171 | } 172 | 173 | fmt.Printf( 174 | "Excluded by .dockerignore: "+ 175 | "%d files totalling %.2f MiB\n", 176 | totalCount-currentCount, 177 | float64(totalStorage-currentStorage)/1024/1024) 178 | fmt.Println() 179 | fmt.Println("Final .tar:") 180 | // Epilogue. 181 | fmt.Printf( 182 | " %v files totalling %.2f MiB (+ %.2f MiB tar overhead)\n", 183 | currentCount, 184 | float64(currentStorage)/1024/1024, 185 | float64(int64(writeCounter)-currentStorage)/1024/1024, 186 | ) 187 | fmt.Printf(" Took %.2f seconds to build\n", time.Since(start).Seconds()) 188 | fmt.Println() 189 | 190 | // Produce Top-N. 191 | topDirStorage := SortedBySize(dirStorage) 192 | topDirFiles := SortedBySize(dirFiles) 193 | topDirTime := SortedBySize(dirTime) 194 | topExtStorage := SortedBySize(extStorage) 195 | 196 | const N = 10 197 | 198 | fmt.Printf("Top %d directories by time spent:\n", N) 199 | for i := 0; i < N && i < len(topDirTime); i++ { 200 | entry := &topDirTime[i] 201 | fmt.Printf("%5d ms: %v\n", entry.Size/1000/1000, entry.Path) 202 | } 203 | fmt.Println() 204 | 205 | fmt.Printf("Top %d directories by storage:\n", N) 206 | for i := 0; i < N && i < len(topDirStorage); i++ { 207 | entry := &topDirStorage[i] 208 | fmt.Printf("%7.2f MiB: %v\n", float64(entry.Size)/1024/1024, entry.Path) 209 | } 210 | fmt.Println() 211 | 212 | fmt.Printf("Top %d directories by file count:\n", N) 213 | for i := 0; i < N && i < len(topDirFiles); i++ { 214 | entry := &topDirFiles[i] 215 | fmt.Printf("%5d: %v\n", entry.Size, entry.Path) 216 | } 217 | fmt.Println() 218 | 219 | fmt.Printf("Top %d file extensions by storage:\n", N) 220 | for i := 0; i < N && i < len(topExtStorage); i++ { 221 | entry := &topExtStorage[i] 222 | fmt.Printf("%7.2f MiB: %v\n", float64(entry.Size)/1024/1024, entry.Path) 223 | } 224 | fmt.Println() 225 | } 226 | 227 | // SortedBySize returns direcotries in m sorted by Size (biggest first). 228 | func SortedBySize(m map[string]int64) []PathSize { 229 | bySize := BySize{} 230 | for dir, size := range m { 231 | bySize = append(bySize, PathSize{dir, size}) 232 | } 233 | sort.Sort(bySize) 234 | return []PathSize(bySize) 235 | } 236 | 237 | // PathSize represents a directory with a size. 238 | type PathSize struct { 239 | Path string 240 | Size int64 241 | } 242 | 243 | // BySize sorts by size (biggest first). 244 | type BySize []PathSize 245 | 246 | func (a BySize) Len() int { return len(a) } 247 | func (a BySize) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 248 | func (a BySize) Less(i, j int) bool { return a[i].Size > a[j].Size } 249 | --------------------------------------------------------------------------------