github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/diff-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  	"strings"
    24  	"time"
    25  
    26  	"github.com/fatih/color"
    27  	"github.com/minio/cli"
    28  	json "github.com/minio/colorjson"
    29  	"github.com/minio/mc/pkg/probe"
    30  	"github.com/minio/pkg/v2/console"
    31  )
    32  
    33  // diff specific flags.
    34  var (
    35  	diffFlags = []cli.Flag{}
    36  )
    37  
    38  // Compute differences in object name, size, and date between two buckets.
    39  var diffCmd = cli.Command{
    40  	Name:         "diff",
    41  	Usage:        "list differences in object name, size, and date between two buckets",
    42  	Action:       mainDiff,
    43  	OnUsageError: onUsageError,
    44  	Before:       setGlobalsFromContext,
    45  	Flags:        append(diffFlags, globalFlags...),
    46  	CustomHelpTemplate: `NAME:
    47    {{.HelpName}} - {{.Usage}}
    48  
    49  USAGE:
    50    {{.HelpName}} [FLAGS] SOURCE TARGET
    51  
    52  FLAGS:
    53    {{range .VisibleFlags}}{{.}}
    54    {{end}}
    55  DESCRIPTION:
    56    Diff only calculates differences in object name, size and time. It *DOES NOT* compare objects' contents.
    57  
    58  LEGEND:
    59    < - object is only in source.
    60    > - object is only in destination.
    61    ! - newer object is in source.
    62  
    63  EXAMPLES:
    64    1. Compare a local folder with a folder on Amazon S3 cloud storage.
    65       {{.Prompt}} {{.HelpName}} ~/Photos s3/mybucket/Photos
    66  
    67    2. Compare two folders on a local filesystem.
    68       {{.Prompt}} {{.HelpName}} ~/Photos /Media/Backup/Photos
    69  `,
    70  }
    71  
    72  // diffMessage json container for diff messages
    73  type diffMessage struct {
    74  	Status        string       `json:"status"`
    75  	FirstURL      string       `json:"first"`
    76  	SecondURL     string       `json:"second"`
    77  	Diff          differType   `json:"diff"`
    78  	Error         *probe.Error `json:"error,omitempty"`
    79  	firstContent  *ClientContent
    80  	secondContent *ClientContent
    81  }
    82  
    83  // String colorized diff message
    84  func (d diffMessage) String() string {
    85  	msg := ""
    86  	switch d.Diff {
    87  	case differInFirst:
    88  		msg = console.Colorize("DiffOnlyInFirst", "< "+d.FirstURL)
    89  	case differInSecond:
    90  		msg = console.Colorize("DiffOnlyInSecond", "> "+d.SecondURL)
    91  	case differInType:
    92  		msg = console.Colorize("DiffType", "! "+d.SecondURL)
    93  	case differInSize:
    94  		msg = console.Colorize("DiffSize", "! "+d.SecondURL)
    95  	case differInMetadata:
    96  		msg = console.Colorize("DiffMetadata", "! "+d.SecondURL)
    97  	case differInAASourceMTime:
    98  		msg = console.Colorize("DiffMMSourceMTime", "! "+d.SecondURL)
    99  	case differInNone:
   100  		msg = console.Colorize("DiffInNone", "= "+d.FirstURL)
   101  	default:
   102  		fatalIf(errDummy().Trace(d.FirstURL, d.SecondURL),
   103  			"Unhandled difference between `"+d.FirstURL+"` and `"+d.SecondURL+"`.")
   104  	}
   105  	return msg
   106  }
   107  
   108  // JSON jsonified diff message
   109  func (d diffMessage) JSON() string {
   110  	d.Status = "success"
   111  	diffJSONBytes, e := json.MarshalIndent(d, "", " ")
   112  	fatalIf(probe.NewError(e),
   113  		"Unable to marshal diff message `"+d.FirstURL+"`, `"+d.SecondURL+"` and `"+fmt.Sprint(d.Diff)+"`.")
   114  	return string(diffJSONBytes)
   115  }
   116  
   117  func checkDiffSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair) {
   118  	if len(cliCtx.Args()) != 2 {
   119  		showCommandHelpAndExit(cliCtx, 1) // last argument is exit code
   120  	}
   121  	for _, arg := range cliCtx.Args() {
   122  		if strings.TrimSpace(arg) == "" {
   123  			fatalIf(errInvalidArgument().Trace(cliCtx.Args()...), "Unable to validate empty argument.")
   124  		}
   125  	}
   126  	URLs := cliCtx.Args()
   127  	firstURL := URLs[0]
   128  	secondURL := URLs[1]
   129  
   130  	// Diff only works between two directories, verify them below.
   131  
   132  	// Verify if firstURL is accessible.
   133  	_, firstContent, err := url2Stat(ctx, url2StatOptions{urlStr: firstURL, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false})
   134  	if err != nil {
   135  		fatalIf(err.Trace(firstURL), fmt.Sprintf("Unable to stat '%s'.", firstURL))
   136  	}
   137  
   138  	// Verify if its a directory.
   139  	if !firstContent.Type.IsDir() {
   140  		fatalIf(errInvalidArgument().Trace(firstURL), fmt.Sprintf("`%s` is not a folder.", firstURL))
   141  	}
   142  
   143  	// Verify if secondURL is accessible.
   144  	_, secondContent, err := url2Stat(ctx, url2StatOptions{urlStr: secondURL, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false})
   145  	if err != nil {
   146  		// Destination doesn't exist is okay.
   147  		if _, ok := err.ToGoError().(ObjectMissing); !ok {
   148  			fatalIf(err.Trace(secondURL), fmt.Sprintf("Unable to stat '%s'.", secondURL))
   149  		}
   150  	}
   151  
   152  	// Verify if its a directory.
   153  	if err == nil && !secondContent.Type.IsDir() {
   154  		fatalIf(errInvalidArgument().Trace(secondURL), fmt.Sprintf("`%s` is not a folder.", secondURL))
   155  	}
   156  }
   157  
   158  // doDiffMain runs the diff.
   159  func doDiffMain(ctx context.Context, firstURL, secondURL string) error {
   160  	// Source and targets are always directories
   161  	sourceSeparator := string(newClientURL(firstURL).Separator)
   162  	if !strings.HasSuffix(firstURL, sourceSeparator) {
   163  		firstURL = firstURL + sourceSeparator
   164  	}
   165  	targetSeparator := string(newClientURL(secondURL).Separator)
   166  	if !strings.HasSuffix(secondURL, targetSeparator) {
   167  		secondURL = secondURL + targetSeparator
   168  	}
   169  
   170  	// Expand aliased urls.
   171  	firstAlias, firstURL, _ := mustExpandAlias(firstURL)
   172  	secondAlias, secondURL, _ := mustExpandAlias(secondURL)
   173  
   174  	firstClient, err := newClientFromAlias(firstAlias, firstURL)
   175  	if err != nil {
   176  		fatalIf(err.Trace(firstAlias, firstURL, secondAlias, secondURL),
   177  			fmt.Sprintf("Failed to diff '%s' and '%s'", firstURL, secondURL))
   178  	}
   179  
   180  	secondClient, err := newClientFromAlias(secondAlias, secondURL)
   181  	if err != nil {
   182  		fatalIf(err.Trace(firstAlias, firstURL, secondAlias, secondURL),
   183  			fmt.Sprintf("Failed to diff '%s' and '%s'", firstURL, secondURL))
   184  	}
   185  
   186  	// Diff first and second urls.
   187  	for diffMsg := range objectDifference(ctx, firstClient, secondClient, true) {
   188  		if diffMsg.Error != nil {
   189  			errorIf(diffMsg.Error, "Unable to calculate objects difference.")
   190  			// Ignore error and proceed to next object.
   191  			continue
   192  		}
   193  		printMsg(diffMsg)
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  // mainDiff main for 'diff'.
   200  func mainDiff(cliCtx *cli.Context) error {
   201  	ctx, cancelDiff := context.WithCancel(globalContext)
   202  	defer cancelDiff()
   203  
   204  	// Parse encryption keys per command.
   205  	encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
   206  	fatalIf(err, "Unable to parse encryption keys.")
   207  
   208  	// check 'diff' cli arguments.
   209  	checkDiffSyntax(ctx, cliCtx, encKeyDB)
   210  
   211  	// Additional command specific theme customization.
   212  	console.SetColor("DiffMessage", color.New(color.FgGreen, color.Bold))
   213  	console.SetColor("DiffOnlyInFirst", color.New(color.FgRed))
   214  	console.SetColor("DiffOnlyInSecond", color.New(color.FgGreen))
   215  	console.SetColor("DiffType", color.New(color.FgMagenta))
   216  	console.SetColor("DiffSize", color.New(color.FgYellow, color.Bold))
   217  	console.SetColor("DiffMetadata", color.New(color.FgYellow, color.Bold))
   218  	console.SetColor("DiffMMSourceMTime", color.New(color.FgYellow, color.Bold))
   219  
   220  	URLs := cliCtx.Args()
   221  	firstURL := URLs.Get(0)
   222  	secondURL := URLs.Get(1)
   223  
   224  	return doDiffMain(ctx, firstURL, secondURL)
   225  }