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 }