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  }