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  }