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  }