github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/main.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  	"bytes"
    22  	"errors"
    23  	"fmt"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"reflect"
    28  	"regexp"
    29  	"runtime"
    30  	"sort"
    31  	"strconv"
    32  	"strings"
    33  	"syscall"
    34  	"time"
    35  
    36  	"github.com/inconshreveable/mousetrap"
    37  	"github.com/minio/cli"
    38  	"github.com/minio/mc/pkg/probe"
    39  	"github.com/minio/minio-go/v7/pkg/set"
    40  	"github.com/minio/pkg/v2/console"
    41  	"github.com/minio/pkg/v2/env"
    42  	"github.com/minio/pkg/v2/trie"
    43  	"github.com/minio/pkg/v2/words"
    44  	"golang.org/x/term"
    45  
    46  	completeinstall "github.com/posener/complete/cmd/install"
    47  )
    48  
    49  // global flags for mc.
    50  var mcFlags = []cli.Flag{
    51  	cli.BoolFlag{
    52  		Name:  "autocompletion",
    53  		Usage: "install auto-completion for your shell",
    54  	},
    55  }
    56  
    57  // Help template for mc
    58  var mcHelpTemplate = `NAME:
    59    {{.Name}} - {{.Usage}}
    60  
    61  USAGE:
    62    {{.Name}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}} [COMMAND FLAGS | -h]{{end}} [ARGUMENTS...]
    63  
    64  COMMANDS:
    65    {{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
    66    {{end}}{{if .VisibleFlags}}
    67  GLOBAL FLAGS:
    68    {{range .VisibleFlags}}{{.}}
    69    {{end}}{{end}}
    70  TIP:
    71    Use '{{.Name}} --autocompletion' to enable shell autocompletion
    72  
    73  COPYRIGHT:
    74    Copyright (c) 2015-` + CopyrightYear + ` MinIO, Inc.
    75  
    76  LICENSE:
    77    GNU AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>
    78  `
    79  
    80  func init() {
    81  	if env.IsSet(mcEnvConfigFile) {
    82  		configFile := env.Get(mcEnvConfigFile, "")
    83  		fatalIf(readAliasesFromFile(configFile).Trace(configFile), "Unable to parse "+configFile)
    84  	}
    85  	if runtime.GOOS == "windows" {
    86  		if mousetrap.StartedByExplorer() {
    87  			fmt.Printf("Don't double-click %s\n", os.Args[0])
    88  			fmt.Println("You need to open cmd.exe/PowerShell and run it from the command line")
    89  			fmt.Println("Press the Enter Key to Exit")
    90  			fmt.Scanln()
    91  			os.Exit(1)
    92  		}
    93  	}
    94  }
    95  
    96  // Main starts mc application
    97  func Main(args []string) error {
    98  	if len(args) > 1 {
    99  		switch args[1] {
   100  		case "mc", filepath.Base(args[0]):
   101  			mainComplete()
   102  			return nil
   103  		}
   104  	}
   105  
   106  	// ``MC_PROFILER`` supported options are [cpu, mem, block, goroutine].
   107  	if p := os.Getenv("MC_PROFILER"); p != "" {
   108  		profilers := strings.Split(p, ",")
   109  		if e := enableProfilers(mustGetProfileDir(), profilers); e != nil {
   110  			console.Fatal(e)
   111  		}
   112  	}
   113  
   114  	probe.Init() // Set project's root source path.
   115  	probe.SetAppInfo("Release-Tag", ReleaseTag)
   116  	probe.SetAppInfo("Commit", ShortCommitID)
   117  
   118  	// Fetch terminal size, if not available, automatically
   119  	// set globalQuiet to true on non-window.
   120  	if w, h, e := term.GetSize(int(os.Stdin.Fd())); e != nil {
   121  		globalQuiet = runtime.GOOS != "windows"
   122  	} else {
   123  		globalTermWidth, globalTermHeight = w, h
   124  	}
   125  
   126  	// Set the mc app name.
   127  	appName := filepath.Base(args[0])
   128  	if runtime.GOOS == "windows" && strings.HasSuffix(strings.ToLower(appName), ".exe") {
   129  		// Trim ".exe" from Windows executable.
   130  		appName = appName[:strings.LastIndex(appName, ".")]
   131  	}
   132  
   133  	// Monitor OS exit signals and cancel the global context in such case
   134  	go trapSignals(os.Interrupt, syscall.SIGTERM, syscall.SIGKILL)
   135  
   136  	globalHelpPager = newTermPager()
   137  	// Wait until the user quits the pager
   138  	defer globalHelpPager.WaitForExit()
   139  
   140  	parsePagerDisableFlag(args)
   141  	// Run the app
   142  	return registerApp(appName).Run(args)
   143  }
   144  
   145  func flagValue(f cli.Flag) reflect.Value {
   146  	fv := reflect.ValueOf(f)
   147  	for fv.Kind() == reflect.Ptr {
   148  		fv = reflect.Indirect(fv)
   149  	}
   150  	return fv
   151  }
   152  
   153  func visibleFlags(fl []cli.Flag) []cli.Flag {
   154  	visible := []cli.Flag{}
   155  	for _, flag := range fl {
   156  		field := flagValue(flag).FieldByName("Hidden")
   157  		if !field.IsValid() || !field.Bool() {
   158  			visible = append(visible, flag)
   159  		}
   160  	}
   161  	return visible
   162  }
   163  
   164  // Function invoked when invalid flag is passed
   165  func onUsageError(ctx *cli.Context, err error, _ bool) error {
   166  	type subCommandHelp struct {
   167  		flagName string
   168  		usage    string
   169  	}
   170  
   171  	// Calculate the maximum width of the flag name field
   172  	// for a good looking printing
   173  	vflags := visibleFlags(ctx.Command.Flags)
   174  	help := make([]subCommandHelp, len(vflags))
   175  	maxWidth := 0
   176  	for i, f := range vflags {
   177  		s := strings.Split(f.String(), "\t")
   178  		if len(s[0]) > maxWidth {
   179  			maxWidth = len(s[0])
   180  		}
   181  
   182  		help[i] = subCommandHelp{flagName: s[0], usage: s[1]}
   183  	}
   184  	maxWidth += 2
   185  
   186  	var errMsg strings.Builder
   187  
   188  	// Do the good-looking printing now
   189  	fmt.Fprintln(&errMsg, "Invalid command usage,", err.Error())
   190  	if len(help) > 0 {
   191  		fmt.Fprintln(&errMsg, "\nSUPPORTED FLAGS:")
   192  		for _, h := range help {
   193  			spaces := string(bytes.Repeat([]byte{' '}, maxWidth-len(h.flagName)))
   194  			fmt.Fprintf(&errMsg, "   %s%s%s\n", h.flagName, spaces, h.usage)
   195  		}
   196  	}
   197  	console.Fatal(errMsg.String())
   198  	return err
   199  }
   200  
   201  // Function invoked when invalid command is passed.
   202  func commandNotFound(ctx *cli.Context, cmds []cli.Command) {
   203  	command := ctx.Args().First()
   204  	if command == "" {
   205  		cli.ShowCommandHelp(ctx, command)
   206  		return
   207  	}
   208  	msg := fmt.Sprintf("`%s` is not a recognized command. Get help using `--help` flag.", command)
   209  	commandsTree := trie.NewTrie()
   210  	for _, cmd := range cmds {
   211  		commandsTree.Insert(cmd.Name)
   212  	}
   213  	closestCommands := findClosestCommands(commandsTree, command)
   214  	if len(closestCommands) > 0 {
   215  		msg += "\n\nDid you mean one of these?\n"
   216  		if len(closestCommands) == 1 {
   217  			cmd := closestCommands[0]
   218  			msg += fmt.Sprintf("        `%s`", cmd)
   219  		} else {
   220  			for _, cmd := range closestCommands {
   221  				msg += fmt.Sprintf("        `%s`\n", cmd)
   222  			}
   223  		}
   224  	}
   225  	fatalIf(errDummy().Trace(), msg)
   226  }
   227  
   228  // Check for sane config environment early on and gracefully report.
   229  func checkConfig() {
   230  	// Refresh the config once.
   231  	loadMcConfig = loadMcConfigFactory()
   232  	// Ensures config file is sane.
   233  	config, err := loadMcConfig()
   234  	// Verify if the path is accesible before validating the config
   235  	fatalIf(err.Trace(mustGetMcConfigPath()), "Unable to access configuration file.")
   236  
   237  	// Validate and print error messges
   238  	ok, errMsgs := validateConfigFile(config)
   239  	if !ok {
   240  		var errorMsg bytes.Buffer
   241  		for index, errMsg := range errMsgs {
   242  			// Print atmost 10 errors
   243  			if index > 10 {
   244  				break
   245  			}
   246  			errorMsg.WriteString(errMsg + "\n")
   247  		}
   248  		console.Fatal(errorMsg.String())
   249  	}
   250  }
   251  
   252  func migrate() {
   253  	// Fix broken config files if any.
   254  	fixConfig()
   255  
   256  	// Migrate config files if any.
   257  	migrateConfig()
   258  
   259  	// Migrate shared urls if any.
   260  	migrateShare()
   261  }
   262  
   263  // initMC - initialize 'mc'.
   264  func initMC() {
   265  	// Check if mc config exists.
   266  	if !isMcConfigExists() {
   267  		err := saveMcConfig(newMcConfig())
   268  		fatalIf(err.Trace(), "Unable to save new mc config.")
   269  
   270  		if !globalQuiet && !globalJSON {
   271  			console.Infoln("Configuration written to `" + mustGetMcConfigPath() + "`. Please update your access credentials.")
   272  		}
   273  	}
   274  
   275  	// Check if mc share directory exists.
   276  	if !isShareDirExists() {
   277  		initShareConfig()
   278  	}
   279  
   280  	// Check if certs dir exists
   281  	if !isCertsDirExists() {
   282  		fatalIf(createCertsDir().Trace(), "Unable to create `CAs` directory.")
   283  	}
   284  
   285  	// Check if CAs dir exists
   286  	if !isCAsDirExists() {
   287  		fatalIf(createCAsDir().Trace(), "Unable to create `CAs` directory.")
   288  	}
   289  
   290  	// Load all authority certificates present in CAs dir
   291  	loadRootCAs()
   292  }
   293  
   294  func getShellName() (string, bool) {
   295  	shellName := os.Getenv("SHELL")
   296  	if shellName != "" || runtime.GOOS == "windows" {
   297  		return strings.ToLower(filepath.Base(shellName)), true
   298  	}
   299  
   300  	ppid := os.Getppid()
   301  	cmd := exec.Command("ps", "-p", strconv.Itoa(ppid), "-o", "comm=")
   302  	ppName, err := cmd.Output()
   303  	if err != nil {
   304  		fatalIf(probe.NewError(err), "Failed to enable autocompletion. Cannot determine shell type and "+
   305  			"no SHELL environment variable found")
   306  	}
   307  	shellName = strings.TrimSpace(string(ppName))
   308  	return strings.ToLower(filepath.Base(shellName)), false
   309  }
   310  
   311  func installAutoCompletion() {
   312  	if runtime.GOOS == "windows" {
   313  		console.Infoln("autocompletion feature is not available for this operating system")
   314  		return
   315  	}
   316  
   317  	shellName, ok := getShellName()
   318  	if !ok {
   319  		console.Infoln("No 'SHELL' env var. Your shell is auto determined as '" + shellName + "'.")
   320  	} else {
   321  		console.Infoln("Your shell is set to '" + shellName + "', by env var 'SHELL'.")
   322  	}
   323  
   324  	supportedShellsSet := set.CreateStringSet("bash", "zsh", "fish")
   325  	if !supportedShellsSet.Contains(shellName) {
   326  		fatalIf(probe.NewError(errors.New("")),
   327  			"'"+shellName+"' is not a supported shell. "+
   328  				"Supported shells are: bash, zsh, fish")
   329  	}
   330  
   331  	e := completeinstall.Install(filepath.Base(os.Args[0]))
   332  	var printMsg string
   333  	if e != nil && strings.Contains(e.Error(), "* already installed") {
   334  		errStr := e.Error()[strings.Index(e.Error(), "\n")+1:]
   335  		re := regexp.MustCompile(`[::space::]*\*.*` + shellName + `.*`)
   336  		relatedMsg := re.FindStringSubmatch(errStr)
   337  		if len(relatedMsg) > 0 {
   338  			printMsg = "\n" + relatedMsg[0]
   339  		} else {
   340  			printMsg = ""
   341  		}
   342  	}
   343  	if printMsg != "" {
   344  		if completeinstall.IsInstalled(filepath.Base(os.Args[0])) || completeinstall.IsInstalled("mc") {
   345  			console.Infoln("autocompletion is enabled.", printMsg)
   346  		} else {
   347  			fatalIf(probe.NewError(e), "Unable to install auto-completion.")
   348  		}
   349  	} else {
   350  		console.Infoln("enabled autocompletion in your '" + shellName + "' rc file. Please restart your shell.")
   351  	}
   352  }
   353  
   354  func registerBefore(ctx *cli.Context) error {
   355  	deprecatedFlagsWarning(ctx)
   356  
   357  	if ctx.IsSet("config-dir") {
   358  		// Set the config directory.
   359  		setMcConfigDir(ctx.String("config-dir"))
   360  	} else if ctx.GlobalIsSet("config-dir") {
   361  		// Set the config directory.
   362  		setMcConfigDir(ctx.GlobalString("config-dir"))
   363  	}
   364  
   365  	// Set global flags.
   366  	setGlobalsFromContext(ctx)
   367  
   368  	// Migrate any old version of config / state files to newer format.
   369  	migrate()
   370  
   371  	// Initialize default config files.
   372  	initMC()
   373  
   374  	// Check if config can be read.
   375  	checkConfig()
   376  
   377  	return nil
   378  }
   379  
   380  // findClosestCommands to match a given string with commands trie tree.
   381  func findClosestCommands(commandsTree *trie.Trie, command string) []string {
   382  	closestCommands := commandsTree.PrefixMatch(command)
   383  	sort.Strings(closestCommands)
   384  	// Suggest other close commands - allow missed, wrongly added and even transposed characters
   385  	for _, value := range commandsTree.Walk(commandsTree.Root()) {
   386  		if sort.SearchStrings(closestCommands, value) < len(closestCommands) {
   387  			continue
   388  		}
   389  		// 2 is arbitrary and represents the max allowed number of typed errors
   390  		if words.DamerauLevenshteinDistance(command, value) < 2 {
   391  			closestCommands = append(closestCommands, value)
   392  		}
   393  	}
   394  	return closestCommands
   395  }
   396  
   397  // Check for updates and print a notification message
   398  func checkUpdate(ctx *cli.Context) {
   399  	// Do not print update messages, if quiet flag is set.
   400  	if ctx.Bool("quiet") || ctx.GlobalBool("quiet") {
   401  		// Its OK to ignore any errors during doUpdate() here.
   402  		if updateMsg, _, currentReleaseTime, latestReleaseTime, _, err := getUpdateInfo("", 2*time.Second); err == nil {
   403  			printMsg(updateMessage{
   404  				Status:  "success",
   405  				Message: updateMsg,
   406  			})
   407  		} else {
   408  			printMsg(updateMessage{
   409  				Status:  "success",
   410  				Message: prepareUpdateMessage("Run `mc update`", latestReleaseTime.Sub(currentReleaseTime)),
   411  			})
   412  		}
   413  	}
   414  }
   415  
   416  var appCmds = []cli.Command{
   417  	aliasCmd,
   418  	adminCmd,
   419  	anonymousCmd,
   420  	batchCmd,
   421  	cpCmd,
   422  	catCmd,
   423  	configCmd,
   424  	diffCmd,
   425  	duCmd,
   426  	encryptCmd,
   427  	eventCmd,
   428  	findCmd,
   429  	getCmd,
   430  	headCmd,
   431  	ilmCmd,
   432  	idpCmd,
   433  	licenseCmd,
   434  	legalHoldCmd,
   435  	lsCmd,
   436  	mbCmd,
   437  	mvCmd,
   438  	mirrorCmd,
   439  	odCmd,
   440  	pingCmd,
   441  	policyCmd,
   442  	pipeCmd,
   443  	putCmd,
   444  	quotaCmd,
   445  	rmCmd,
   446  	retentionCmd,
   447  	rbCmd,
   448  	replicateCmd,
   449  	readyCmd,
   450  	sqlCmd,
   451  	statCmd,
   452  	supportCmd,
   453  	shareCmd,
   454  	treeCmd,
   455  	tagCmd,
   456  	undoCmd,
   457  	updateCmd,
   458  	versionCmd,
   459  	watchCmd,
   460  }
   461  
   462  func printMCVersion(c *cli.Context) {
   463  	fmt.Fprintf(c.App.Writer, "%s version %s (commit-id=%s)\n", c.App.Name, c.App.Version, CommitID)
   464  	fmt.Fprintf(c.App.Writer, "Runtime: %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
   465  	fmt.Fprintf(c.App.Writer, "Copyright (c) 2015-%s MinIO, Inc.\n", CopyrightYear)
   466  	fmt.Fprintf(c.App.Writer, "License GNU AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>\n")
   467  }
   468  
   469  func registerApp(name string) *cli.App {
   470  	cli.HelpFlag = cli.BoolFlag{
   471  		Name:  "help, h",
   472  		Usage: "show help",
   473  	}
   474  
   475  	// Override default cli version printer
   476  	cli.VersionPrinter = printMCVersion
   477  
   478  	app := cli.NewApp()
   479  	app.Name = name
   480  	app.Action = func(ctx *cli.Context) error {
   481  		if strings.HasPrefix(ReleaseTag, "RELEASE.") {
   482  			// Check for new updates from dl.min.io.
   483  			checkUpdate(ctx)
   484  		}
   485  
   486  		if ctx.Bool("autocompletion") || ctx.GlobalBool("autocompletion") {
   487  			// Install shell completions
   488  			installAutoCompletion()
   489  			return nil
   490  		}
   491  
   492  		if ctx.Args().First() == "" {
   493  			showAppHelpAndExit(ctx)
   494  		}
   495  
   496  		commandNotFound(ctx, app.Commands)
   497  		return exitStatus(globalErrorExitStatus)
   498  	}
   499  
   500  	app.Before = registerBefore
   501  	app.HideHelpCommand = true
   502  	app.Usage = "MinIO Client for object storage and filesystems."
   503  	app.Commands = appCmds
   504  	app.Author = "MinIO, Inc."
   505  	app.Version = ReleaseTag
   506  	app.Flags = append(mcFlags, globalFlags...)
   507  	app.CustomAppHelpTemplate = mcHelpTemplate
   508  	app.EnableBashCompletion = true
   509  	app.OnUsageError = onUsageError
   510  
   511  	if isTerminal() && !globalPagerDisabled {
   512  		app.HelpWriter = globalHelpPager
   513  	} else {
   514  		app.HelpWriter = os.Stdout
   515  	}
   516  
   517  	return app
   518  }
   519  
   520  // mustGetProfilePath must get location that the profile will be written to.
   521  func mustGetProfileDir() string {
   522  	return filepath.Join(mustGetMcConfigDir(), globalProfileDir)
   523  }
   524  
   525  func showCommandHelpAndExit(cliCtx *cli.Context, code int) {
   526  	cli.ShowCommandHelp(cliCtx, cliCtx.Command.Name)
   527  	// Wait until the user quits the pager
   528  	globalHelpPager.WaitForExit()
   529  	os.Exit(code)
   530  }
   531  
   532  func showAppHelpAndExit(cliCtx *cli.Context) {
   533  	cli.ShowAppHelp(cliCtx)
   534  	// Wait until the user quits the pager
   535  	globalHelpPager.WaitForExit()
   536  	os.Exit(globalErrorExitStatus)
   537  }