github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/mirror-url.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 "path/filepath" 24 "runtime" 25 "strings" 26 "time" 27 28 "github.com/minio/cli" 29 "github.com/minio/pkg/v2/wildcard" 30 ) 31 32 // 33 // * MIRROR ARGS - VALID CASES 34 // ========================= 35 // mirror(d1..., d2) -> []mirror(d1/f, d2/d1/f) 36 37 // checkMirrorSyntax(URLs []string) 38 func checkMirrorSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair) (srcURL, tgtURL string) { 39 if len(cliCtx.Args()) != 2 { 40 showCommandHelpAndExit(cliCtx, 1) // last argument is exit code. 41 } 42 43 // extract URLs. 44 URLs := cliCtx.Args() 45 srcURL = URLs[0] 46 tgtURL = URLs[1] 47 48 if cliCtx.Bool("force") && cliCtx.Bool("remove") { 49 errorIf(errInvalidArgument().Trace(URLs...), "`--force` is deprecated, please use `--overwrite` instead with `--remove` for the same functionality.") 50 } else if cliCtx.Bool("force") { 51 errorIf(errInvalidArgument().Trace(URLs...), "`--force` is deprecated, please use `--overwrite` instead for the same functionality.") 52 } 53 54 _, expandedSourcePath, _ := mustExpandAlias(srcURL) 55 srcClient := newClientURL(expandedSourcePath) 56 _, expandedTargetPath, _ := mustExpandAlias(tgtURL) 57 destClient := newClientURL(expandedTargetPath) 58 59 // Mirror with preserve option on windows 60 // only works for object storage to object storage 61 if runtime.GOOS == "windows" && cliCtx.Bool("a") { 62 if srcClient.Type == fileSystem || destClient.Type == fileSystem { 63 errorIf(errInvalidArgument(), "Preserve functionality on windows support object storage to object storage transfer only.") 64 } 65 } 66 67 /****** Generic rules *******/ 68 if !cliCtx.Bool("watch") && !cliCtx.Bool("active-active") && !cliCtx.Bool("multi-master") { 69 _, srcContent, err := url2Stat(ctx, url2StatOptions{urlStr: srcURL, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false}) 70 if err != nil { 71 fatalIf(err.Trace(srcURL), "Unable to stat source `"+srcURL+"`.") 72 } 73 74 if !srcContent.Type.IsDir() { 75 fatalIf(errInvalidArgument().Trace(srcContent.URL.String(), srcContent.Type.String()), fmt.Sprintf("Source `%s` is not a folder. Only folders are supported by mirror command.", srcURL)) 76 } 77 78 if srcClient.Type == fileSystem && !filepath.IsAbs(srcURL) { 79 origSrcURL := srcURL 80 var e error 81 // Changing relative path to absolute path, if it is a local directory. 82 // Save original in case of error 83 if srcURL, e = filepath.Abs(srcURL); e != nil { 84 srcURL = origSrcURL 85 } 86 } 87 } 88 89 return 90 } 91 92 func matchExcludeOptions(excludeOptions []string, srcSuffix string, typ ClientURLType) bool { 93 // if type is file system, remove leading slash 94 if typ == fileSystem { 95 if strings.HasPrefix(srcSuffix, "/") { 96 srcSuffix = srcSuffix[1:] 97 } else if runtime.GOOS == "windows" && strings.HasPrefix(srcSuffix, `\`) { 98 srcSuffix = srcSuffix[1:] 99 } 100 } 101 for _, pattern := range excludeOptions { 102 if wildcard.Match(pattern, srcSuffix) { 103 return true 104 } 105 } 106 return false 107 } 108 109 func matchExcludeBucketOptions(excludeBuckets []string, srcSuffix string) bool { 110 if strings.HasPrefix(srcSuffix, "/") { 111 srcSuffix = srcSuffix[1:] 112 } else if runtime.GOOS == "windows" && strings.HasPrefix(srcSuffix, `\`) { 113 srcSuffix = srcSuffix[1:] 114 } 115 var bucketName string 116 if runtime.GOOS == "windows" { 117 bucketName = strings.Split(srcSuffix, `\`)[0] 118 } else { 119 bucketName = strings.Split(srcSuffix, "/")[0] 120 } 121 for _, pattern := range excludeBuckets { 122 if wildcard.Match(pattern, bucketName) { 123 return true 124 } 125 } 126 return false 127 } 128 129 func deltaSourceTarget(ctx context.Context, sourceURL, targetURL string, opts mirrorOptions, URLsCh chan<- URLs) { 130 // source and targets are always directories 131 sourceSeparator := string(newClientURL(sourceURL).Separator) 132 if !strings.HasSuffix(sourceURL, sourceSeparator) { 133 sourceURL = sourceURL + sourceSeparator 134 } 135 targetSeparator := string(newClientURL(targetURL).Separator) 136 if !strings.HasSuffix(targetURL, targetSeparator) { 137 targetURL = targetURL + targetSeparator 138 } 139 140 // Extract alias and expanded URL 141 sourceAlias, sourceURL, _ := mustExpandAlias(sourceURL) 142 targetAlias, targetURL, _ := mustExpandAlias(targetURL) 143 144 defer close(URLsCh) 145 146 sourceClnt, err := newClientFromAlias(sourceAlias, sourceURL) 147 if err != nil { 148 URLsCh <- URLs{Error: err.Trace(sourceAlias, sourceURL)} 149 return 150 } 151 152 targetClnt, err := newClientFromAlias(targetAlias, targetURL) 153 if err != nil { 154 URLsCh <- URLs{Error: err.Trace(targetAlias, targetURL)} 155 return 156 } 157 158 // If the passed source URL points to fs, fetch the absolute src path 159 // to correctly calculate targetPath 160 if sourceAlias == "" { 161 tmpSrcURL, e := filepath.Abs(sourceURL) 162 if e == nil { 163 sourceURL = tmpSrcURL 164 } 165 } 166 167 // List both source and target, compare and return values through channel. 168 for diffMsg := range objectDifference(ctx, sourceClnt, targetClnt, opts.isMetadata) { 169 if diffMsg.Error != nil { 170 // Send all errors through the channel 171 URLsCh <- URLs{Error: diffMsg.Error, ErrorCond: differInUnknown} 172 continue 173 } 174 175 srcSuffix := strings.TrimPrefix(diffMsg.FirstURL, sourceURL) 176 // Skip the source object if it matches the Exclude options provided 177 if matchExcludeOptions(opts.excludeOptions, srcSuffix, newClientURL(sourceURL).Type) { 178 continue 179 } 180 181 // Skip the source bucket if it matches the Exclude options provided 182 if matchExcludeBucketOptions(opts.excludeBuckets, srcSuffix) { 183 continue 184 } 185 186 tgtSuffix := strings.TrimPrefix(diffMsg.SecondURL, targetURL) 187 // Skip the target object if it matches the Exclude options provided 188 if matchExcludeOptions(opts.excludeOptions, tgtSuffix, newClientURL(targetURL).Type) { 189 continue 190 } 191 192 // Skip the target bucket if it matches the Exclude options provided 193 if matchExcludeBucketOptions(opts.excludeBuckets, tgtSuffix) { 194 continue 195 } 196 197 if diffMsg.firstContent != nil { 198 var found bool 199 for _, esc := range opts.excludeStorageClasses { 200 if esc == diffMsg.firstContent.StorageClass { 201 found = true 202 break 203 } 204 } 205 if found { 206 continue 207 } 208 } 209 210 switch diffMsg.Diff { 211 case differInNone: 212 // No difference, continue. 213 case differInType: 214 URLsCh <- URLs{Error: errInvalidTarget(diffMsg.SecondURL)} 215 case differInSize, differInMetadata, differInAASourceMTime: 216 if !opts.isOverwrite && !opts.isFake && !opts.activeActive { 217 // Size or time or etag differs but --overwrite not set. 218 URLsCh <- URLs{ 219 Error: errOverWriteNotAllowed(diffMsg.SecondURL), 220 ErrorCond: diffMsg.Diff, 221 } 222 continue 223 } 224 225 sourceSuffix := strings.TrimPrefix(diffMsg.FirstURL, sourceURL) 226 // Either available only in source or size differs and force is set 227 targetPath := urlJoinPath(targetURL, sourceSuffix) 228 sourceContent := diffMsg.firstContent 229 targetContent := &ClientContent{URL: *newClientURL(targetPath)} 230 URLsCh <- URLs{ 231 SourceAlias: sourceAlias, 232 SourceContent: sourceContent, 233 TargetAlias: targetAlias, 234 TargetContent: targetContent, 235 } 236 case differInFirst: 237 // Only in first, always copy. 238 sourceSuffix := strings.TrimPrefix(diffMsg.FirstURL, sourceURL) 239 targetPath := urlJoinPath(targetURL, sourceSuffix) 240 sourceContent := diffMsg.firstContent 241 targetContent := &ClientContent{URL: *newClientURL(targetPath)} 242 URLsCh <- URLs{ 243 SourceAlias: sourceAlias, 244 SourceContent: sourceContent, 245 TargetAlias: targetAlias, 246 TargetContent: targetContent, 247 } 248 case differInSecond: 249 if !opts.isRemove && !opts.isFake { 250 continue 251 } 252 URLsCh <- URLs{ 253 TargetAlias: targetAlias, 254 TargetContent: diffMsg.secondContent, 255 } 256 default: 257 URLsCh <- URLs{ 258 Error: errUnrecognizedDiffType(diffMsg.Diff).Trace(diffMsg.FirstURL, diffMsg.SecondURL), 259 ErrorCond: diffMsg.Diff, 260 } 261 } 262 } 263 } 264 265 type mirrorOptions struct { 266 isFake, isOverwrite, activeActive bool 267 isWatch, isRemove, isMetadata bool 268 isRetriable bool 269 isSummary bool 270 skipErrors bool 271 excludeOptions, excludeStorageClasses, excludeBuckets []string 272 encKeyDB map[string][]prefixSSEPair 273 md5, disableMultipart bool 274 olderThan, newerThan string 275 storageClass string 276 userMetadata map[string]string 277 } 278 279 // Prepares urls that need to be copied or removed based on requested options. 280 func prepareMirrorURLs(ctx context.Context, sourceURL, targetURL string, opts mirrorOptions) <-chan URLs { 281 URLsCh := make(chan URLs) 282 go deltaSourceTarget(ctx, sourceURL, targetURL, opts, URLsCh) 283 return URLsCh 284 }