github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/tree-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 "errors" 23 "fmt" 24 "path/filepath" 25 "strings" 26 "time" 27 28 "github.com/fatih/color" 29 "github.com/minio/cli" 30 "github.com/minio/mc/pkg/probe" 31 "github.com/minio/pkg/v2/console" 32 ) 33 34 const ( 35 treeEntry = "├─ " 36 treeLastEntry = "└─ " 37 treeNext = "│" 38 treeLevel = " " 39 ) 40 41 // Structured message depending on the type of console. 42 type treeMessage struct { 43 Entry string 44 IsDir bool 45 BranchString string 46 } 47 48 // Colorized message for console printing. 49 func (t treeMessage) String() string { 50 entryType := "File" 51 if t.IsDir { 52 entryType = "Dir" 53 } 54 return fmt.Sprintf("%s%s", t.BranchString, console.Colorize(entryType, t.Entry)) 55 } 56 57 // JSON'ified message for scripting. 58 // Does No-op. JSON requests are redirected to `ls -r --json` 59 func (t treeMessage) JSON() string { 60 fatalIf(probe.NewError(errors.New("JSON() should never be called here")), "Unable to list in tree format. Please report this issue at https://github.com/minio/mc/issues") 61 return "" 62 } 63 64 var treeFlags = []cli.Flag{ 65 cli.BoolFlag{ 66 Name: "files, f", 67 Usage: "includes files in tree", 68 }, 69 cli.IntFlag{ 70 Name: "depth, d", 71 Usage: "sets the depth threshold", 72 Value: -1, 73 }, 74 cli.StringFlag{ 75 Name: "rewind", 76 Usage: "display tree no later than specified date", 77 }, 78 } 79 80 // trees files and folders. 81 var treeCmd = cli.Command{ 82 Name: "tree", 83 Usage: "list buckets and objects in a tree format", 84 Action: mainTree, 85 OnUsageError: onUsageError, 86 Before: setGlobalsFromContext, 87 Flags: append(treeFlags, globalFlags...), 88 CustomHelpTemplate: `NAME: 89 {{.HelpName}} - {{.Usage}} 90 91 USAGE: 92 {{.HelpName}} [FLAGS] TARGET [TARGET ...] 93 94 FLAGS: 95 {{range .VisibleFlags}}{{.}} 96 {{end}} 97 EXAMPLES: 98 1. List all buckets and directories on MinIO object storage server in tree format. 99 {{.Prompt}} {{.HelpName}} myminio 100 101 2. List all directories in "mybucket" on MinIO object storage server in tree format. 102 {{.Prompt}} {{.HelpName}} myminio/mybucket/ 103 104 3. List all directories in "mybucket" on MinIO object storage server hosted on Microsoft Windows in tree format. 105 {{.Prompt}} {{.HelpName}} myminio\mybucket\ 106 107 4. List all directories and objects in "mybucket" on MinIO object storage server in tree format. 108 {{.Prompt}} {{.HelpName}} --files myminio/mybucket/ 109 110 5. List all directories upto depth level '2' in tree format. 111 {{.Prompt}} {{.HelpName}} --depth 2 myminio/mybucket/ 112 `, 113 } 114 115 // parseTreeSyntax - validate all the passed arguments 116 func parseTreeSyntax(ctx context.Context, cliCtx *cli.Context) (args []string, depth int, files bool, timeRef time.Time) { 117 args = cliCtx.Args() 118 depth = cliCtx.Int("depth") 119 files = cliCtx.Bool("files") 120 121 rewind := cliCtx.String("rewind") 122 timeRef = parseRewindFlag(rewind) 123 124 if depth < -1 || cliCtx.Int("depth") == 0 { 125 fatalIf(errInvalidArgument().Trace(args...), 126 "please set a proper depth, for example: '--depth 1' to limit the tree output, default (-1) output displays everything") 127 } 128 129 if len(args) == 0 { 130 return 131 } 132 133 for _, url := range args { 134 _, _, err := url2Stat(ctx, url2StatOptions{urlStr: url, versionID: "", fileAttr: false, encKeyDB: nil, timeRef: timeRef, isZip: false, ignoreBucketExistsCheck: false}) 135 fatalIf(err.Trace(url), "Unable to tree `"+url+"`.") 136 } 137 return 138 } 139 140 // doTree - list all entities inside a folder in a tree format. 141 func doTree(ctx context.Context, url string, timeRef time.Time, level int, branchString string, depth int, includeFiles bool) error { 142 targetAlias, targetURL, _ := mustExpandAlias(url) 143 if !strings.HasSuffix(targetURL, "/") { 144 targetURL += "/" 145 } 146 147 clnt, err := newClientFromAlias(targetAlias, targetURL) 148 fatalIf(err.Trace(targetURL), "Unable to initialize target `"+targetURL+"`.") 149 150 prefixPath := clnt.GetURL().Path 151 separator := string(clnt.GetURL().Separator) 152 if !strings.HasSuffix(prefixPath, separator) { 153 prefixPath = filepath.Dir(prefixPath) + "/" 154 } 155 156 bucketNameShowed := false 157 var prev *ClientContent 158 show := func(end bool) error { 159 currbranchString := branchString 160 if level == 1 && !bucketNameShowed { 161 bucketNameShowed = true 162 printMsg(treeMessage{ 163 Entry: url, 164 IsDir: true, 165 BranchString: branchString, 166 }) 167 } 168 169 isLevelClosed := strings.HasSuffix(currbranchString, treeLastEntry) 170 if isLevelClosed { 171 currbranchString = strings.TrimSuffix(currbranchString, treeLastEntry) 172 } else { 173 currbranchString = strings.TrimSuffix(currbranchString, treeEntry) 174 } 175 176 if level != 1 { 177 if isLevelClosed { 178 currbranchString += " " + treeLevel 179 } else { 180 currbranchString += treeNext + treeLevel 181 } 182 } 183 184 if end { 185 currbranchString += treeLastEntry 186 } else { 187 currbranchString += treeEntry 188 } 189 190 // Convert any os specific delimiters to "/". 191 contentURL := filepath.ToSlash(prev.URL.Path) 192 prefixPath = filepath.ToSlash(prefixPath) 193 194 // Trim prefix of current working dir 195 prefixPath = strings.TrimPrefix(prefixPath, "."+separator) 196 197 if prev.Type.IsDir() { 198 nextURL := "" 199 if targetAlias != "" { 200 nextURL = targetAlias + "/" + contentURL 201 } else { 202 nextURL = contentURL 203 } 204 205 if nextURL == url { 206 return nil 207 } 208 printMsg(treeMessage{ 209 Entry: strings.TrimSuffix(strings.TrimPrefix(contentURL, prefixPath), "/"), 210 IsDir: true, 211 BranchString: currbranchString, 212 }) 213 } else { 214 printMsg(treeMessage{ 215 Entry: strings.TrimPrefix(contentURL, prefixPath), 216 IsDir: false, 217 BranchString: currbranchString, 218 }) 219 } 220 221 if prev.Type.IsDir() { 222 url := "" 223 if targetAlias != "" { 224 url = targetAlias + "/" + contentURL 225 } else { 226 url = contentURL 227 } 228 229 if depth == -1 || level <= depth { 230 if err := doTree(ctx, url, timeRef, level+1, currbranchString, depth, includeFiles); err != nil { 231 return err 232 } 233 } 234 } 235 236 return nil 237 } 238 239 for content := range clnt.List(ctx, ListOptions{Recursive: false, TimeRef: timeRef, ShowDir: DirFirst}) { 240 if content.Err != nil { 241 errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to tree.") 242 continue 243 } 244 245 if !includeFiles && !content.Type.IsDir() { 246 continue 247 } 248 249 if prev != nil { 250 if err := show(false); err != nil { 251 return err 252 } 253 } 254 255 prev = content 256 } 257 258 if prev != nil { 259 if err := show(true); err != nil { 260 return err 261 } 262 } 263 264 return nil 265 } 266 267 // mainTree - is a handler for mc tree command 268 func mainTree(cliCtx *cli.Context) error { 269 ctx, cancelList := context.WithCancel(globalContext) 270 defer cancelList() 271 272 console.SetColor("File", color.New(color.Bold)) 273 console.SetColor("Dir", color.New(color.FgCyan, color.Bold)) 274 275 // parse 'tree' cliCtx arguments. 276 args, depth, includeFiles, timeRef := parseTreeSyntax(ctx, cliCtx) 277 278 // mimic operating system tool behavior. 279 if len(args) == 0 { 280 args = []string{"."} 281 } 282 283 var cErr error 284 for _, targetURL := range args { 285 if !globalJSON { 286 if e := doTree(ctx, targetURL, timeRef, 1, "", depth, includeFiles); e != nil { 287 cErr = e 288 } 289 } else { 290 targetAlias, targetURL, _ := mustExpandAlias(targetURL) 291 if !strings.HasSuffix(targetURL, "/") { 292 targetURL += "/" 293 } 294 clnt, err := newClientFromAlias(targetAlias, targetURL) 295 fatalIf(err.Trace(targetURL), "Unable to initialize target `"+targetURL+"`.") 296 opts := doListOptions{ 297 timeRef: timeRef, 298 isRecursive: true, 299 isIncomplete: false, 300 isSummary: false, 301 withOlderVersions: false, 302 listZip: false, 303 filter: "*", 304 } 305 if e := doList(ctx, clnt, opts); e != nil { 306 cErr = e 307 } 308 } 309 } 310 return cErr 311 }