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 }