github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/cp-url.go (about) 1 // Copyright (c) 2015-2024 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 "path/filepath" 23 "strings" 24 "time" 25 26 "github.com/minio/mc/pkg/probe" 27 ) 28 29 type copyURLsType uint8 30 31 // NOTE: All the parse rules should reduced to A: Copy(Source, Target). 32 // 33 // * VALID RULES 34 // ======================= 35 // A: copy(f, f) -> copy(f, f) 36 // B: copy(f, d) -> copy(f, d/f) -> []A 37 // C: copy(d1..., d2) -> []copy(f, d2/d1/f) -> []A 38 // D: copy([]f, d) -> []B 39 40 // * INVALID RULES 41 // ========================= 42 // copy(d, f) 43 // copy(d..., f) 44 // copy([](f|d)..., f) 45 46 const ( 47 copyURLsTypeInvalid copyURLsType = iota 48 copyURLsTypeA 49 copyURLsTypeB 50 copyURLsTypeC 51 copyURLsTypeD 52 ) 53 54 // guessCopyURLType guesses the type of clientURL. This approach all allows prepareURL 55 // functions to accurately report failure causes. 56 func guessCopyURLType(ctx context.Context, o prepareCopyURLsOpts) (*copyURLsContent, *probe.Error) { 57 cc := new(copyURLsContent) 58 59 // Extract alias before fiddling with the clientURL. 60 cc.sourceURL = o.sourceURLs[0] 61 cc.sourceAlias, _, _ = mustExpandAlias(cc.sourceURL) 62 // Find alias and expanded clientURL. 63 cc.targetAlias, cc.targetURL, _ = mustExpandAlias(o.targetURL) 64 65 if len(o.sourceURLs) == 1 { // 1 Source, 1 Target 66 var err *probe.Error 67 if !o.isRecursive { 68 _, cc.sourceContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.sourceURL, versionID: o.versionID, fileAttr: false, encKeyDB: o.encKeyDB, timeRef: o.timeRef, isZip: o.isZip, ignoreBucketExistsCheck: false}) 69 } else { 70 _, cc.sourceContent, err = firstURL2Stat(ctx, cc.sourceURL, o.timeRef, o.isZip) 71 } 72 73 if err != nil { 74 cc.copyType = copyURLsTypeInvalid 75 return cc, err 76 } 77 78 // If recursion is ON, it is type C. 79 // If source is a folder, it is Type C. 80 if cc.sourceContent.Type.IsDir() || o.isRecursive { 81 cc.copyType = copyURLsTypeC 82 return cc, nil 83 } 84 85 // If target is a folder, it is Type B. 86 var isDir bool 87 isDir, cc.targetContent = isAliasURLDir(ctx, o.targetURL, o.encKeyDB, o.timeRef, o.ignoreBucketExistsCheck) 88 if isDir { 89 cc.copyType = copyURLsTypeB 90 cc.sourceVersionID = cc.sourceContent.VersionID 91 return cc, nil 92 } 93 94 // else Type A. 95 cc.copyType = copyURLsTypeA 96 cc.sourceVersionID = cc.sourceContent.VersionID 97 return cc, nil 98 } 99 100 var isDir bool 101 // Multiple source args and target is a folder. It is Type D. 102 isDir, cc.targetContent = isAliasURLDir(ctx, o.targetURL, o.encKeyDB, o.timeRef, o.ignoreBucketExistsCheck) 103 if isDir { 104 cc.copyType = copyURLsTypeD 105 return cc, nil 106 } 107 108 cc.copyType = copyURLsTypeInvalid 109 return cc, errInvalidArgument().Trace() 110 } 111 112 // SINGLE SOURCE - Type A: copy(f, f) -> copy(f, f) 113 // prepareCopyURLsTypeA - prepares target and source clientURLs for copying. 114 func prepareCopyURLsTypeA(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) URLs { 115 var err *probe.Error 116 if cc.sourceContent == nil { 117 _, cc.sourceContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.sourceURL, versionID: cc.sourceVersionID, fileAttr: false, encKeyDB: o.encKeyDB, timeRef: time.Time{}, isZip: o.isZip, ignoreBucketExistsCheck: false}) 118 if err != nil { 119 // Source does not exist or insufficient privileges. 120 return URLs{Error: err.Trace(cc.sourceURL)} 121 } 122 } 123 124 if !cc.sourceContent.Type.IsRegular() { 125 // Source is not a regular file 126 return URLs{Error: errInvalidSource(cc.sourceURL).Trace(cc.sourceURL)} 127 } 128 // All OK.. We can proceed. Type A 129 return makeCopyContentTypeA(cc) 130 } 131 132 // prepareCopyContentTypeA - makes CopyURLs content for copying. 133 func makeCopyContentTypeA(cc copyURLsContent) URLs { 134 targetContent := ClientContent{URL: *newClientURL(cc.targetURL)} 135 return URLs{ 136 SourceAlias: cc.sourceAlias, 137 SourceContent: cc.sourceContent, 138 TargetAlias: cc.targetAlias, 139 TargetContent: &targetContent, 140 } 141 } 142 143 // SINGLE SOURCE - Type B: copy(f, d) -> copy(f, d/f) -> A 144 // prepareCopyURLsTypeB - prepares target and source clientURLs for copying. 145 func prepareCopyURLsTypeB(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) URLs { 146 var err *probe.Error 147 if cc.sourceContent == nil { 148 _, cc.sourceContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.sourceURL, versionID: cc.sourceVersionID, fileAttr: false, encKeyDB: o.encKeyDB, timeRef: time.Time{}, isZip: o.isZip, ignoreBucketExistsCheck: o.ignoreBucketExistsCheck}) 149 if err != nil { 150 // Source does not exist or insufficient privileges. 151 return URLs{Error: err.Trace(cc.sourceURL)} 152 } 153 } 154 155 if !cc.sourceContent.Type.IsRegular() { 156 if cc.sourceContent.Type.IsDir() { 157 return URLs{Error: errSourceIsDir(cc.sourceURL).Trace(cc.sourceURL)} 158 } 159 // Source is not a regular file. 160 return URLs{Error: errInvalidSource(cc.sourceURL).Trace(cc.sourceURL)} 161 } 162 163 if cc.targetContent == nil { 164 _, cc.targetContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.targetURL, versionID: "", fileAttr: false, encKeyDB: o.encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: o.ignoreBucketExistsCheck}) 165 if err == nil { 166 if !cc.targetContent.Type.IsDir() { 167 return URLs{Error: errInvalidTarget(cc.targetURL).Trace(cc.targetURL)} 168 } 169 } 170 } 171 // All OK.. We can proceed. Type B: source is a file, target is a folder and exists. 172 return makeCopyContentTypeB(cc) 173 } 174 175 // makeCopyContentTypeB - CopyURLs content for copying. 176 func makeCopyContentTypeB(cc copyURLsContent) URLs { 177 // All OK.. We can proceed. Type B: source is a file, target is a folder and exists. 178 targetURLParse := newClientURL(cc.targetURL) 179 targetURLParse.Path = filepath.ToSlash(filepath.Join(targetURLParse.Path, filepath.Base(cc.sourceContent.URL.Path))) 180 cc.targetURL = targetURLParse.String() 181 return makeCopyContentTypeA(cc) 182 } 183 184 // SINGLE SOURCE - Type C: copy(d1..., d2) -> []copy(d1/f, d1/d2/f) -> []A 185 // prepareCopyRecursiveURLTypeC - prepares target and source clientURLs for copying. 186 func prepareCopyURLsTypeC(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) <-chan URLs { 187 copyURLsCh := make(chan URLs, 1) 188 189 returnErrorAndCloseChannel := func(err *probe.Error) chan URLs { 190 copyURLsCh <- URLs{Error: err} 191 close(copyURLsCh) 192 return copyURLsCh 193 } 194 195 c, err := newClient(cc.sourceURL) 196 if err != nil { 197 return returnErrorAndCloseChannel(err.Trace(cc.sourceURL)) 198 } 199 200 if cc.targetContent == nil { 201 _, cc.targetContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.targetURL, versionID: "", fileAttr: false, encKeyDB: o.encKeyDB, timeRef: time.Time{}, isZip: o.isZip, ignoreBucketExistsCheck: false}) 202 if err == nil { 203 if !cc.targetContent.Type.IsDir() { 204 return returnErrorAndCloseChannel(errTargetIsNotDir(cc.targetURL).Trace(cc.targetURL)) 205 } 206 } 207 } 208 209 if cc.sourceContent == nil { 210 _, cc.sourceContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.sourceURL, versionID: "", fileAttr: false, encKeyDB: o.encKeyDB, timeRef: time.Time{}, isZip: o.isZip, ignoreBucketExistsCheck: false}) 211 if err != nil { 212 return returnErrorAndCloseChannel(err.Trace(cc.sourceURL)) 213 } 214 } 215 216 if cc.sourceContent.Type.IsDir() { 217 // Require --recursive flag if we are copying a directory 218 if !o.isRecursive { 219 return returnErrorAndCloseChannel(errRequiresRecursive(cc.sourceURL).Trace(cc.sourceURL)) 220 } 221 222 // Check if we are going to copy a directory into itself 223 if isURLContains(cc.sourceURL, cc.targetURL, string(c.GetURL().Separator)) { 224 return returnErrorAndCloseChannel(errCopyIntoSelf(cc.sourceURL).Trace(cc.targetURL)) 225 } 226 } 227 228 go func(sourceClient Client, cc copyURLsContent, o prepareCopyURLsOpts, copyURLsCh chan URLs) { 229 defer close(copyURLsCh) 230 231 for sourceContent := range sourceClient.List(ctx, ListOptions{Recursive: o.isRecursive, TimeRef: o.timeRef, ShowDir: DirNone, ListZip: o.isZip}) { 232 if sourceContent.Err != nil { 233 // Listing failed. 234 copyURLsCh <- URLs{Error: sourceContent.Err.Trace(sourceClient.GetURL().String())} 235 continue 236 } 237 238 if !sourceContent.Type.IsRegular() { 239 // Source is not a regular file. Skip it for copy. 240 continue 241 } 242 243 // Clone cc 244 newCC := cc 245 newCC.sourceContent = sourceContent 246 // All OK.. We can proceed. Type B: source is a file, target is a folder and exists. 247 copyURLsCh <- makeCopyContentTypeC(newCC, sourceClient.GetURL()) 248 } 249 }(c, cc, o, copyURLsCh) 250 251 return copyURLsCh 252 } 253 254 // makeCopyContentTypeC - CopyURLs content for copying. 255 func makeCopyContentTypeC(cc copyURLsContent, sourceClientURL ClientURL) URLs { 256 newSourceURL := cc.sourceContent.URL 257 pathSeparatorIndex := strings.LastIndex(sourceClientURL.Path, string(sourceClientURL.Separator)) 258 newSourceSuffix := filepath.ToSlash(newSourceURL.Path) 259 if pathSeparatorIndex > 1 { 260 sourcePrefix := filepath.ToSlash(sourceClientURL.Path[:pathSeparatorIndex]) 261 newSourceSuffix = strings.TrimPrefix(newSourceSuffix, sourcePrefix) 262 } 263 newTargetURL := urlJoinPath(cc.targetURL, newSourceSuffix) 264 cc.targetURL = newTargetURL 265 return makeCopyContentTypeA(cc) 266 } 267 268 // MULTI-SOURCE - Type D: copy([](f|d...), d) -> []B 269 // prepareCopyURLsTypeE - prepares target and source clientURLs for copying. 270 func prepareCopyURLsTypeD(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) <-chan URLs { 271 copyURLsCh := make(chan URLs, 1) 272 copyURLsFilterCh := make(chan URLs, 1) 273 274 go func(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) { 275 defer close(copyURLsFilterCh) 276 277 for _, sourceURL := range o.sourceURLs { 278 // Clone CC 279 newCC := cc 280 newCC.sourceURL = sourceURL 281 282 for cpURLs := range prepareCopyURLsTypeC(ctx, newCC, o) { 283 copyURLsFilterCh <- cpURLs 284 } 285 } 286 }(ctx, cc, o) 287 288 go func() { 289 defer close(copyURLsCh) 290 filter := make(map[string]struct{}) 291 for cpURLs := range copyURLsFilterCh { 292 if cpURLs.Error != nil || cpURLs.TargetContent == nil { 293 copyURLsCh <- cpURLs 294 continue 295 } 296 297 url := cpURLs.TargetContent.URL.String() 298 _, ok := filter[url] 299 if !ok { 300 filter[url] = struct{}{} 301 copyURLsCh <- cpURLs 302 } 303 } 304 }() 305 306 return copyURLsCh 307 } 308 309 type prepareCopyURLsOpts struct { 310 sourceURLs []string 311 targetURL string 312 isRecursive bool 313 encKeyDB map[string][]prefixSSEPair 314 olderThan, newerThan string 315 timeRef time.Time 316 versionID string 317 isZip bool 318 ignoreBucketExistsCheck bool 319 } 320 321 type copyURLsContent struct { 322 targetContent *ClientContent 323 targetAlias string 324 targetURL string 325 sourceContent *ClientContent 326 sourceAlias string 327 sourceURL string 328 copyType copyURLsType 329 sourceVersionID string 330 } 331 332 // prepareCopyURLs - prepares target and source clientURLs for copying. 333 func prepareCopyURLs(ctx context.Context, o prepareCopyURLsOpts) chan URLs { 334 copyURLsCh := make(chan URLs) 335 go func(o prepareCopyURLsOpts) { 336 defer close(copyURLsCh) 337 copyURLsContent, err := guessCopyURLType(ctx, o) 338 if err != nil { 339 copyURLsCh <- URLs{Error: errUnableToGuess().Trace(o.sourceURLs...)} 340 return 341 } 342 343 switch copyURLsContent.copyType { 344 case copyURLsTypeA: 345 copyURLsCh <- prepareCopyURLsTypeA(ctx, *copyURLsContent, o) 346 case copyURLsTypeB: 347 copyURLsCh <- prepareCopyURLsTypeB(ctx, *copyURLsContent, o) 348 case copyURLsTypeC: 349 for cURLs := range prepareCopyURLsTypeC(ctx, *copyURLsContent, o) { 350 copyURLsCh <- cURLs 351 } 352 case copyURLsTypeD: 353 for cURLs := range prepareCopyURLsTypeD(ctx, *copyURLsContent, o) { 354 copyURLsCh <- cURLs 355 } 356 default: 357 copyURLsCh <- URLs{Error: errInvalidArgument().Trace(o.sourceURLs...)} 358 } 359 }(o) 360 361 finalCopyURLsCh := make(chan URLs) 362 go func() { 363 defer close(finalCopyURLsCh) 364 for cpURLs := range copyURLsCh { 365 if cpURLs.Error != nil { 366 finalCopyURLsCh <- cpURLs 367 continue 368 } 369 // Skip objects older than --older-than parameter if specified 370 if o.olderThan != "" && isOlder(cpURLs.SourceContent.Time, o.olderThan) { 371 continue 372 } 373 374 // Skip objects newer than --newer-than parameter if specified 375 if o.newerThan != "" && isNewer(cpURLs.SourceContent.Time, o.newerThan) { 376 continue 377 } 378 379 finalCopyURLsCh <- cpURLs 380 } 381 }() 382 383 return finalCopyURLsCh 384 }