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  }