github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/du-main.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "context" 22 "fmt" 23 "net/url" 24 "path" 25 "strings" 26 "time" 27 28 "github.com/dustin/go-humanize" 29 "github.com/fatih/color" 30 "github.com/minio/cli" 31 json "github.com/minio/colorjson" 32 "github.com/minio/mc/pkg/probe" 33 "github.com/minio/pkg/v2/console" 34 ) 35 36 // du specific flags. 37 var ( 38 duFlags = []cli.Flag{ 39 cli.IntFlag{ 40 Name: "depth, d", 41 Usage: "print the total for a folder prefix only if it is N or fewer levels below the command line argument", 42 }, 43 cli.BoolFlag{ 44 Name: "recursive, r", 45 Usage: "recursively print the total for a folder prefix", 46 }, 47 cli.StringFlag{ 48 Name: "rewind", 49 Usage: "include all object versions no later than specified date", 50 }, 51 cli.BoolFlag{ 52 Name: "versions", 53 Usage: "include all object versions", 54 }, 55 } 56 ) 57 58 // Summarize disk usage. 59 var duCmd = cli.Command{ 60 Name: "du", 61 Usage: "summarize disk usage recursively", 62 Action: mainDu, 63 OnUsageError: onUsageError, 64 Before: setGlobalsFromContext, 65 Flags: append(duFlags, globalFlags...), 66 CustomHelpTemplate: `NAME: 67 {{.HelpName}} - {{.Usage}} 68 69 USAGE: 70 {{.HelpName}} [FLAGS] TARGET 71 72 FLAGS: 73 {{range .VisibleFlags}}{{.}} 74 {{end}} 75 76 EXAMPLES: 77 1. Summarize disk usage of 'jazz-songs' bucket recursively. 78 {{.Prompt}} {{.HelpName}} s3/jazz-songs 79 80 2. Summarize disk usage of 'louis' prefix in 'jazz-songs' bucket upto two levels. 81 {{.Prompt}} {{.HelpName}} --depth=2 s3/jazz-songs/louis/ 82 83 3. Summarize disk usage of 'jazz-songs' bucket at a fixed date/time 84 {{.Prompt}} {{.HelpName}} --rewind "2020.01.01" s3/jazz-songs/ 85 86 4. Summarize disk usage of 'jazz-songs' bucket with all objects versions 87 {{.Prompt}} {{.HelpName}} --versions s3/jazz-songs/ 88 `, 89 } 90 91 // Structured message depending on the type of console. 92 type duMessage struct { 93 Prefix string `json:"prefix"` 94 Size int64 `json:"size"` 95 Objects int64 `json:"objects"` 96 Status string `json:"status"` 97 IsVersions bool `json:"isVersions"` 98 } 99 100 // Colorized message for console printing. 101 func (r duMessage) String() string { 102 humanSize := strings.Join(strings.Fields(humanize.IBytes(uint64(r.Size))), "") 103 cnt := fmt.Sprintf("%d object", r.Objects) 104 if r.IsVersions { 105 cnt = fmt.Sprintf("%d version", r.Objects) 106 } 107 if r.Objects != 1 { 108 cnt += "s" // pluralize 109 } 110 return fmt.Sprintf("%s\t%s\t%s", console.Colorize("Size", humanSize), 111 console.Colorize("Objects", cnt), 112 console.Colorize("Prefix", r.Prefix)) 113 } 114 115 // JSON'ified message for scripting. 116 func (r duMessage) JSON() string { 117 msgBytes, e := json.MarshalIndent(r, "", " ") 118 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 119 return string(msgBytes) 120 } 121 122 func du(ctx context.Context, urlStr string, timeRef time.Time, withVersions bool, depth int) (sz, objs int64, err error) { 123 targetAlias, targetURL, _ := mustExpandAlias(urlStr) 124 125 if !strings.HasSuffix(targetURL, "/") { 126 targetURL += "/" 127 } 128 129 clnt, pErr := newClientFromAlias(targetAlias, targetURL) 130 if pErr != nil { 131 errorIf(pErr.Trace(urlStr), "Failed to summarize disk usage `"+urlStr+"`.") 132 return 0, 0, exitStatus(globalErrorExitStatus) // End of journey. 133 } 134 135 // No disk usage details below this level, 136 // just do a recursive listing 137 recursive := depth == 1 138 139 targetAbsolutePath := path.Clean(clnt.GetURL().String()) 140 141 contentCh := clnt.List(ctx, ListOptions{ 142 TimeRef: timeRef, 143 WithOlderVersions: withVersions, 144 Recursive: recursive, 145 ShowDir: DirFirst, 146 }) 147 size := int64(0) 148 objects := int64(0) 149 for content := range contentCh { 150 if content.Err != nil { 151 switch content.Err.ToGoError().(type) { 152 // handle this specifically for filesystem related errors. 153 case BrokenSymlink, TooManyLevelsSymlink, PathNotFound, ObjectOnGlacier: 154 continue 155 case PathInsufficientPermission: 156 errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") 157 continue 158 } 159 errorIf(content.Err.Trace(urlStr), "Failed to find disk usage of `"+urlStr+"` recursively.") 160 return 0, 0, exitStatus(globalErrorExitStatus) 161 } 162 163 if content.URL.Path == targetAbsolutePath { 164 continue 165 } 166 167 if content.Type.IsDir() && !recursive { 168 depth := depth 169 if depth > 0 { 170 depth-- 171 } 172 173 subDirAlias := content.URL.Path 174 if targetAlias != "" { 175 subDirAlias = targetAlias + "/" + content.URL.Path 176 } 177 used, n, err := du(ctx, subDirAlias, timeRef, withVersions, depth) 178 if err != nil { 179 return 0, 0, err 180 } 181 size += used 182 objects += n 183 } else { 184 if !content.IsDeleteMarker && !content.Type.IsDir() { 185 size += content.Size 186 objects++ 187 } 188 } 189 } 190 191 if depth != 0 { 192 u, e := url.Parse(targetURL) 193 if e != nil { 194 panic(e) 195 } 196 197 printMsg(duMessage{ 198 Prefix: strings.Trim(u.Path, "/"), 199 Size: size, 200 Objects: objects, 201 Status: "success", 202 IsVersions: withVersions, 203 }) 204 } 205 206 return size, objects, nil 207 } 208 209 // main for du command. 210 func mainDu(cliCtx *cli.Context) error { 211 if !cliCtx.Args().Present() { 212 showCommandHelpAndExit(cliCtx, 1) 213 } 214 215 // Set colors. 216 console.SetColor("Remove", color.New(color.FgGreen, color.Bold)) 217 console.SetColor("Prefix", color.New(color.FgCyan, color.Bold)) 218 console.SetColor("Objects", color.New(color.FgGreen)) 219 console.SetColor("Size", color.New(color.FgYellow)) 220 221 ctx, cancelRm := context.WithCancel(globalContext) 222 defer cancelRm() 223 224 // du specific flags. 225 depth := cliCtx.Int("depth") 226 if depth == 0 { 227 if cliCtx.Bool("recursive") { 228 if !cliCtx.IsSet("depth") { 229 depth = -1 230 } 231 } else { 232 depth = 1 233 } 234 } 235 236 withVersions := cliCtx.Bool("versions") 237 timeRef := parseRewindFlag(cliCtx.String("rewind")) 238 239 var duErr error 240 var isDir bool 241 for _, urlStr := range cliCtx.Args() { 242 isDir, _ = isAliasURLDir(ctx, urlStr, nil, time.Time{}, false) 243 if !isDir { 244 fatalIf(errInvalidArgument().Trace(urlStr), fmt.Sprintf("Source `%s` is not a folder. Only folders are supported by 'du' command.", urlStr)) 245 } 246 247 if _, _, err := du(ctx, urlStr, timeRef, withVersions, depth); duErr == nil { 248 duErr = err 249 } 250 } 251 252 return duErr 253 }