github.com/secman-team/gh-api@v1.8.2/core/config/config_file.go (about)

     1  package config
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"syscall"
    12  
    13  	"github.com/mitchellh/go-homedir"
    14  	"gopkg.in/yaml.v3"
    15  )
    16  
    17  const (
    18  	GH_CONFIG_DIR = "GH_CONFIG_DIR"
    19  )
    20  
    21  func ConfigDir() string {
    22  	if v := os.Getenv(GH_CONFIG_DIR); v != "" {
    23  		return v
    24  	}
    25  
    26  	homeDir, _ := homeDirAutoMigrate()
    27  	return homeDir
    28  }
    29  
    30  func ConfigFile() string {
    31  	return path.Join(ConfigDir(), "config.yml")
    32  }
    33  
    34  func HostsConfigFile() string {
    35  	return path.Join(ConfigDir(), "hosts.yml")
    36  }
    37  
    38  func ParseDefaultConfig() (Config, error) {
    39  	return parseConfig(ConfigFile())
    40  }
    41  
    42  func HomeDirPath(subdir string) (string, error) {
    43  	homeDir, err := os.UserHomeDir()
    44  	if err != nil {
    45  		// TODO: remove go-homedir fallback in GitHub CLI v2
    46  		if legacyDir, err := homedir.Dir(); err == nil {
    47  			return filepath.Join(legacyDir, subdir), nil
    48  		}
    49  		return "", err
    50  	}
    51  
    52  	newPath := filepath.Join(homeDir, subdir)
    53  	if s, err := os.Stat(newPath); err == nil && s.IsDir() {
    54  		return newPath, nil
    55  	}
    56  
    57  	// TODO: remove go-homedir fallback in GitHub CLI v2
    58  	if legacyDir, err := homedir.Dir(); err == nil {
    59  		legacyPath := filepath.Join(legacyDir, subdir)
    60  		if s, err := os.Stat(legacyPath); err == nil && s.IsDir() {
    61  			return legacyPath, nil
    62  		}
    63  	}
    64  
    65  	return newPath, nil
    66  }
    67  
    68  // Looks up the `~/.config/gh` directory with backwards-compatibility with go-homedir and auto-migration
    69  // when an old homedir location was found.
    70  func homeDirAutoMigrate() (string, error) {
    71  	homeDir, err := os.UserHomeDir()
    72  	if err != nil {
    73  		// TODO: remove go-homedir fallback in GitHub CLI v2
    74  		if legacyDir, err := homedir.Dir(); err == nil {
    75  			return filepath.Join(legacyDir, ".config", "gh"), nil
    76  		}
    77  		return "", err
    78  	}
    79  
    80  	newPath := filepath.Join(homeDir, ".config", "gh")
    81  	_, newPathErr := os.Stat(newPath)
    82  	if newPathErr == nil || !os.IsNotExist(err) {
    83  		return newPath, newPathErr
    84  	}
    85  
    86  	// TODO: remove go-homedir fallback in GitHub CLI v2
    87  	if legacyDir, err := homedir.Dir(); err == nil {
    88  		legacyPath := filepath.Join(legacyDir, ".config", "gh")
    89  		if s, err := os.Stat(legacyPath); err == nil && s.IsDir() {
    90  			_ = os.MkdirAll(filepath.Dir(newPath), 0755)
    91  			return newPath, os.Rename(legacyPath, newPath)
    92  		}
    93  	}
    94  
    95  	return newPath, nil
    96  }
    97  
    98  var ReadConfigFile = func(filename string) ([]byte, error) {
    99  	f, err := os.Open(filename)
   100  	if err != nil {
   101  		return nil, pathError(err)
   102  	}
   103  	defer f.Close()
   104  
   105  	data, err := ioutil.ReadAll(f)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	return data, nil
   111  }
   112  
   113  var WriteConfigFile = func(filename string, data []byte) error {
   114  	err := os.MkdirAll(path.Dir(filename), 0771)
   115  	if err != nil {
   116  		return pathError(err)
   117  	}
   118  
   119  	cfgFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) // cargo coded from setup
   120  	if err != nil {
   121  		return err
   122  	}
   123  	defer cfgFile.Close()
   124  
   125  	n, err := cfgFile.Write(data)
   126  	if err == nil && n < len(data) {
   127  		err = io.ErrShortWrite
   128  	}
   129  
   130  	return err
   131  }
   132  
   133  var BackupConfigFile = func(filename string) error {
   134  	return os.Rename(filename, filename+".bak")
   135  }
   136  
   137  func parseConfigFile(filename string) ([]byte, *yaml.Node, error) {
   138  	data, err := ReadConfigFile(filename)
   139  	if err != nil {
   140  		return nil, nil, err
   141  	}
   142  
   143  	root, err := parseConfigData(data)
   144  	if err != nil {
   145  		return nil, nil, err
   146  	}
   147  	return data, root, err
   148  }
   149  
   150  func parseConfigData(data []byte) (*yaml.Node, error) {
   151  	var root yaml.Node
   152  	err := yaml.Unmarshal(data, &root)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	if len(root.Content) == 0 {
   158  		return &yaml.Node{
   159  			Kind:    yaml.DocumentNode,
   160  			Content: []*yaml.Node{{Kind: yaml.MappingNode}},
   161  		}, nil
   162  	}
   163  	if root.Content[0].Kind != yaml.MappingNode {
   164  		return &root, fmt.Errorf("expected a top level map")
   165  	}
   166  	return &root, nil
   167  }
   168  
   169  func isLegacy(root *yaml.Node) bool {
   170  	for _, v := range root.Content[0].Content {
   171  		if v.Value == "github.com" {
   172  			return true
   173  		}
   174  	}
   175  
   176  	return false
   177  }
   178  
   179  func migrateConfig(filename string) error {
   180  	b, err := ReadConfigFile(filename)
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	var hosts map[string][]yaml.Node
   186  	err = yaml.Unmarshal(b, &hosts)
   187  	if err != nil {
   188  		return fmt.Errorf("error decoding legacy format: %w", err)
   189  	}
   190  
   191  	cfg := NewBlankConfig()
   192  	for hostname, entries := range hosts {
   193  		if len(entries) < 1 {
   194  			continue
   195  		}
   196  		mapContent := entries[0].Content
   197  		for i := 0; i < len(mapContent)-1; i += 2 {
   198  			if err := cfg.Set(hostname, mapContent[i].Value, mapContent[i+1].Value); err != nil {
   199  				return err
   200  			}
   201  		}
   202  	}
   203  
   204  	err = BackupConfigFile(filename)
   205  	if err != nil {
   206  		return fmt.Errorf("failed to back up existing config: %w", err)
   207  	}
   208  
   209  	return cfg.Write()
   210  }
   211  
   212  func parseConfig(filename string) (Config, error) {
   213  	_, root, err := parseConfigFile(filename)
   214  	if err != nil {
   215  		if os.IsNotExist(err) {
   216  			root = NewBlankRoot()
   217  		} else {
   218  			return nil, err
   219  		}
   220  	}
   221  
   222  	if isLegacy(root) {
   223  		err = migrateConfig(filename)
   224  		if err != nil {
   225  			return nil, fmt.Errorf("error migrating legacy config: %w", err)
   226  		}
   227  
   228  		_, root, err = parseConfigFile(filename)
   229  		if err != nil {
   230  			return nil, fmt.Errorf("failed to reparse migrated config: %w", err)
   231  		}
   232  	} else {
   233  		if _, hostsRoot, err := parseConfigFile(HostsConfigFile()); err == nil {
   234  			if len(hostsRoot.Content[0].Content) > 0 {
   235  				newContent := []*yaml.Node{
   236  					{Value: "hosts"},
   237  					hostsRoot.Content[0],
   238  				}
   239  				restContent := root.Content[0].Content
   240  				root.Content[0].Content = append(newContent, restContent...)
   241  			}
   242  		} else if !errors.Is(err, os.ErrNotExist) {
   243  			return nil, err
   244  		}
   245  	}
   246  
   247  	return NewConfig(root), nil
   248  }
   249  
   250  func pathError(err error) error {
   251  	var pathError *os.PathError
   252  	if errors.As(err, &pathError) && errors.Is(pathError.Err, syscall.ENOTDIR) {
   253  		if p := findRegularFile(pathError.Path); p != "" {
   254  			return fmt.Errorf("remove or rename regular file `%s` (must be a directory)", p)
   255  		}
   256  
   257  	}
   258  	return err
   259  }
   260  
   261  func findRegularFile(p string) string {
   262  	for {
   263  		if s, err := os.Stat(p); err == nil && s.Mode().IsRegular() {
   264  			return p
   265  		}
   266  		newPath := path.Dir(p)
   267  		if newPath == p || newPath == "/" || newPath == "." {
   268  			break
   269  		}
   270  		p = newPath
   271  	}
   272  	return ""
   273  }