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  }