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 }