github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/common-methods.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  	"errors"
    23  	"io"
    24  	"net/http"
    25  	"os"
    26  	"path/filepath"
    27  	"regexp"
    28  	"strconv"
    29  	"strings"
    30  	"time"
    31  
    32  	"golang.org/x/net/http/httpguts"
    33  
    34  	"github.com/dustin/go-humanize"
    35  	"github.com/minio/mc/pkg/probe"
    36  	"github.com/minio/minio-go/v7"
    37  	"github.com/minio/minio-go/v7/pkg/encrypt"
    38  	"github.com/minio/pkg/v2/env"
    39  )
    40  
    41  // Check if the passed URL represents a folder. It may or may not exist yet.
    42  // If it exists, we can easily check if it is a folder, if it doesn't exist,
    43  // we can guess if the url is a folder from how it looks.
    44  func isAliasURLDir(ctx context.Context, aliasURL string, keys map[string][]prefixSSEPair, timeRef time.Time, ignoreBucketExists bool) (bool, *ClientContent) {
    45  	// If the target url exists, check if it is a directory
    46  	// and return immediately.
    47  	_, targetContent, err := url2Stat(ctx, url2StatOptions{
    48  		urlStr:                  aliasURL,
    49  		versionID:               "",
    50  		fileAttr:                false,
    51  		encKeyDB:                keys,
    52  		timeRef:                 timeRef,
    53  		isZip:                   false,
    54  		ignoreBucketExistsCheck: ignoreBucketExists,
    55  	})
    56  	if err == nil {
    57  		return targetContent.Type.IsDir(), targetContent
    58  	}
    59  
    60  	_, expandedURL, _ := mustExpandAlias(aliasURL)
    61  
    62  	// Check if targetURL is an FS or S3 aliased url
    63  	if expandedURL == aliasURL {
    64  		// This is an FS url, check if the url has a separator at the end
    65  		return strings.HasSuffix(aliasURL, string(filepath.Separator)), targetContent
    66  	}
    67  
    68  	// This is an S3 url, then:
    69  	//   *) If alias format is specified, return false
    70  	//   *) If alias/bucket is specified, return true
    71  	//   *) If alias/bucket/prefix, check if prefix has
    72  	//       has a trailing slash.
    73  	pathURL := filepath.ToSlash(aliasURL)
    74  	fields := strings.Split(pathURL, "/")
    75  	switch len(fields) {
    76  	// Nothing or alias format
    77  	case 0, 1:
    78  		return false, targetContent
    79  	// alias/bucket format
    80  	case 2:
    81  		return true, targetContent
    82  	} // default case..
    83  
    84  	// alias/bucket/prefix format
    85  	return strings.HasSuffix(pathURL, "/"), targetContent
    86  }
    87  
    88  // getSourceStreamMetadataFromURL gets a reader from URL.
    89  func getSourceStreamMetadataFromURL(ctx context.Context, aliasedURL, versionID string, timeRef time.Time, encKeyDB map[string][]prefixSSEPair, zip bool) (reader io.ReadCloser,
    90  	content *ClientContent, err *probe.Error,
    91  ) {
    92  	alias, urlStrFull, _, err := expandAlias(aliasedURL)
    93  	if err != nil {
    94  		return nil, nil, err.Trace(aliasedURL)
    95  	}
    96  	if !timeRef.IsZero() {
    97  		_, content, err := url2Stat(ctx, url2StatOptions{urlStr: aliasedURL, versionID: "", fileAttr: false, encKeyDB: nil, timeRef: timeRef, isZip: false, ignoreBucketExistsCheck: false})
    98  		if err != nil {
    99  			return nil, nil, err
   100  		}
   101  		versionID = content.VersionID
   102  	}
   103  	return getSourceStream(ctx, alias, urlStrFull, getSourceOpts{
   104  		GetOptions: GetOptions{
   105  			SSE:       getSSE(aliasedURL, encKeyDB[alias]),
   106  			VersionID: versionID,
   107  			Zip:       zip,
   108  		},
   109  	})
   110  }
   111  
   112  type getSourceOpts struct {
   113  	GetOptions
   114  	preserve bool
   115  }
   116  
   117  // getSourceStreamFromURL gets a reader from URL.
   118  func getSourceStreamFromURL(ctx context.Context, urlStr string, encKeyDB map[string][]prefixSSEPair, opts getSourceOpts) (reader io.ReadCloser, err *probe.Error) {
   119  	alias, urlStrFull, _, err := expandAlias(urlStr)
   120  	if err != nil {
   121  		return nil, err.Trace(urlStr)
   122  	}
   123  	opts.SSE = getSSE(urlStr, encKeyDB[alias])
   124  	reader, _, err = getSourceStream(ctx, alias, urlStrFull, opts)
   125  	return reader, err
   126  }
   127  
   128  // Verify if reader is a generic ReaderAt
   129  func isReadAt(reader io.Reader) (ok bool) {
   130  	var v *os.File
   131  	v, ok = reader.(*os.File)
   132  	if ok {
   133  		// Stdin, Stdout and Stderr all have *os.File type
   134  		// which happen to also be io.ReaderAt compatible
   135  		// we need to add special conditions for them to
   136  		// be ignored by this function.
   137  		for _, f := range []string{
   138  			"/dev/stdin",
   139  			"/dev/stdout",
   140  			"/dev/stderr",
   141  		} {
   142  			if f == v.Name() {
   143  				ok = false
   144  				break
   145  			}
   146  		}
   147  	}
   148  	return
   149  }
   150  
   151  // getSourceStream gets a reader from URL.
   152  func getSourceStream(ctx context.Context, alias, urlStr string, opts getSourceOpts) (reader io.ReadCloser, content *ClientContent, err *probe.Error) {
   153  	sourceClnt, err := newClientFromAlias(alias, urlStr)
   154  	if err != nil {
   155  		return nil, nil, err.Trace(alias, urlStr)
   156  	}
   157  
   158  	reader, content, err = sourceClnt.Get(ctx, opts.GetOptions)
   159  	if err != nil {
   160  		return nil, nil, err.Trace(alias, urlStr)
   161  	}
   162  
   163  	return reader, content, nil
   164  }
   165  
   166  // putTargetRetention sets retention headers if any
   167  func putTargetRetention(ctx context.Context, alias, urlStr string, metadata map[string]string) *probe.Error {
   168  	targetClnt, err := newClientFromAlias(alias, urlStr)
   169  	if err != nil {
   170  		return err.Trace(alias, urlStr)
   171  	}
   172  	lockModeStr, ok := metadata[AmzObjectLockMode]
   173  	lockMode := minio.RetentionMode("")
   174  	if ok {
   175  		lockMode = minio.RetentionMode(lockModeStr)
   176  		delete(metadata, AmzObjectLockMode)
   177  	}
   178  
   179  	retainUntilDateStr, ok := metadata[AmzObjectLockRetainUntilDate]
   180  	retainUntilDate := timeSentinel
   181  	if ok {
   182  		delete(metadata, AmzObjectLockRetainUntilDate)
   183  		if t, e := time.Parse(time.RFC3339, retainUntilDateStr); e == nil {
   184  			retainUntilDate = t.UTC()
   185  		}
   186  	}
   187  	if err := targetClnt.PutObjectRetention(ctx, "", lockMode, retainUntilDate, false); err != nil {
   188  		return err.Trace(alias, urlStr)
   189  	}
   190  	return nil
   191  }
   192  
   193  // putTargetStream writes to URL from Reader.
   194  func putTargetStream(ctx context.Context, alias, urlStr, mode, until, legalHold string, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (int64, *probe.Error) {
   195  	targetClnt, err := newClientFromAlias(alias, urlStr)
   196  	if err != nil {
   197  		return 0, err.Trace(alias, urlStr)
   198  	}
   199  
   200  	if mode != "" {
   201  		opts.metadata[AmzObjectLockMode] = mode
   202  	}
   203  	if until != "" {
   204  		opts.metadata[AmzObjectLockRetainUntilDate] = until
   205  	}
   206  	if legalHold != "" {
   207  		opts.metadata[AmzObjectLockLegalHold] = legalHold
   208  	}
   209  
   210  	n, err := targetClnt.Put(ctx, reader, size, progress, opts)
   211  	if err != nil {
   212  		return n, err.Trace(alias, urlStr)
   213  	}
   214  	return n, nil
   215  }
   216  
   217  // putTargetStreamWithURL writes to URL from reader. If length=-1, read until EOF.
   218  func putTargetStreamWithURL(urlStr string, reader io.Reader, size int64, opts PutOptions) (int64, *probe.Error) {
   219  	alias, urlStrFull, _, err := expandAlias(urlStr)
   220  	if err != nil {
   221  		return 0, err.Trace(alias, urlStr)
   222  	}
   223  	contentType := guessURLContentType(urlStr)
   224  	if opts.metadata == nil {
   225  		opts.metadata = map[string]string{}
   226  	}
   227  	opts.metadata["Content-Type"] = contentType
   228  	return putTargetStream(context.Background(), alias, urlStrFull, "", "", "", reader, size, nil, opts)
   229  }
   230  
   231  // copySourceToTargetURL copies to targetURL from source.
   232  func copySourceToTargetURL(ctx context.Context, alias, urlStr, source, sourceVersionID, mode, until, legalHold string, size int64, progress io.Reader, opts CopyOptions) *probe.Error {
   233  	targetClnt, err := newClientFromAlias(alias, urlStr)
   234  	if err != nil {
   235  		return err.Trace(alias, urlStr)
   236  	}
   237  
   238  	opts.versionID = sourceVersionID
   239  	opts.size = size
   240  	opts.metadata[AmzObjectLockMode] = mode
   241  	opts.metadata[AmzObjectLockRetainUntilDate] = until
   242  	opts.metadata[AmzObjectLockLegalHold] = legalHold
   243  
   244  	err = targetClnt.Copy(ctx, source, opts, progress)
   245  	if err != nil {
   246  		return err.Trace(alias, urlStr)
   247  	}
   248  	return nil
   249  }
   250  
   251  func filterMetadata(metadata map[string]string) map[string]string {
   252  	newMetadata := map[string]string{}
   253  	for k, v := range metadata {
   254  		if httpguts.ValidHeaderFieldName(k) && httpguts.ValidHeaderFieldValue(v) {
   255  			newMetadata[k] = v
   256  		}
   257  	}
   258  	for k := range metadata {
   259  		if strings.HasPrefix(http.CanonicalHeaderKey(k), http.CanonicalHeaderKey(serverEncryptionKeyPrefix)) {
   260  			delete(newMetadata, k)
   261  		}
   262  	}
   263  	return newMetadata
   264  }
   265  
   266  // getAllMetadata - returns a map of user defined function
   267  // by combining the usermetadata of object and values passed by attr keyword
   268  func getAllMetadata(ctx context.Context, sourceAlias, sourceURLStr string, srcSSE encrypt.ServerSide, urls URLs) (map[string]string, *probe.Error) {
   269  	metadata := make(map[string]string)
   270  	sourceClnt, err := newClientFromAlias(sourceAlias, sourceURLStr)
   271  	if err != nil {
   272  		return nil, err.Trace(sourceAlias, sourceURLStr)
   273  	}
   274  
   275  	st, err := sourceClnt.Stat(ctx, StatOptions{preserve: true, sse: srcSSE})
   276  	if err != nil {
   277  		return nil, err.Trace(sourceAlias, sourceURLStr)
   278  	}
   279  
   280  	for k, v := range st.Metadata {
   281  		metadata[http.CanonicalHeaderKey(k)] = v
   282  	}
   283  
   284  	for k, v := range urls.TargetContent.UserMetadata {
   285  		metadata[http.CanonicalHeaderKey(k)] = v
   286  	}
   287  
   288  	return filterMetadata(metadata), nil
   289  }
   290  
   291  // uploadSourceToTargetURL - uploads to targetURL from source.
   292  // optionally optimizes copy for object sizes <= 5GiB by using
   293  // server side copy operation.
   294  func uploadSourceToTargetURL(ctx context.Context, uploadOpts uploadSourceToTargetURLOpts) URLs {
   295  	sourceAlias := uploadOpts.urls.SourceAlias
   296  	sourceURL := uploadOpts.urls.SourceContent.URL
   297  	sourceVersion := uploadOpts.urls.SourceContent.VersionID
   298  	targetAlias := uploadOpts.urls.TargetAlias
   299  	targetURL := uploadOpts.urls.TargetContent.URL
   300  	length := uploadOpts.urls.SourceContent.Size
   301  	sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, uploadOpts.urls.SourceContent.URL.Path))
   302  	targetPath := filepath.ToSlash(filepath.Join(targetAlias, uploadOpts.urls.TargetContent.URL.Path))
   303  
   304  	srcSSE := getSSE(sourcePath, uploadOpts.encKeyDB[sourceAlias])
   305  	tgtSSE := getSSE(targetPath, uploadOpts.encKeyDB[targetAlias])
   306  
   307  	var err *probe.Error
   308  	metadata := map[string]string{}
   309  	var mode, until, legalHold string
   310  
   311  	// add object retention fields in metadata for target, if target wants
   312  	// to override defaults from source, usually happens in `cp` command.
   313  	// for the most part source metadata is copied over.
   314  	if uploadOpts.urls.TargetContent.RetentionEnabled {
   315  		m := minio.RetentionMode(strings.ToUpper(uploadOpts.urls.TargetContent.RetentionMode))
   316  		if !m.IsValid() {
   317  			return uploadOpts.urls.WithError(probe.NewError(errors.New("invalid retention mode")).Trace(targetURL.String()))
   318  		}
   319  
   320  		var dur uint64
   321  		var unit minio.ValidityUnit
   322  		dur, unit, err = parseRetentionValidity(uploadOpts.urls.TargetContent.RetentionDuration)
   323  		if err != nil {
   324  			return uploadOpts.urls.WithError(err.Trace(targetURL.String()))
   325  		}
   326  
   327  		mode = uploadOpts.urls.TargetContent.RetentionMode
   328  
   329  		until, err = getRetainUntilDate(dur, unit)
   330  		if err != nil {
   331  			return uploadOpts.urls.WithError(err.Trace(sourceURL.String()))
   332  		}
   333  	}
   334  
   335  	// add object legal hold fields in metadata for target, if target wants
   336  	// to override defaults from source, usually happens in `cp` command.
   337  	// for the most part source metadata is copied over.
   338  	if uploadOpts.urls.TargetContent.LegalHoldEnabled {
   339  		switch minio.LegalHoldStatus(uploadOpts.urls.TargetContent.LegalHold) {
   340  		case minio.LegalHoldDisabled:
   341  		case minio.LegalHoldEnabled:
   342  		default:
   343  			return uploadOpts.urls.WithError(errInvalidArgument().Trace(uploadOpts.urls.TargetContent.LegalHold))
   344  		}
   345  		legalHold = uploadOpts.urls.TargetContent.LegalHold
   346  	}
   347  
   348  	for k, v := range uploadOpts.urls.SourceContent.UserMetadata {
   349  		metadata[http.CanonicalHeaderKey(k)] = v
   350  	}
   351  	for k, v := range uploadOpts.urls.SourceContent.Metadata {
   352  		metadata[http.CanonicalHeaderKey(k)] = v
   353  	}
   354  
   355  	// Optimize for server side copy if the host is same.
   356  	if sourceAlias == targetAlias && !uploadOpts.isZip {
   357  		// preserve new metadata and save existing ones.
   358  		if uploadOpts.preserve {
   359  			currentMetadata, err := getAllMetadata(ctx, sourceAlias, sourceURL.String(), srcSSE, uploadOpts.urls)
   360  			if err != nil {
   361  				return uploadOpts.urls.WithError(err.Trace(sourceURL.String()))
   362  			}
   363  			for k, v := range currentMetadata {
   364  				metadata[k] = v
   365  			}
   366  		}
   367  
   368  		// Get metadata from target content as well
   369  		for k, v := range uploadOpts.urls.TargetContent.Metadata {
   370  			metadata[http.CanonicalHeaderKey(k)] = v
   371  		}
   372  
   373  		// Get userMetadata from target content as well
   374  		for k, v := range uploadOpts.urls.TargetContent.UserMetadata {
   375  			metadata[http.CanonicalHeaderKey(k)] = v
   376  		}
   377  
   378  		sourcePath := filepath.ToSlash(sourceURL.Path)
   379  		if uploadOpts.urls.SourceContent.RetentionEnabled {
   380  			err = putTargetRetention(ctx, targetAlias, targetURL.String(), metadata)
   381  			return uploadOpts.urls.WithError(err.Trace(sourceURL.String()))
   382  		}
   383  
   384  		opts := CopyOptions{
   385  			srcSSE:           srcSSE,
   386  			tgtSSE:           tgtSSE,
   387  			metadata:         filterMetadata(metadata),
   388  			disableMultipart: uploadOpts.urls.DisableMultipart,
   389  			isPreserve:       uploadOpts.preserve,
   390  			storageClass:     uploadOpts.urls.TargetContent.StorageClass,
   391  		}
   392  
   393  		err = copySourceToTargetURL(ctx, targetAlias, targetURL.String(), sourcePath, sourceVersion, mode, until,
   394  			legalHold, length, uploadOpts.progress, opts)
   395  	} else {
   396  		if uploadOpts.urls.SourceContent.RetentionEnabled {
   397  			// preserve new metadata and save existing ones.
   398  			if uploadOpts.preserve {
   399  				currentMetadata, err := getAllMetadata(ctx, sourceAlias, sourceURL.String(), srcSSE, uploadOpts.urls)
   400  				if err != nil {
   401  					return uploadOpts.urls.WithError(err.Trace(sourceURL.String()))
   402  				}
   403  				for k, v := range currentMetadata {
   404  					metadata[k] = v
   405  				}
   406  			}
   407  
   408  			// Get metadata from target content as well
   409  			for k, v := range uploadOpts.urls.TargetContent.Metadata {
   410  				metadata[http.CanonicalHeaderKey(k)] = v
   411  			}
   412  
   413  			// Get userMetadata from target content as well
   414  			for k, v := range uploadOpts.urls.TargetContent.UserMetadata {
   415  				metadata[http.CanonicalHeaderKey(k)] = v
   416  			}
   417  
   418  			err = putTargetRetention(ctx, targetAlias, targetURL.String(), metadata)
   419  			return uploadOpts.urls.WithError(err.Trace(sourceURL.String()))
   420  		}
   421  
   422  		// Proceed with regular stream copy.
   423  		var (
   424  			content *ClientContent
   425  			reader  io.ReadCloser
   426  		)
   427  
   428  		reader, content, err = getSourceStream(ctx, sourceAlias, sourceURL.String(), getSourceOpts{
   429  			GetOptions: GetOptions{
   430  				VersionID: sourceVersion,
   431  				SSE:       srcSSE,
   432  				Zip:       uploadOpts.isZip,
   433  				Preserve:  uploadOpts.preserve,
   434  			},
   435  		})
   436  		if err != nil {
   437  			return uploadOpts.urls.WithError(err.Trace(sourceURL.String()))
   438  		}
   439  		defer reader.Close()
   440  
   441  		if uploadOpts.updateProgressTotal {
   442  			pg, ok := uploadOpts.progress.(*progressBar)
   443  			if ok {
   444  				pg.SetTotal(content.Size)
   445  			}
   446  		}
   447  
   448  		metadata := make(map[string]string, len(content.Metadata))
   449  		for k, v := range content.Metadata {
   450  			metadata[k] = v
   451  		}
   452  
   453  		// Get metadata from target content as well
   454  		for k, v := range uploadOpts.urls.TargetContent.Metadata {
   455  			metadata[http.CanonicalHeaderKey(k)] = v
   456  		}
   457  
   458  		// Get userMetadata from target content as well
   459  		for k, v := range uploadOpts.urls.TargetContent.UserMetadata {
   460  			metadata[http.CanonicalHeaderKey(k)] = v
   461  		}
   462  
   463  		var e error
   464  		var multipartSize uint64
   465  		var multipartThreads int
   466  		var v string
   467  		if uploadOpts.multipartSize == "" {
   468  			v = env.Get("MC_UPLOAD_MULTIPART_SIZE", "")
   469  		} else {
   470  			v = uploadOpts.multipartSize
   471  		}
   472  		if v != "" {
   473  			multipartSize, e = humanize.ParseBytes(v)
   474  			if e != nil {
   475  				return uploadOpts.urls.WithError(probe.NewError(e))
   476  			}
   477  		}
   478  
   479  		if uploadOpts.multipartThreads == "" {
   480  			multipartThreads, e = strconv.Atoi(env.Get("MC_UPLOAD_MULTIPART_THREADS", "4"))
   481  		} else {
   482  			multipartThreads, e = strconv.Atoi(uploadOpts.multipartThreads)
   483  		}
   484  		if e != nil {
   485  			return uploadOpts.urls.WithError(probe.NewError(e))
   486  		}
   487  
   488  		putOpts := PutOptions{
   489  			metadata:         filterMetadata(metadata),
   490  			sse:              tgtSSE,
   491  			storageClass:     uploadOpts.urls.TargetContent.StorageClass,
   492  			md5:              uploadOpts.urls.MD5,
   493  			disableMultipart: uploadOpts.urls.DisableMultipart,
   494  			isPreserve:       uploadOpts.preserve,
   495  			multipartSize:    multipartSize,
   496  			multipartThreads: uint(multipartThreads),
   497  		}
   498  
   499  		if isReadAt(reader) || length == 0 {
   500  			_, err = putTargetStream(ctx, targetAlias, targetURL.String(), mode, until,
   501  				legalHold, reader, length, uploadOpts.progress, putOpts)
   502  		} else {
   503  			_, err = putTargetStream(ctx, targetAlias, targetURL.String(), mode, until,
   504  				legalHold, io.LimitReader(reader, length), length, uploadOpts.progress, putOpts)
   505  		}
   506  	}
   507  	if err != nil {
   508  		return uploadOpts.urls.WithError(err.Trace(sourceURL.String()))
   509  	}
   510  
   511  	return uploadOpts.urls.WithError(nil)
   512  }
   513  
   514  // newClientFromAlias gives a new client interface for matching
   515  // alias entry in the mc config file. If no matching host config entry
   516  // is found, fs client is returned.
   517  func newClientFromAlias(alias, urlStr string) (Client, *probe.Error) {
   518  	alias, _, hostCfg, err := expandAlias(alias)
   519  	if err != nil {
   520  		return nil, err.Trace(alias, urlStr)
   521  	}
   522  
   523  	if hostCfg == nil {
   524  		// No matching host config. So we treat it like a
   525  		// filesystem.
   526  		fsClient, fsErr := fsNew(urlStr)
   527  		if fsErr != nil {
   528  			return nil, fsErr.Trace(alias, urlStr)
   529  		}
   530  		return fsClient, nil
   531  	}
   532  
   533  	s3Config := NewS3Config(alias, urlStr, hostCfg)
   534  	s3Client, err := S3New(s3Config)
   535  	if err != nil {
   536  		return nil, err.Trace(alias, urlStr)
   537  	}
   538  	return s3Client, nil
   539  }
   540  
   541  // urlRgx - verify if aliased url is real URL.
   542  var urlRgx = regexp.MustCompile("^https?://")
   543  
   544  // newClient gives a new client interface
   545  func newClient(aliasedURL string) (Client, *probe.Error) {
   546  	alias, urlStrFull, hostCfg, err := expandAlias(aliasedURL)
   547  	if err != nil {
   548  		return nil, err.Trace(aliasedURL)
   549  	}
   550  	// Verify if the aliasedURL is a real URL, fail in those cases
   551  	// indicating the user to add alias.
   552  	if hostCfg == nil && urlRgx.MatchString(aliasedURL) {
   553  		return nil, errInvalidAliasedURL(aliasedURL).Trace(aliasedURL)
   554  	}
   555  	return newClientFromAlias(alias, urlStrFull)
   556  }
   557  
   558  // ParseForm parses a http.Request form and populates the array
   559  func ParseForm(r *http.Request) error {
   560  	if err := r.ParseForm(); err != nil {
   561  		return err
   562  	}
   563  	for k, v := range r.PostForm {
   564  		if _, ok := r.Form[k]; !ok {
   565  			r.Form[k] = v
   566  		}
   567  	}
   568  	return nil
   569  }
   570  
   571  type uploadSourceToTargetURLOpts struct {
   572  	urls                URLs
   573  	progress            io.Reader
   574  	encKeyDB            map[string][]prefixSSEPair
   575  	preserve, isZip     bool
   576  	multipartSize       string
   577  	multipartThreads    string
   578  	updateProgressTotal bool
   579  }