Files
s3-cli/cmd/config.go
zeekay b34e2a3c37 Rebrand: Hanzo S3 CLI -- remove all MinIO branding
- Replace copyright headers: "MinIO, Inc." -> "Hanzo AI, Inc." across 411+ Go files
- Replace "MinIO Object Storage stack" -> "Hanzo S3 stack" in all file headers
- Replace all user-facing "MinIO" references -> "Hanzo S3" in Usage, help text, examples
- Replace example alias "myminio" -> "mys3" throughout
- Replace example endpoints play.min.io -> s3.hanzo.ai, dl.min.io -> s3.hanzo.ai
- Replace doc URLs min.io -> hanzo.space
- Rewrite README.md/README_zh_CN.md: product is "Hanzo S3 CLI" (the s3 command)
- Update CONTRIBUTING.md, CONFLICT.md, NOTICE, code_of_conduct.md
- Update all Dockerfiles: labels, entrypoints, image refs -> ghcr.io/hanzos3/cli
- Update Makefile: build output, docker tags, install paths -> s3
- Update docker-buildx.sh: image tags -> ghcr.io/hanzos3/cli
- DO NOT change go.mod module path or import paths (github.com/minio/mc preserved)
- DO NOT change Go identifiers, SDK types, or wire protocol constants
- All unit tests pass
2026-02-21 14:19:07 -08:00

352 lines
9.7 KiB
Go

// Copyright (c) 2015-2022 Hanzo AI, Inc.
//
// This file is part of Hanzo S3 stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"bufio"
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/minio/mc/pkg/probe"
"github.com/minio/pkg/v3/env"
"github.com/mitchellh/go-homedir"
)
// mcCustomConfigDir contains the whole path to config dir. Only access via get/set functions.
var mcCustomConfigDir string
// setMcConfigDir - set a custom Hanzo S3 Client config folder.
func setMcConfigDir(configDir string) {
mcCustomConfigDir = configDir
}
// getMcConfigDir - construct Hanzo S3 Client config folder.
func getMcConfigDir() (string, *probe.Error) {
if mcCustomConfigDir != "" {
return mcCustomConfigDir, nil
}
homeDir, e := homedir.Dir()
if e != nil {
return "", probe.NewError(e)
}
configDir := filepath.Join(homeDir, defaultMCConfigDir())
return configDir, nil
}
// Return default default mc config directory.
// Generally you want to use getMcConfigDir which returns custom overrides.
func defaultMCConfigDir() string {
if runtime.GOOS == "windows" {
// For windows the path is slightly different
cmd := filepath.Base(os.Args[0])
if strings.HasSuffix(strings.ToLower(cmd), ".exe") {
cmd = cmd[:strings.LastIndex(cmd, ".")]
}
return fmt.Sprintf("%s\\", cmd)
}
return fmt.Sprintf(".%s/", filepath.Base(os.Args[0]))
}
// mustGetMcConfigDir - construct Hanzo S3 Client config folder or fail
func mustGetMcConfigDir() (configDir string) {
configDir, err := getMcConfigDir()
fatalIf(err.Trace(), "Unable to get mcConfigDir.")
return configDir
}
// createMcConfigDir - create Hanzo S3 Client config folder
func createMcConfigDir() *probe.Error {
p, err := getMcConfigDir()
if err != nil {
return err.Trace()
}
if e := os.MkdirAll(p, 0o700); e != nil {
return probe.NewError(e)
}
return nil
}
// getMcConfigPath - construct Hanzo S3 Client configuration path
func getMcConfigPath() (string, *probe.Error) {
if mcCustomConfigDir != "" {
return filepath.Join(mcCustomConfigDir, globalMCConfigFile), nil
}
dir, err := getMcConfigDir()
if err != nil {
return "", err.Trace()
}
return filepath.Join(dir, globalMCConfigFile), nil
}
// mustGetMcConfigPath - similar to getMcConfigPath, ignores errors
func mustGetMcConfigPath() string {
path, err := getMcConfigPath()
fatalIf(err.Trace(), "Unable to get mcConfigPath.")
return path
}
// newMcConfig - initializes a new version '10' config.
func newMcConfig() *configV10 {
cfg := newConfigV10()
cfg.loadDefaults()
return cfg
}
// loadMcConfigCached - returns loadMcConfig with a closure for config cache.
func loadMcConfigFactory() func() (*configV10, *probe.Error) {
// Load once and cache in a closure.
cfgCache, err := loadConfigV10()
// loadMcConfig - reads configuration file and returns config.
return func() (*configV10, *probe.Error) {
return cfgCache, err
}
}
// loadMcConfig - returns configuration, initialized later.
var loadMcConfig func() (*configV10, *probe.Error)
// saveMcConfig - saves configuration file and returns error if any.
func saveMcConfig(config *configV10) *probe.Error {
if config == nil {
return errInvalidArgument().Trace()
}
err := createMcConfigDir()
if err != nil {
return err.Trace(mustGetMcConfigDir())
}
// Save the config.
if err := saveConfigV10(config); err != nil {
return err.Trace(mustGetMcConfigPath())
}
// Refresh the config cache.
loadMcConfig = loadMcConfigFactory()
return nil
}
// isMcConfigExists returns err if config doesn't exist.
func isMcConfigExists() bool {
configFile, err := getMcConfigPath()
if err != nil {
return false
}
if _, e := os.Stat(configFile); e != nil {
return false
}
return true
}
// cleanAlias removes any forbidden trailing slashes or backslashes
// before any validation to avoid annoying mc complaints.
func cleanAlias(s string) string {
s = strings.TrimSuffix(s, "/")
s = strings.TrimSuffix(s, "\\")
return s
}
// isValidAlias - Check if alias valid.
func isValidAlias(alias string) bool {
return regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9-_]*$").MatchString(alias)
}
// getAliasConfig retrieves host specific configuration such as access keys, signature type.
func getAliasConfig(alias string) (*aliasConfigV10, *probe.Error) {
mcCfg, err := loadMcConfig()
if err != nil {
return nil, err.Trace(alias)
}
// if host is exact return quickly.
if _, ok := mcCfg.Aliases[alias]; ok {
hostCfg := mcCfg.Aliases[alias]
hostCfg.Src = mustGetMcConfigPath()
return &hostCfg, nil
}
// return error if cannot be matched.
return nil, errNoMatchingHost(alias).Trace(alias)
}
// mustGetHostConfig retrieves host specific configuration such as access keys, signature type.
func mustGetHostConfig(alias string) *aliasConfigV10 {
// look for it in the environment variable first.
aliasCfg, _ := expandAliasFromEnv(env.Get(mcEnvHostPrefix+alias, ""))
// If alias is not found,
// look for it in the customized configuration.
if aliasCfg == nil {
aliasCfg = aliasToConfigMap[alias]
}
if aliasCfg == nil {
aliasCfg, _ = getAliasConfig(alias)
}
return aliasCfg
}
var (
hostKeys = regexp.MustCompile("^(https?://)(.*?):(.*)@(.*?)$")
hostKeyTokens = regexp.MustCompile("^(https?://)(.*?):(.*?):(.*)@(.*?)$")
)
// parse url usually obtained from env.
func parseEnvURLStr(envURL string) (*url.URL, string, string, string, *probe.Error) {
var accessKey, secretKey, sessionToken string
var parsedURL string
if hostKeyTokens.MatchString(envURL) {
parts := hostKeyTokens.FindStringSubmatch(envURL)
if len(parts) != 6 {
return nil, "", "", "", errInvalidArgument().Trace(envURL)
}
accessKey = parts[2]
secretKey = parts[3]
sessionToken = parts[4]
parsedURL = fmt.Sprintf("%s%s", parts[1], parts[5])
} else if hostKeys.MatchString(envURL) {
parts := hostKeys.FindStringSubmatch(envURL)
if len(parts) != 5 {
return nil, "", "", "", errInvalidArgument().Trace(envURL)
}
accessKey = parts[2]
secretKey = parts[3]
parsedURL = fmt.Sprintf("%s%s", parts[1], parts[4])
}
var u *url.URL
var e error
if parsedURL != "" {
u, e = url.Parse(parsedURL)
} else {
u, e = url.Parse(envURL)
}
if e != nil {
return nil, "", "", "", probe.NewError(e)
}
// Look for if URL has invalid values and return error.
if (u.Scheme != "http" && u.Scheme != "https") ||
(u.Path != "/" && u.Path != "") ||
u.Opaque != "" ||
u.ForceQuery ||
u.RawQuery != "" ||
u.Fragment != "" {
return nil, "", "", "", errInvalidArgument().Trace(u.String())
}
if accessKey == "" && secretKey == "" {
if u.User != nil {
accessKey = u.User.Username()
secretKey, _ = u.User.Password()
}
}
return u, accessKey, secretKey, sessionToken, nil
}
const (
mcEnvHostPrefix = "MC_HOST_"
mcEnvConfigFile = "MC_CONFIG_ENV_FILE"
)
var aliasToConfigMap = make(map[string]*aliasConfigV10)
func readAliasesFromFile(envConfigFile string) *probe.Error {
r, e := os.Open(envConfigFile)
if e != nil {
return probe.NewError(e).Trace(envConfigFile)
}
defer r.Close()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
envLine := scanner.Text()
strs := strings.SplitN(envLine, "=", 2)
if len(strs) != 2 {
return probe.NewError(fmt.Errorf("parsing error at %s", envLine)).Trace(envConfigFile)
}
alias := strings.TrimPrefix(strs[0], mcEnvHostPrefix)
if len(alias) == 0 {
return probe.NewError(fmt.Errorf("parsing error at %s", envLine)).Trace(envConfigFile)
}
aliasConfig, err := expandAliasFromEnv(strs[1])
if err != nil {
return err.Trace(envLine)
}
aliasConfig.Src = envConfigFile
aliasToConfigMap[alias] = aliasConfig
}
if e := scanner.Err(); e != nil {
return probe.NewError(e).Trace(envConfigFile)
}
return nil
}
func expandAliasFromEnv(envURL string) (*aliasConfigV10, *probe.Error) {
u, accessKey, secretKey, sessionToken, err := parseEnvURLStr(envURL)
if err != nil {
return nil, err.Trace(envURL)
}
return &aliasConfigV10{
URL: u.String(),
API: "S3v4",
AccessKey: accessKey,
SecretKey: secretKey,
SessionToken: sessionToken,
Src: "env",
}, nil
}
// expandAlias expands aliased URL if any match is found, returns as is otherwise.
func expandAlias(aliasedURL string) (alias, urlStr string, aliasCfg *aliasConfigV10, err *probe.Error) {
// Extract alias from the URL.
alias, path := url2Alias(aliasedURL)
if env.IsSet(mcEnvHostPrefix + alias) {
aliasCfg, err = expandAliasFromEnv(env.Get(mcEnvHostPrefix+alias, ""))
if err != nil {
return "", "", nil, err.Trace(aliasedURL)
}
return alias, urlJoinPath(aliasCfg.URL, path), aliasCfg, nil
}
aliasCfg = aliasToConfigMap[alias]
if aliasCfg != nil {
return alias, urlJoinPath(aliasCfg.URL, path), aliasCfg, nil
}
// Find the matching alias entry and expand the URL.
if aliasCfg = mustGetHostConfig(alias); aliasCfg != nil {
return alias, urlJoinPath(aliasCfg.URL, path), aliasCfg, nil
}
return "", aliasedURL, nil, nil // No matching entry found. Return original URL as is.
}
// mustExpandAlias expands aliased URL if any match is found, returns as is otherwise.
func mustExpandAlias(aliasedURL string) (alias, urlStr string, aliasCfg *aliasConfigV10) {
alias, urlStr, aliasCfg, _ = expandAlias(aliasedURL)
return alias, urlStr, aliasCfg
}