github.com/htcondor/osdf-client/v6@v6.13.0-rc1.0.20231009141709-766e7b4d1dc8/cmd/stashcp/main.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"os"
     7  	"strings"
     8  
     9  	stashcp "github.com/htcondor/osdf-client/v6"
    10  	namespaces "github.com/htcondor/osdf-client/v6/namespaces"
    11  	"github.com/jessevdk/go-flags"
    12  	log "github.com/sirupsen/logrus"
    13  )
    14  
    15  var (
    16  	version = "dev"
    17  	commit  = "none"
    18  	date    = "unknown"
    19  	builtBy = "unknown"
    20  )
    21  
    22  type SourceDestination struct {
    23  	Sources []string `positional-arg-name:"sources" short:"i" long:"input" description:"Source file(s)" default:"-"`
    24  
    25  	// A useless variable.  It should alwasy be empty.  The Sources variable above should "grab" all of the positional arguments
    26  	// The only reason we have this variable is so the help message has the [sources...] [destination] help mesage.
    27  	Destination string `positional-arg-name:"destination" short:"o" long:"output" description:"Destination file/directory" default:"-"`
    28  }
    29  
    30  type Options struct {
    31  	// Turn on the debug logging
    32  	Debug bool `short:"d" long:"debug" description:"Turn on debug logging"`
    33  
    34  	// Specify the configuration file
    35  	Closest bool `long:"closest" description:"Return the closest cache and exit"`
    36  
    37  	// Cache to use
    38  	Cache string `short:"c" long:"cache" description:"Cache to use"`
    39  
    40  	// A JSON file containing the list of caches
    41  	CacheJSON string `short:"j" long:"caches-json" description:"A JSON file containing the list of caches"`
    42  
    43  	// Comma separated list of methods to try, in order.  Default: cvmfs,http
    44  	Methods string `long:"methods" description:"Comma separated list of methods to try, in order." default:"cvmfs,http"`
    45  
    46  	// Token file to use for reading and/or writing
    47  	Token string `long:"token" short:"t" description:"Token file to use for reading and/or writing"`
    48  
    49  	ListCaches bool `long:"list-names" description:"Return the names of pre-configured cache lists and exit"`
    50  
    51  	ListDir bool `long:"list-dir" short:"l" description:"List the directory pointed to by source"`
    52  
    53  	// Version information
    54  	Version bool `long:"version" short:"v" description:"Print the version and exit"`
    55  
    56  	// Namespace information
    57  	PrintNamespaces bool `long:"namespaces" description:"Print the namespace information and exit"`
    58  
    59  	// List Types (xroot or xroots)
    60  	ListType string `long:"cache-list-name" short:"n" description:"Cache list to use, currently either xroot or xroots" default:"xroot"`
    61  
    62  	// Recursive walking of directories
    63  	Recursive bool `short:"r" description:"Recursively copy a directory.  Forces methods to only be http to get the freshest directory contents"`
    64  
    65  	// Progress bars
    66  	ProgessBars bool `long:"progress" short:"p" description:"Show progress bars, turned on if run from a terminal"`
    67  
    68  	// PluginInterface specifies how the output should be formatted
    69  	PluginInterface bool `long:"plugininterface" description:"Output in HTCondor plugin format.  Turned on if executable is named stash_plugin"`
    70  
    71  	// Positional arguemnts
    72  	SourceDestination SourceDestination `description:"Source and Destination Files" positional-args:"1"`
    73  }
    74  
    75  var options Options
    76  
    77  var parser = flags.NewParser(&options, flags.Default)
    78  
    79  func main() {
    80  
    81  	stashcp.Options.Version = version
    82  	
    83  	// Capture the start time of the transfer
    84  	if _, err := parser.Parse(); err != nil {
    85  		if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
    86  			os.Exit(0)
    87  		} else {
    88  			log.Errorln(err)
    89  			os.Exit(1)
    90  		}
    91  	}
    92  
    93  	if options.Debug {
    94  		// Set logging to debug level
    95  		err := setLogging(log.DebugLevel)
    96  		if err != nil {
    97  			log.Panicln("Failed to set logging level to Debug:", err)
    98  		}
    99  	} else {
   100  		err := setLogging(log.ErrorLevel)
   101  		if err != nil {
   102  			log.Panicln("Failed to set logging level to Error:", err)
   103  		}
   104  
   105  	}
   106  
   107  	if options.Version {
   108  		fmt.Println("Version:", version)
   109  		fmt.Println("Build Date:", date)
   110  		fmt.Println("Build Commit:", commit)
   111  		fmt.Println("Built By:", builtBy)
   112  		os.Exit(0)
   113  	}
   114  
   115  	// Set the progress bars to the command line option
   116  	stashcp.Options.ProgressBars = options.ProgessBars
   117  	stashcp.Options.Token = options.Token
   118  
   119  	// Check if the program was executed from a terminal
   120  	// https://rosettacode.org/wiki/Check_output_device_is_a_terminal#Go
   121  	if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
   122  		stashcp.Options.ProgressBars = true
   123  	} else {
   124  		stashcp.Options.ProgressBars = false
   125  	}
   126  
   127  	if options.PrintNamespaces {
   128  		namespaces, err := namespaces.GetNamespaces()
   129  		if err != nil {
   130  			fmt.Println("Failed to get namespaces:", err)
   131  			os.Exit(1)
   132  		}
   133  		fmt.Printf("%+v\n", namespaces)
   134  		os.Exit(0)
   135  	}
   136  
   137  	// Just return all the caches that it knows about
   138  	// Print out all of the caches and exit
   139  	if options.ListCaches {
   140  		cacheList, err := stashcp.GetBestCache(options.ListType)
   141  		if err != nil {
   142  			log.Errorln("Failed to get best caches:", err)
   143  			os.Exit(1)
   144  		}
   145  		// Print the caches, comma separated,
   146  		fmt.Println(strings.Join(cacheList[:], ","))
   147  		os.Exit(0)
   148  	}
   149  
   150  	if options.Closest {
   151  		cacheList, err := stashcp.GetBestCache(options.ListType)
   152  		if err != nil {
   153  			log.Errorln("Failed to get best stashcache: ", err)
   154  		}
   155  		fmt.Println(cacheList[0])
   156  		os.Exit(0)
   157  	}
   158  
   159  	log.Debugln("Len of source:", len(options.SourceDestination.Sources))
   160  	if len(options.SourceDestination.Sources) < 2 {
   161  		log.Errorln("No Source or Destination")
   162  		parser.WriteHelp(os.Stdout)
   163  		os.Exit(1)
   164  	}
   165  	source := options.SourceDestination.Sources[:len(options.SourceDestination.Sources)-1]
   166  	dest := options.SourceDestination.Sources[len(options.SourceDestination.Sources)-1]
   167  
   168  	log.Debugln("Sources:", source)
   169  	log.Debugln("Destination:", dest)
   170  	if options.ListDir {
   171  		dirUrl, _ := url.Parse("http://stash.osgconnect.net:1094")
   172  		dirUrl.Path = source[0]
   173  		isDir, err := stashcp.IsDir(dirUrl, "", namespaces.Namespace{})
   174  		if err != nil {
   175  			log.Errorln("Error getting directory listing:", err)
   176  		}
   177  		log.Debugln("Dir is a directory?", isDir)
   178  		return
   179  	}
   180  
   181  	/*
   182  		TODO: Parse a caches JSON, is this needed anymore?
   183  		if args.caches_json {
   184  			caches_json_location = caches_json
   185  
   186  		} else if val, jsonPresent := os.LookupEnv("CACHES_JSON"); jsonPresent {
   187  			caches_json_location = val
   188  		} else {
   189  			prefix = os.Getenv("OSG_LOCATION", "/")
   190  			caches_file = filepath.Join(prefix, "etc/stashcache/caches.json")
   191  			if _, err := os.Stat(caches_file); err == nil {
   192  				caches_json_location = caches_file
   193  			}
   194  		}
   195  
   196  		caches_list_name = args.cache_list_name
   197  	*/
   198  
   199  	// Check for manually entered cache to use ??
   200  	nearestCache, nearestCacheIsPresent := os.LookupEnv("NEAREST_CACHE")
   201  
   202  	if nearestCacheIsPresent {
   203  		stashcp.NearestCache = nearestCache
   204  		stashcp.NearestCacheList = append(stashcp.NearestCacheList, stashcp.NearestCache)
   205  		stashcp.CacheOverride = true
   206  	} else if options.Cache != "" {
   207  		stashcp.NearestCache = options.Cache
   208  		stashcp.NearestCacheList = append(stashcp.NearestCacheList, options.Cache)
   209  		stashcp.CacheOverride = true
   210  	}
   211  
   212  	// Convert the methods
   213  	splitMethods := strings.Split(options.Methods, ",")
   214  
   215  	// If the user overrides the cache, then only use HTTP
   216  	if stashcp.CacheOverride {
   217  		splitMethods = []string{"http"}
   218  	}
   219  
   220  	if len(source) > 1 {
   221  		if destStat, err := os.Stat(dest); err != nil && destStat.IsDir() {
   222  			log.Errorln("Destination is not a directory")
   223  			os.Exit(1)
   224  		}
   225  	}
   226  
   227  	var result error
   228  	var downloaded int64 = 0
   229  	lastSrc := ""
   230  	for _, src := range source {
   231  		var tmpDownloaded int64
   232  		tmpDownloaded, result = stashcp.DoStashCPSingle(src, dest, splitMethods, options.Recursive)
   233  		downloaded += tmpDownloaded
   234  		if result != nil {
   235  			lastSrc = src
   236  			break
   237  		} else {
   238  			stashcp.ClearErrors()
   239  		}
   240  	}
   241  
   242  	// Exit with failure
   243  	if result != nil {
   244  		// Print the list of errors
   245  		errMsg := stashcp.GetErrors()
   246  		if errMsg == "" {
   247  			errMsg = result.Error()
   248  		}
   249  		log.Errorln("Failure downloading " + lastSrc + ": " + errMsg)
   250  		if stashcp.ErrorsRetryable() {
   251  			log.Errorln("Errors are retryable")
   252  			os.Exit(11)
   253  		}
   254  		os.Exit(1)
   255  	}
   256  
   257  }
   258  
   259  func setLogging(logLevel log.Level) error {
   260  	textFormatter := log.TextFormatter{}
   261  	textFormatter.DisableLevelTruncation = true
   262  	textFormatter.FullTimestamp = true
   263  	log.SetFormatter(&textFormatter)
   264  	log.SetLevel(logLevel)
   265  	return nil
   266  }