github.com/pelicanplatform/pelican@v1.0.5/client/handle_http.go (about) 1 /*************************************************************** 2 * 3 * Copyright (C) 2023, University of Nebraska-Lincoln 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); you 6 * may not use this file except in compliance with the License. You may 7 * obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 ***************************************************************/ 18 19 package client 20 21 import ( 22 "context" 23 "fmt" 24 "io" 25 "net" 26 "net/http" 27 "net/http/httputil" 28 "net/url" 29 "os" 30 "path" 31 "regexp" 32 "strconv" 33 "strings" 34 "sync" 35 "sync/atomic" 36 "syscall" 37 "time" 38 39 grab "github.com/opensaucerer/grab/v3" 40 "github.com/pkg/errors" 41 log "github.com/sirupsen/logrus" 42 "github.com/studio-b12/gowebdav" 43 "github.com/vbauerster/mpb/v8" 44 "github.com/vbauerster/mpb/v8/decor" 45 46 "github.com/pelicanplatform/pelican/config" 47 "github.com/pelicanplatform/pelican/namespaces" 48 "github.com/pelicanplatform/pelican/param" 49 ) 50 51 var progressContainer = mpb.New() 52 53 type StoppedTransferError struct { 54 Err string 55 } 56 57 func (e *StoppedTransferError) Error() string { 58 return e.Err 59 } 60 61 type HttpErrResp struct { 62 Code int 63 Err string 64 } 65 66 func (e *HttpErrResp) Error() string { 67 return e.Err 68 } 69 70 // SlowTransferError is an error that is returned when a transfer takes longer than the configured timeout 71 type SlowTransferError struct { 72 BytesTransferred int64 73 BytesPerSecond int64 74 BytesTotal int64 75 Duration time.Duration 76 } 77 78 func (e *SlowTransferError) Error() string { 79 return "cancelled transfer, too slow. Detected speed: " + 80 ByteCountSI(e.BytesPerSecond) + 81 "/s, total transferred: " + 82 ByteCountSI(e.BytesTransferred) + 83 ", total transfer time: " + 84 e.Duration.String() 85 } 86 87 func (e *SlowTransferError) Is(target error) bool { 88 _, ok := target.(*SlowTransferError) 89 return ok 90 } 91 92 type FileDownloadError struct { 93 Text string 94 Err error 95 } 96 97 func (e *FileDownloadError) Error() string { 98 return e.Text 99 } 100 101 func (e *FileDownloadError) Unwrap() error { 102 return e.Err 103 } 104 105 // Determines whether or not we can interact with the site HTTP proxy 106 func IsProxyEnabled() bool { 107 if _, isSet := os.LookupEnv("http_proxy"); !isSet { 108 return false 109 } 110 if param.Client_DisableHttpProxy.GetBool() { 111 return false 112 } 113 return true 114 } 115 116 // Determine whether we are allowed to skip the proxy as a fallback 117 func CanDisableProxy() bool { 118 return !param.Client_DisableProxyFallback.GetBool() 119 } 120 121 // ConnectionSetupError is an error that is returned when a connection to the remote server fails 122 type ConnectionSetupError struct { 123 URL string 124 Err error 125 } 126 127 func (e *ConnectionSetupError) Error() string { 128 if e.Err != nil { 129 if len(e.URL) > 0 { 130 return "failed connection setup to " + e.URL + ": " + e.Err.Error() 131 } else { 132 return "failed connection setup: " + e.Err.Error() 133 } 134 } else { 135 return "Connection to remote server failed" 136 } 137 138 } 139 140 func (e *ConnectionSetupError) Unwrap() error { 141 return e.Err 142 } 143 144 func (e *ConnectionSetupError) Is(target error) bool { 145 _, ok := target.(*ConnectionSetupError) 146 return ok 147 } 148 149 // HasPort test the host if it includes a port 150 func HasPort(host string) bool { 151 var checkPort = regexp.MustCompile("^.*:[0-9]+$") 152 return checkPort.MatchString(host) 153 } 154 155 type TransferDetails struct { 156 // Url is the url.URL of the cache and port 157 Url url.URL 158 159 // Proxy specifies if a proxy should be used 160 Proxy bool 161 162 // Specifies the pack option in the transfer URL 163 PackOption string 164 } 165 166 // NewTransferDetails creates the TransferDetails struct with the given cache 167 func NewTransferDetails(cache namespaces.Cache, opts TransferDetailsOptions) []TransferDetails { 168 details := make([]TransferDetails, 0) 169 var cacheEndpoint string 170 if opts.NeedsToken { 171 cacheEndpoint = cache.AuthEndpoint 172 } else { 173 cacheEndpoint = cache.Endpoint 174 } 175 176 // Form the URL 177 cacheURL, err := url.Parse(cacheEndpoint) 178 if err != nil { 179 log.Errorln("Failed to parse cache:", cache, "error:", err) 180 return nil 181 } 182 if cacheURL.Host == "" { 183 // Assume the cache is just a hostname 184 cacheURL.Host = cacheEndpoint 185 cacheURL.Path = "" 186 cacheURL.Scheme = "" 187 cacheURL.Opaque = "" 188 } 189 log.Debugf("Parsed Cache: %s\n", cacheURL.String()) 190 if opts.NeedsToken { 191 cacheURL.Scheme = "https" 192 if !HasPort(cacheURL.Host) { 193 // Add port 8444 and 8443 194 cacheURL.Host += ":8444" 195 details = append(details, TransferDetails{ 196 Url: *cacheURL, 197 Proxy: false, 198 PackOption: opts.PackOption, 199 }) 200 // Strip the port off and add 8443 201 cacheURL.Host = cacheURL.Host[:len(cacheURL.Host)-5] + ":8443" 202 } 203 // Whether port is specified or not, add a transfer without proxy 204 details = append(details, TransferDetails{ 205 Url: *cacheURL, 206 Proxy: false, 207 PackOption: opts.PackOption, 208 }) 209 } else { 210 cacheURL.Scheme = "http" 211 if !HasPort(cacheURL.Host) { 212 cacheURL.Host += ":8000" 213 } 214 isProxyEnabled := IsProxyEnabled() 215 details = append(details, TransferDetails{ 216 Url: *cacheURL, 217 Proxy: isProxyEnabled, 218 PackOption: opts.PackOption, 219 }) 220 if isProxyEnabled && CanDisableProxy() { 221 details = append(details, TransferDetails{ 222 Url: *cacheURL, 223 Proxy: false, 224 PackOption: opts.PackOption, 225 }) 226 } 227 } 228 229 return details 230 } 231 232 type TransferResults struct { 233 Error error 234 Downloaded int64 235 } 236 237 type TransferDetailsOptions struct { 238 NeedsToken bool 239 PackOption string 240 } 241 242 type CacheInterface interface{} 243 244 func GenerateTransferDetailsUsingCache(cache CacheInterface, opts TransferDetailsOptions) []TransferDetails { 245 if directorCache, ok := cache.(namespaces.DirectorCache); ok { 246 return NewTransferDetailsUsingDirector(directorCache, opts) 247 } else if cache, ok := cache.(namespaces.Cache); ok { 248 return NewTransferDetails(cache, opts) 249 } 250 return nil 251 } 252 253 func download_http(sourceUrl *url.URL, destination string, payload *payloadStruct, namespace namespaces.Namespace, recursive bool, tokenName string, OSDFDirectorUrl string) (bytesTransferred int64, err error) { 254 255 // First, create a handler for any panics that occur 256 defer func() { 257 if r := recover(); r != nil { 258 log.Errorln("Panic occurred in download_http:", r) 259 ret := fmt.Sprintf("Unrecoverable error (panic) occurred in download_http: %v", r) 260 err = errors.New(ret) 261 bytesTransferred = 0 262 263 // Attempt to add the panic to the error accumulator 264 AddError(errors.New(ret)) 265 } 266 }() 267 268 packOption := sourceUrl.Query().Get("pack") 269 if packOption != "" { 270 log.Debugln("Will use unpack option value", packOption) 271 } 272 sourceUrl = &url.URL{Path: sourceUrl.Path} 273 274 var token string 275 if namespace.UseTokenOnRead { 276 var err error 277 token, err = getToken(sourceUrl, namespace, false, tokenName) 278 if err != nil { 279 log.Errorln("Failed to get token though required to read from this namespace:", err) 280 return 0, err 281 } 282 } 283 284 // Check the env var "USE_OSDF_DIRECTOR" and decide if ordered caches should come from director 285 var transfers []TransferDetails 286 var files []string 287 closestNamespaceCaches, err := GetCachesFromNamespace(namespace, OSDFDirectorUrl != "") 288 if err != nil { 289 log.Errorln("Failed to get namespaced caches (treated as non-fatal):", err) 290 } 291 292 log.Debugln("Matched caches:", closestNamespaceCaches) 293 294 // Make sure we only try as many caches as we have 295 cachesToTry := CachesToTry 296 if cachesToTry > len(closestNamespaceCaches) { 297 cachesToTry = len(closestNamespaceCaches) 298 } 299 log.Debugln("Trying the caches:", closestNamespaceCaches[:cachesToTry]) 300 301 if recursive { 302 var err error 303 files, err = walkDavDir(sourceUrl, namespace, token, "", false) 304 if err != nil { 305 log.Errorln("Error from walkDavDir", err) 306 return 0, err 307 } 308 } else { 309 files = append(files, sourceUrl.Path) 310 } 311 312 for _, cache := range closestNamespaceCaches[:cachesToTry] { 313 // Parse the cache URL 314 log.Debugln("Cache:", cache) 315 td := TransferDetailsOptions{ 316 NeedsToken: namespace.ReadHTTPS || namespace.UseTokenOnRead, 317 PackOption: packOption, 318 } 319 transfers = append(transfers, GenerateTransferDetailsUsingCache(cache, td)...) 320 } 321 322 if len(transfers) > 0 { 323 log.Debugln("Transfers:", transfers[0].Url.Opaque) 324 } else { 325 log.Debugln("No transfers possible as no caches are found") 326 return 0, errors.New("No transfers possible as no caches are found") 327 } 328 // Create the wait group and the transfer files 329 var wg sync.WaitGroup 330 331 workChan := make(chan string) 332 results := make(chan TransferResults, len(files)) 333 //tf := TransferFiles{files: files} 334 335 if ObjectClientOptions.Recursive && ObjectClientOptions.ProgressBars { 336 log.SetOutput(progressContainer) 337 } 338 // Start the workers 339 for i := 1; i <= 5; i++ { 340 wg.Add(1) 341 go startDownloadWorker(sourceUrl.Path, destination, token, transfers, &wg, workChan, results) 342 } 343 344 // For each file, send it to the worker 345 for _, file := range files { 346 workChan <- file 347 } 348 close(workChan) 349 350 // Wait for all the transfers to complete 351 wg.Wait() 352 353 var downloaded int64 354 var downloadError error = nil 355 // Every transfer should send a TransferResults to the results channel 356 for i := 0; i < len(files); i++ { 357 select { 358 case result := <-results: 359 downloaded += result.Downloaded 360 if result.Error != nil { 361 downloadError = result.Error 362 } 363 default: 364 // Didn't get a result, that's weird 365 downloadError = errors.New("failed to get outputs from one of the transfers") 366 } 367 } 368 // Make sure to close the progressContainer after all download complete 369 if ObjectClientOptions.Recursive && ObjectClientOptions.ProgressBars { 370 progressContainer.Wait() 371 log.SetOutput(os.Stdout) 372 } 373 return downloaded, downloadError 374 375 } 376 377 func startDownloadWorker(source string, destination string, token string, transfers []TransferDetails, wg *sync.WaitGroup, workChan <-chan string, results chan<- TransferResults) { 378 379 defer wg.Done() 380 var success bool 381 for file := range workChan { 382 // Remove the source from the file path 383 newFile := strings.Replace(file, source, "", 1) 384 finalDest := path.Join(destination, newFile) 385 directory := path.Dir(finalDest) 386 var downloaded int64 387 err := os.MkdirAll(directory, 0700) 388 if err != nil { 389 results <- TransferResults{Error: errors.New("Failed to make directory:" + directory)} 390 continue 391 } 392 for _, transfer := range transfers { 393 transfer.Url.Path = file 394 log.Debugln("Constructed URL:", transfer.Url.String()) 395 if downloaded, err = DownloadHTTP(transfer, finalDest, token); err != nil { 396 log.Debugln("Failed to download:", err) 397 var ope *net.OpError 398 var cse *ConnectionSetupError 399 errorString := "Failed to download from " + transfer.Url.Hostname() + ":" + 400 transfer.Url.Port() + " " 401 if errors.As(err, &ope) && ope.Op == "proxyconnect" { 402 log.Debugln(ope) 403 AddrString, _ := os.LookupEnv("http_proxy") 404 if ope.Addr != nil { 405 AddrString = " " + ope.Addr.String() 406 } 407 errorString += "due to proxy " + AddrString + " error: " + ope.Unwrap().Error() 408 } else if errors.As(err, &cse) { 409 errorString += "+ proxy=" + strconv.FormatBool(transfer.Proxy) + ": " 410 if sce, ok := cse.Unwrap().(grab.StatusCodeError); ok { 411 errorString += sce.Error() 412 } else { 413 errorString += err.Error() 414 } 415 } else { 416 errorString += "+ proxy=" + strconv.FormatBool(transfer.Proxy) + 417 ": " + err.Error() 418 } 419 AddError(&FileDownloadError{errorString, err}) 420 continue 421 } else { 422 log.Debugln("Downloaded bytes:", downloaded) 423 success = true 424 break 425 } 426 427 } 428 if !success { 429 log.Debugln("Failed to download with HTTP") 430 results <- TransferResults{Error: errors.New("failed to download with HTTP")} 431 return 432 } else { 433 results <- TransferResults{ 434 Downloaded: downloaded, 435 Error: nil, 436 } 437 } 438 } 439 } 440 441 func parseTransferStatus(status string) (int, string) { 442 parts := strings.SplitN(status, ": ", 2) 443 if len(parts) != 2 { 444 return 0, "" 445 } 446 447 statusCode, err := strconv.Atoi(strings.TrimSpace(parts[0])) 448 if err != nil { 449 return 0, "" 450 } 451 452 return statusCode, strings.TrimSpace(parts[1]) 453 } 454 455 // DownloadHTTP - Perform the actual download of the file 456 func DownloadHTTP(transfer TransferDetails, dest string, token string) (int64, error) { 457 458 // Create the client, request, and context 459 client := grab.NewClient() 460 transport := config.GetTransport() 461 if !transfer.Proxy { 462 transport.Proxy = nil 463 } 464 httpClient, ok := client.HTTPClient.(*http.Client) 465 if !ok { 466 return 0, errors.New("Internal error: implementation is not a http.Client type") 467 } 468 httpClient.Transport = transport 469 470 ctx, cancel := context.WithCancel(context.Background()) 471 defer cancel() 472 log.Debugln("Transfer URL String:", transfer.Url.String()) 473 var req *grab.Request 474 var err error 475 var unpacker *autoUnpacker 476 if transfer.PackOption != "" { 477 behavior, err := GetBehavior(transfer.PackOption) 478 if err != nil { 479 return 0, err 480 } 481 unpacker = newAutoUnpacker(dest, behavior) 482 if req, err = grab.NewRequestToWriter(unpacker, transfer.Url.String()); err != nil { 483 return 0, errors.Wrap(err, "Failed to create new download request") 484 } 485 } else if req, err = grab.NewRequest(dest, transfer.Url.String()); err != nil { 486 return 0, errors.Wrap(err, "Failed to create new download request") 487 } 488 489 if token != "" { 490 req.HTTPRequest.Header.Set("Authorization", "Bearer "+token) 491 } 492 // Set the headers 493 req.HTTPRequest.Header.Set("X-Transfer-Status", "true") 494 req.HTTPRequest.Header.Set("TE", "trailers") 495 req.WithContext(ctx) 496 497 // Test the transfer speed every 5 seconds 498 t := time.NewTicker(5000 * time.Millisecond) 499 defer t.Stop() 500 501 // Progress ticker 502 progressTicker := time.NewTicker(500 * time.Millisecond) 503 defer progressTicker.Stop() 504 downloadLimit := param.Client_MinimumDownloadSpeed.GetInt() 505 506 // If we are doing a recursive, decrease the download limit by the number of likely workers ~5 507 if ObjectClientOptions.Recursive { 508 downloadLimit /= 5 509 } 510 511 // Start the transfer 512 log.Debugln("Starting the HTTP transfer...") 513 filename := path.Base(dest) 514 resp := client.Do(req) 515 // Check the error real quick 516 if resp.IsComplete() { 517 if err := resp.Err(); err != nil { 518 if errors.Is(err, grab.ErrBadLength) { 519 err = fmt.Errorf("Local copy of file is larger than remote copy %w", grab.ErrBadLength) 520 } 521 log.Errorln("Failed to download:", err) 522 return 0, &ConnectionSetupError{Err: err} 523 } 524 } 525 526 // Size of the download 527 contentLength := resp.Size() 528 // Do a head request for content length if resp.Size is unknown 529 if contentLength <= 0 && ObjectClientOptions.ProgressBars { 530 headClient := &http.Client{Transport: config.GetTransport()} 531 headRequest, _ := http.NewRequest("HEAD", transfer.Url.String(), nil) 532 headResponse, err := headClient.Do(headRequest) 533 if err != nil { 534 log.Errorln("Could not successfully get response for HEAD request") 535 return 0, errors.Wrap(err, "Could not determine the size of the remote object") 536 } 537 defer headResponse.Body.Close() 538 contentLengthStr := headResponse.Header.Get("Content-Length") 539 contentLength, err = strconv.ParseInt(contentLengthStr, 10, 64) 540 if err != nil { 541 log.Errorln("problem converting content-length to an int", err) 542 contentLength = resp.Size() 543 } 544 } 545 546 var progressBar *mpb.Bar 547 if ObjectClientOptions.ProgressBars { 548 progressBar = progressContainer.AddBar(0, 549 mpb.PrependDecorators( 550 decor.Name(filename, decor.WCSyncSpaceR), 551 decor.CountersKibiByte("% .2f / % .2f"), 552 ), 553 mpb.AppendDecorators( 554 decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 90), ""), 555 decor.OnComplete(decor.Name(" ] "), ""), 556 decor.OnComplete(decor.EwmaSpeed(decor.SizeB1024(0), "% .2f", 5), "Done!"), 557 ), 558 ) 559 } 560 561 stoppedTransferTimeout := int64(param.Client_StoppedTransferTimeout.GetInt()) 562 slowTransferRampupTime := int64(param.Client_SlowTransferRampupTime.GetInt()) 563 slowTransferWindow := int64(param.Client_SlowTransferWindow.GetInt()) 564 var previousCompletedBytes int64 = 0 565 var startBelowLimit int64 = 0 566 var previousCompletedTime = time.Now() 567 var noProgressStartTime time.Time 568 var lastBytesComplete int64 569 // Loop of the download 570 Loop: 571 for { 572 select { 573 case <-progressTicker.C: 574 if ObjectClientOptions.ProgressBars { 575 progressBar.SetTotal(contentLength, false) 576 currentCompletedBytes := resp.BytesComplete() 577 bytesDelta := currentCompletedBytes - previousCompletedBytes 578 previousCompletedBytes = currentCompletedBytes 579 currentCompletedTime := time.Now() 580 timeElapsed := currentCompletedTime.Sub(previousCompletedTime) 581 progressBar.EwmaIncrInt64(bytesDelta, timeElapsed) 582 previousCompletedTime = currentCompletedTime 583 } 584 585 case <-t.C: 586 // Check that progress is being made and that it is not too slow 587 if resp.BytesComplete() == lastBytesComplete { 588 if noProgressStartTime.IsZero() { 589 noProgressStartTime = time.Now() 590 } else if time.Since(noProgressStartTime) > time.Duration(stoppedTransferTimeout)*time.Second { 591 errMsg := "No progress for more than " + time.Since(noProgressStartTime).Truncate(time.Millisecond).String() 592 log.Errorln(errMsg) 593 if ObjectClientOptions.ProgressBars { 594 progressBar.Abort(true) 595 progressBar.Wait() 596 } 597 return 5, &StoppedTransferError{ 598 Err: errMsg, 599 } 600 } 601 } else { 602 noProgressStartTime = time.Time{} 603 } 604 lastBytesComplete = resp.BytesComplete() 605 606 // Check if we are downloading fast enough 607 if resp.BytesPerSecond() < float64(downloadLimit) { 608 // Give the download `slowTransferRampupTime` (default 120) seconds to start 609 if resp.Duration() < time.Second*time.Duration(slowTransferRampupTime) { 610 continue 611 } else if startBelowLimit == 0 { 612 warning := []byte("Warning! Downloading too slow...\n") 613 status, err := progressContainer.Write(warning) 614 if err != nil { 615 log.Errorln("Problem displaying slow message", err, status) 616 continue 617 } 618 startBelowLimit = time.Now().Unix() 619 continue 620 } else if (time.Now().Unix() - startBelowLimit) < slowTransferWindow { 621 // If the download is below the threshold for less than `SlowTransferWindow` (default 30) seconds, continue 622 continue 623 } 624 // The download is below the threshold for more than `SlowTransferWindow` seconds, cancel the download 625 cancel() 626 if ObjectClientOptions.ProgressBars { 627 progressBar.Abort(true) 628 progressBar.Wait() 629 } 630 631 log.Errorln("Cancelled: Download speed of ", resp.BytesPerSecond(), "bytes/s", " is below the limit of", downloadLimit, "bytes/s") 632 633 return 0, &SlowTransferError{ 634 BytesTransferred: resp.BytesComplete(), 635 BytesPerSecond: int64(resp.BytesPerSecond()), 636 Duration: resp.Duration(), 637 BytesTotal: contentLength, 638 } 639 640 } else { 641 // The download is fast enough, reset the startBelowLimit 642 startBelowLimit = 0 643 } 644 645 case <-resp.Done: 646 // download is complete 647 if ObjectClientOptions.ProgressBars { 648 downloadError := resp.Err() 649 if downloadError != nil { 650 log.Errorln(downloadError.Error()) 651 progressBar.Abort(true) 652 progressBar.Wait() 653 } else { 654 progressBar.SetTotal(contentLength, true) 655 // call wait here for the bar to complete and flush 656 // If recursive, we still want to use container so keep it open 657 if ObjectClientOptions.Recursive { 658 progressBar.Wait() 659 } else { // Otherwise just close it 660 progressContainer.Wait() 661 } 662 } 663 } 664 break Loop 665 } 666 } 667 //fmt.Printf("\nDownload saved to", resp.Filename) 668 err = resp.Err() 669 if err != nil { 670 // Connection errors 671 if errors.Is(err, syscall.ECONNREFUSED) || 672 errors.Is(err, syscall.ECONNRESET) || 673 errors.Is(err, syscall.ECONNABORTED) { 674 return 0, &ConnectionSetupError{URL: resp.Request.URL().String()} 675 } 676 log.Debugln("Got error from HTTP download", err) 677 return 0, err 678 } else { 679 // Check the trailers for any error information 680 trailer := resp.HTTPResponse.Trailer 681 if errorStatus := trailer.Get("X-Transfer-Status"); errorStatus != "" { 682 statusCode, statusText := parseTransferStatus(errorStatus) 683 if statusCode != 200 { 684 log.Debugln("Got error from file transfer") 685 return 0, errors.New("transfer error: " + statusText) 686 } 687 } 688 } 689 // Valid responses include 200 and 206. The latter occurs if the download was resumed after a 690 // prior attempt. 691 if resp.HTTPResponse.StatusCode != 200 && resp.HTTPResponse.StatusCode != 206 { 692 log.Debugln("Got failure status code:", resp.HTTPResponse.StatusCode) 693 return 0, &HttpErrResp{resp.HTTPResponse.StatusCode, fmt.Sprintf("Request failed (HTTP status %d): %s", 694 resp.HTTPResponse.StatusCode, resp.Err().Error())} 695 } 696 697 if unpacker != nil { 698 unpacker.Close() 699 if err := unpacker.Error(); err != nil { 700 return 0, err 701 } 702 } 703 704 log.Debugln("HTTP Transfer was successful") 705 return resp.BytesComplete(), nil 706 } 707 708 type Sizer interface { 709 Size() int64 710 BytesComplete() int64 711 } 712 713 type ConstantSizer struct { 714 size int64 715 read atomic.Int64 716 } 717 718 func (cs *ConstantSizer) Size() int64 { 719 return cs.size 720 } 721 722 func (cs *ConstantSizer) BytesComplete() int64 { 723 return cs.read.Load() 724 } 725 726 // ProgressReader wraps the io.Reader to get progress 727 // Adapted from https://stackoverflow.com/questions/26050380/go-tracking-post-request-progress 728 type ProgressReader struct { 729 reader io.ReadCloser 730 sizer Sizer 731 closed chan bool 732 } 733 734 // Read implements the common read function for io.Reader 735 func (pr *ProgressReader) Read(p []byte) (n int, err error) { 736 n, err = pr.reader.Read(p) 737 if cs, ok := pr.sizer.(*ConstantSizer); ok { 738 cs.read.Add(int64(n)) 739 } 740 return n, err 741 } 742 743 // Close implments the close function of io.Closer 744 func (pr *ProgressReader) Close() error { 745 err := pr.reader.Close() 746 // Also, send the closed channel a message 747 pr.closed <- true 748 return err 749 } 750 751 func (pr *ProgressReader) BytesComplete() int64 { 752 return pr.sizer.BytesComplete() 753 } 754 755 func (pr *ProgressReader) Size() int64 { 756 return pr.sizer.Size() 757 } 758 759 // Recursively uploads a directory with all files and nested dirs, keeping file structure on server side 760 func UploadDirectory(src string, dest *url.URL, token string, namespace namespaces.Namespace) (int64, error) { 761 var files []string 762 var amountDownloaded int64 763 srcUrl := url.URL{Path: src} 764 // Get the list of files as well as make any directories on the server end 765 files, err := walkDavDir(&srcUrl, namespace, token, dest.Path, true) 766 if err != nil { 767 return 0, err 768 } 769 770 if ObjectClientOptions.ProgressBars { 771 log.SetOutput(progressContainer) 772 } 773 // Upload all of our files within the proper directories 774 for _, file := range files { 775 tempDest := url.URL{} 776 tempDest.Path, err = url.JoinPath(dest.Path, file) 777 if err != nil { 778 return 0, err 779 } 780 downloaded, err := UploadFile(file, &tempDest, token, namespace) 781 if err != nil { 782 return 0, err 783 } 784 amountDownloaded += downloaded 785 } 786 // Close progress bar container 787 if ObjectClientOptions.ProgressBars { 788 progressContainer.Wait() 789 log.SetOutput(os.Stdout) 790 } 791 return amountDownloaded, err 792 } 793 794 // UploadFile Uploads a file using HTTP 795 func UploadFile(src string, origDest *url.URL, token string, namespace namespaces.Namespace) (int64, error) { 796 797 log.Debugln("In UploadFile") 798 log.Debugln("Dest", origDest.String()) 799 800 // Stat the file to get the size (for progress bar) 801 fileInfo, err := os.Stat(src) 802 if err != nil { 803 log.Errorln("Error checking local file ", src, ":", err) 804 return 0, err 805 } 806 807 var ioreader io.ReadCloser 808 var sizer Sizer 809 pack := origDest.Query().Get("pack") 810 nonZeroSize := true 811 if pack != "" { 812 if !fileInfo.IsDir() { 813 return 0, errors.Errorf("Upload with pack=%v only works when input (%v) is a directory", pack, src) 814 } 815 behavior, err := GetBehavior(pack) 816 if err != nil { 817 return 0, err 818 } 819 if behavior == autoBehavior { 820 behavior = defaultBehavior 821 } 822 ap := newAutoPacker(src, behavior) 823 ioreader = ap 824 sizer = ap 825 } else { 826 // Try opening the file to send 827 file, err := os.Open(src) 828 if err != nil { 829 log.Errorln("Error opening local file:", err) 830 return 0, err 831 } 832 ioreader = file 833 sizer = &ConstantSizer{size: fileInfo.Size()} 834 nonZeroSize = fileInfo.Size() > 0 835 } 836 837 // Parse the writeback host as a URL 838 writebackhostUrl, err := url.Parse(namespace.WriteBackHost) 839 if err != nil { 840 return 0, err 841 } 842 843 dest := &url.URL{ 844 Host: writebackhostUrl.Host, 845 Scheme: "https", 846 Path: origDest.Path, 847 } 848 849 // Create the wrapped reader and send it to the request 850 closed := make(chan bool, 1) 851 errorChan := make(chan error, 1) 852 responseChan := make(chan *http.Response) 853 reader := &ProgressReader{ioreader, sizer, closed} 854 putContext, cancel := context.WithCancel(context.Background()) 855 defer cancel() 856 log.Debugln("Full destination URL:", dest.String()) 857 var request *http.Request 858 // For files that are 0 length, we need to send a PUT request with an nil body 859 if nonZeroSize { 860 request, err = http.NewRequestWithContext(putContext, "PUT", dest.String(), reader) 861 } else { 862 request, err = http.NewRequestWithContext(putContext, "PUT", dest.String(), http.NoBody) 863 } 864 if err != nil { 865 log.Errorln("Error creating request:", err) 866 return 0, err 867 } 868 // Set the authorization header 869 request.Header.Set("Authorization", "Bearer "+token) 870 var lastKnownWritten int64 871 t := time.NewTicker(20 * time.Second) 872 defer t.Stop() 873 go doPut(request, responseChan, errorChan) 874 var lastError error = nil 875 876 var progressBar *mpb.Bar 877 if ObjectClientOptions.ProgressBars { 878 progressBar = progressContainer.AddBar(0, 879 mpb.PrependDecorators( 880 decor.Name(src, decor.WCSyncSpaceR), 881 decor.CountersKibiByte("% .2f / % .2f"), 882 ), 883 mpb.AppendDecorators( 884 decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 90), ""), 885 decor.OnComplete(decor.Name(" ] "), ""), 886 decor.OnComplete(decor.EwmaSpeed(decor.SizeB1024(0), "% .2f", 5), "Done!"), 887 ), 888 ) 889 // Shutdown progress bar at the end of the function 890 defer func() { 891 if lastError == nil { 892 progressBar.SetTotal(reader.Size(), true) 893 } else { 894 progressBar.Abort(true) 895 } 896 // If it is recursive, we need to reuse the mpb instance. Closed later 897 if ObjectClientOptions.Recursive { 898 progressBar.Wait() 899 } else { // If not recursive, go ahead and close it 900 progressContainer.Wait() 901 } 902 }() 903 } 904 tickerDuration := 500 * time.Millisecond 905 progressTicker := time.NewTicker(tickerDuration) 906 defer progressTicker.Stop() 907 908 // Do the select on a ticker, and the writeChan 909 Loop: 910 for { 911 select { 912 case <-progressTicker.C: 913 if progressBar != nil { 914 progressBar.SetTotal(reader.Size(), false) 915 progressBar.EwmaSetCurrent(reader.BytesComplete(), tickerDuration) 916 } 917 918 case <-t.C: 919 // If we are not making any progress, if we haven't written 1MB in the last 5 seconds 920 currentRead := reader.BytesComplete() 921 log.Debugln("Current read:", currentRead) 922 log.Debugln("Last known written:", lastKnownWritten) 923 if lastKnownWritten < currentRead { 924 // We have made progress! 925 lastKnownWritten = currentRead 926 } else { 927 // No progress has been made in the last 1 second 928 log.Errorln("No progress made in last 5 second in upload") 929 lastError = errors.New("upload cancelled, no progress in 5 seconds") 930 break Loop 931 } 932 933 case <-closed: 934 // The file has been closed, we're done here 935 log.Debugln("File closed") 936 case response := <-responseChan: 937 if response.StatusCode != 200 { 938 log.Errorln("Got failure status code:", response.StatusCode) 939 lastError = &HttpErrResp{response.StatusCode, fmt.Sprintf("Request failed (HTTP status %d)", 940 response.StatusCode)} 941 break Loop 942 } 943 break Loop 944 945 case err := <-errorChan: 946 log.Warningln("Unexpected error when performing upload:", err) 947 lastError = err 948 break Loop 949 950 } 951 } 952 953 if fileInfo.Size() == 0 { 954 return 0, lastError 955 } else { 956 return reader.BytesComplete(), lastError 957 } 958 959 } 960 961 var UploadClient = &http.Client{Transport: config.GetTransport()} 962 963 // Actually perform the Put request to the server 964 func doPut(request *http.Request, responseChan chan<- *http.Response, errorChan chan<- error) { 965 client := UploadClient 966 dump, _ := httputil.DumpRequestOut(request, false) 967 log.Debugf("Dumping request: %s", dump) 968 response, err := client.Do(request) 969 if err != nil { 970 log.Errorln("Error with PUT:", err) 971 errorChan <- err 972 return 973 } 974 dump, _ = httputil.DumpResponse(response, true) 975 log.Debugf("Dumping response: %s", dump) 976 if response.StatusCode != 200 { 977 log.Errorln("Error status code:", response.Status) 978 log.Debugln("From the server:") 979 textResponse, err := io.ReadAll(response.Body) 980 if err != nil { 981 log.Errorln("Error reading response from server:", err) 982 responseChan <- response 983 return 984 } 985 log.Debugln(string(textResponse)) 986 } 987 responseChan <- response 988 989 } 990 991 func walkDavDir(url *url.URL, namespace namespaces.Namespace, token string, destPath string, upload bool) ([]string, error) { 992 993 // Create the client to walk the filesystem 994 rootUrl := *url 995 if namespace.DirListHost != "" { 996 // Parse the dir list host 997 dirListURL, err := url.Parse(namespace.DirListHost) 998 if err != nil { 999 log.Errorln("Failed to parse dirlisthost from namespaces into URL:", err) 1000 return nil, err 1001 } 1002 rootUrl = *dirListURL 1003 1004 } else { 1005 log.Errorln("Host for directory listings is unknown") 1006 return nil, errors.New("Host for directory listings is unknown") 1007 } 1008 log.Debugln("Dir list host: ", rootUrl.String()) 1009 1010 auth := &bearerAuth{token: token} 1011 c := gowebdav.NewAuthClient(rootUrl.String(), auth) 1012 1013 // XRootD does not like keep alives and kills things, so turn them off. 1014 transport := config.GetTransport() 1015 c.SetTransport(transport) 1016 var files []string 1017 var err error 1018 if upload { 1019 files, err = walkDirUpload(url.Path, c, destPath) 1020 } else { 1021 files, err = walkDir(url.Path, c) 1022 } 1023 log.Debugln("Found files:", files) 1024 return files, err 1025 1026 } 1027 1028 // For uploads, we want to make directories on the server end 1029 func walkDirUpload(path string, client *gowebdav.Client, destPath string) ([]string, error) { 1030 // List of files to return 1031 var files []string 1032 // Whenever this function is called, we should create a new dir on the server side for uploads 1033 err := client.Mkdir(destPath+path, 0755) 1034 if err != nil { 1035 return nil, err 1036 } 1037 log.Debugf("Creating directory: %s", destPath+path) 1038 1039 // Get our list of files 1040 infos, err := os.ReadDir(path) 1041 if err != nil { 1042 return nil, err 1043 } 1044 for _, info := range infos { 1045 newPath := path + "/" + info.Name() 1046 if info.IsDir() { 1047 // Recursively call this function to create any nested dir's as well as list their files 1048 returnedFiles, err := walkDirUpload(newPath, client, destPath) 1049 if err != nil { 1050 return nil, err 1051 } 1052 files = append(files, returnedFiles...) 1053 } else { 1054 // It is a normal file 1055 files = append(files, newPath) 1056 } 1057 } 1058 return files, err 1059 } 1060 1061 func walkDir(path string, client *gowebdav.Client) ([]string, error) { 1062 var files []string 1063 log.Debugln("Reading directory: ", path) 1064 infos, err := client.ReadDir(path) 1065 if err != nil { 1066 return nil, err 1067 } 1068 for _, info := range infos { 1069 newPath := path + "/" + info.Name() 1070 if info.IsDir() { 1071 returnedFiles, err := walkDir(newPath, client) 1072 if err != nil { 1073 return nil, err 1074 } 1075 files = append(files, returnedFiles...) 1076 } else { 1077 // It is a normal file 1078 files = append(files, newPath) 1079 } 1080 } 1081 return files, nil 1082 } 1083 1084 func StatHttp(dest *url.URL, namespace namespaces.Namespace) (uint64, error) { 1085 1086 scitoken_contents, err := getToken(dest, namespace, false, "") 1087 if err != nil { 1088 return 0, err 1089 } 1090 1091 // Parse the writeback host as a URL 1092 writebackhostUrl, err := url.Parse(namespace.WriteBackHost) 1093 if err != nil { 1094 return 0, err 1095 } 1096 dest.Host = writebackhostUrl.Host 1097 dest.Scheme = "https" 1098 1099 canDisableProxy := CanDisableProxy() 1100 disableProxy := !IsProxyEnabled() 1101 1102 var resp *http.Response 1103 for { 1104 transport := config.GetTransport() 1105 if disableProxy { 1106 log.Debugln("Performing HEAD (without proxy)", dest.String()) 1107 transport.Proxy = nil 1108 } else { 1109 log.Debugln("Performing HEAD", dest.String()) 1110 } 1111 1112 client := &http.Client{Transport: transport} 1113 req, err := http.NewRequest("HEAD", dest.String(), nil) 1114 if err != nil { 1115 log.Errorln("Failed to create HTTP request:", err) 1116 return 0, err 1117 } 1118 1119 if scitoken_contents != "" { 1120 req.Header.Set("Authorization", "Bearer "+scitoken_contents) 1121 } 1122 1123 resp, err = client.Do(req) 1124 if err == nil { 1125 break 1126 } 1127 if urle, ok := err.(*url.Error); canDisableProxy && !disableProxy && ok && urle.Unwrap() != nil { 1128 if ope, ok := urle.Unwrap().(*net.OpError); ok && ope.Op == "proxyconnect" { 1129 log.Warnln("Failed to connect to proxy; will retry without:", ope) 1130 disableProxy = true 1131 continue 1132 } 1133 } 1134 log.Errorln("Failed to get HTTP response:", err) 1135 return 0, err 1136 } 1137 1138 if resp.StatusCode == 200 { 1139 defer resp.Body.Close() 1140 contentLengthStr := resp.Header.Get("Content-Length") 1141 if len(contentLengthStr) == 0 { 1142 log.Errorln("HEAD response did not include Content-Length header") 1143 return 0, errors.New("HEAD response did not include Content-Length header") 1144 } 1145 contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64) 1146 if err != nil { 1147 log.Errorf("Unable to parse Content-Length header value (%s) as integer: %s", contentLengthStr, err) 1148 return 0, err 1149 } 1150 return uint64(contentLength), nil 1151 } else { 1152 response_b, err := io.ReadAll(resp.Body) 1153 if err != nil { 1154 log.Errorln("Failed to read error message:", err) 1155 return 0, err 1156 } 1157 defer resp.Body.Close() 1158 return 0, &HttpErrResp{resp.StatusCode, fmt.Sprintf("Request failed (HTTP status %d): %s", resp.StatusCode, string(response_b))} 1159 } 1160 }