Browse Source

Added files ported from another project.

master
Benjamin Shelton 1 year ago
parent
commit
939f7c2a87
6 changed files with 415 additions and 0 deletions
  1. +1
    -0
      .gitignore
  2. +106
    -0
      config.go
  3. +92
    -0
      config.sample.yaml
  4. +176
    -0
      config_test.go
  5. +3
    -0
      config_windows_test.go
  6. +37
    -0
      path.go

+ 1
- 0
.gitignore View File

@@ -0,0 +1 @@
config.yaml

+ 106
- 0
config.go View File

@@ -0,0 +1,106 @@
// Configuration file helpers.

package fileutils

import (
"os"
"path/filepath"
"strings"
)

// DefaultConfig is the default package-level configuration which FindConfig
// will attempt to locate. You should change this to whatever your default
// configuration file is named by setting `fileutils.DefaultConfig = "value"`.
var DefaultConfig = "config.yaml"

// FindConfig locates a configuration file (default: "config.yaml") and returns
// the absolute path of its location.
//
// This routine starts by examining the following locations, in order of
// precedence:
//
// 1) The current directory.
// 2) (Linux) $XDG_CONFIG_HOME is searched, followed by each directory listed in
// $XDG_CONFIG_DIRS. If none of these values is set, the user's
// ~/.config/`base` directory is examined.
// 3) (Windows) The directory %APPDATA%/`base` is searched for the specific
// configuration, if applicable.
// 4) (Linux) Failing the contents of the user directories and the current
// working directory, the last place this will look is in /etc/`base`
//
// (where the string `base`, above, is replaced with the value of FindConfig's
// second argument.)
//
// If the file cannot be located, the empty string will be returned.
//
// For the Windows implementation, see util_windows.go.
func FindConfig(name, base string) string {
var path string
var err error

if name == "" {
name = DefaultConfig
}

if name[0] == '/' {
// If the path contains symlinks, we'll get the "correct" path instead.
path, err = filepath.Abs(name)
if err != nil {
path = name
}
if stat, err := os.Stat(path); !os.IsNotExist(err) {
if stat != nil && !stat.IsDir() {
return path
}
}
}

etc := filepath.Join("/etc", base)
xdgHome := os.ExpandEnv(filepath.Join("${HOME}/.config", base))

// Check current directory.
cwd, err := os.Getwd()
if err == nil {
if stat, err := os.Stat(filepath.Join(cwd, name)); !os.IsNotExist(err) {
if stat != nil && !stat.IsDir() {
return filepath.Join(cwd, name)
}
}
}

if env, exists := os.LookupEnv("XDG_CONFIG_HOME"); exists {
path = AbsPath(os.ExpandEnv(env))
if stat, err := os.Stat(filepath.Join(path, name)); !os.IsNotExist(err) {
if stat != nil && !stat.IsDir() {
return filepath.Join(env, name)
}
}
}

if env, exists := os.LookupEnv("XDG_CONFIG_DIRS"); exists {
env = os.ExpandEnv(env)
for _, path := range strings.Split(env, ":") {
path = AbsPath(path)
if stat, err := os.Stat(filepath.Join(path, name)); !os.IsNotExist(err) {
if stat != nil && !stat.IsDir() {
return filepath.Join(path, name)
}
}
}
}

if stat, err := os.Stat(filepath.Join(xdgHome, name)); !os.IsNotExist(err) {
if stat != nil && !stat.IsDir() {
return filepath.Join(xdgHome, name)
}
}

if stat, err := os.Stat(filepath.Join(etc, name)); !os.IsNotExist(err) {
if stat != nil && !stat.IsDir() {
return filepath.Join(etc, name)
}
}

// If we cannot find the file, we'll return the empty string.
return ""
}

+ 92
- 0
config.sample.yaml View File

@@ -0,0 +1,92 @@
# File utilities unit test configuration.
#
# This configuration is not likely to be used for all unit tests, but you should
# read the appropriate sections carefully. Unit test failures will be reported
# when a) this file is missing and b) this file is not configured for your test
# environment.
#
# WE WILL NOT ACCEPT BUG REPORTS FOR FAILING UNIT TESTS CAUSED BY INDIVIDUALS
# WHO DID NOT READ THE DOCUMENTATION FOR TESTING CONFIGURATION(S).

app:

# FindConfig tests.
#
# Read the following section carefully along with each subsection if you're
# running tests. Failure to change values to locations appropriate for your
# test environment will cause FindConfig tests to fail.
#
# WE WILL NOT ACCEPT BUG REPORTS FOR FAILING UNIT TESTS CAUSED BY INDIVIDUALS
# WHO DID NOT READ THIS SECTION.
#
# Pay particular attention to each location specified in the config herewith.
# Specifically, note that the *entire* path is used. This means that paths are
# interpolated via filepath.Abs() to obtain the absolute path, then the
# containing directory and the file name are split out into their own parts.
# What we do with each component depends on the test itself. For tests that
# require an environment variable to be set, we'll set it to the contents of
# the interpolated directory. For tests that include a -base suffice, we pass
# the contents of -base into FindConfig's argument of the same name.
find-config:
# Absolute path test. This should bail before any other file location tests
# are attempted.
abs: "/full/path/to/file/change.me"

# We use the sample YAML application configuration that comes with our app.
cwd: ./config.sample.yaml

# Here we use the same value as above since we cannot guarantee similar
# files across environments (one option is ~/.config/fontconfig, but that's
# only likely to exist if you have an X server installed). The unit tests
# work by taking the absolute path of this location, splitting out the root,
# stting it to the XDG_CONFIG_HOME environment variable, and then running
# the config finder.
xdg-config-home: ./app/util_test.go

# For XDG_CONFIG_DIRS, we need to be somewhat more creative. As a
# consequence, we're now entering terrain that is unlikely to be true across
# all systems. For this reason, you may need to change the sammple value
# (below) for ~/.config/fontconfig to somewhere else.
xdg-config-dirs:
- ./config.sample.yaml
- ~/.config/fontconfig

# This configuration controls the manually-tested ~/.config code path which
# is usually reached when both XDG_CONFIG_HOME and XDG_CONFIG_DIRS are
# unset. As with xdg-config-dirs (above), the same caveat applies. If
# ~/.config/fontconfig doesn't exist, you'll need to create it. However,
# bear in mind that for this particular test, FindConfig actually uses the
# hard-cooded path ~/.config. If you genuinely wish to test this, ensure the
# directory ~/.config has been created and populate it with a file for which
# you wish to test its presence below.
xdg-config-home-forced: ~/.config/fontconfig

# Controls the argument for `base` in FindConfig. If this is the empty
# string, no paths are appended before examining the directory for the
# basename of the file specified in xdg-config-home-forced.
#
# For example, if your application typically looks under ~/.config/app for
# its configuration file `config.json`, to emulate this, you would set the
# value of xdg-config-home-forced to ~/.config/app/config.json and then set
# this value to "app". This is because the value of `base` in FindConfig is
# always appended to the root "${HOME}/.config", which is hard coded.
xdg-config-home-base: ""

# For the /etc test, we pick a file that's fairly common across most Linux
# distributions. A better option for more widely supported UNIX and
# UNIX-like systems might be to test for /etc/resolv.conf. Either way, the
# same requirements apply here as with the prior tests: The file should
# exist, and it should exist in the hard-coded path /etc.
etc: /etc/os-release

# As with xdg-config-home-base, this controls the value of `base` passed
# into FindConfig. If this is anything but the empty string, it is appended
# to /etc. For example, if you were emulating a configuration file for your
# application, the value of etc (above) should read
# "/etc/appname/config.json" and the value of this should be set to
# "appname". This is because FindConfig simply looks in /etc for the
# specified file, but the "base" directory under /etc can be specific
# separately (and in fact the server and client both pass in "appname" for
# the value of `base`).
etc-base: ""


+ 176
- 0
config_test.go View File

@@ -0,0 +1,176 @@
// This particular file tests whatever functions are defined in util.go. For the Windows equivalent, please see util_windows_test.

package fileutils_test

import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"

"git.pluggableideas.com/destrealm/go/fileutils"
"gopkg.in/yaml.v2"
)

// For the unit tests involving configuration discovery, most everything should
// be done automatically, but you'll need to do a couple of things first. In
// particular, you'll need to copy the config.sample.yaml file to config.yaml
// and edit it according to your own local configuration. For XDG_CONFIG_HOME
// (which resides in ~/.config), you may need to either create the directory
// ~/.config or you may need to pick a file within. Follow the instructions in
// the YAML file for more specific information.

type App struct {
App struct {
FindConfig *FindConfig `yaml:"find-config"`
} `yaml:"app"`
}

type FindConfig struct {
Abs string `yaml:"abs"`
CWD string
XDGConfigHome string `yaml:"xdg-config-home"`
XDGConfigDirs []string `yaml:"xdg-config-dirs"`
XDGConfigHomeForced string `yaml:"xdg-config-home-forced"`
XDGConfigHomeBase string `yaml:"xdg-config-home-base"`
Etc string
EtcBase string `yaml:"etc-base"`
}

func readConfig(path string) (*FindConfig, error) {
config := &App{}
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}

if err := yaml.Unmarshal(contents, config); err != nil {
return nil, err
}

return config.App.FindConfig, nil
}

func Test_FindConfig(t *testing.T) {
// Change to parent directory, because this unit test runs under `app`.
if err := os.Chdir("../"); err != nil {
t.Error("unable to chdir to parent:", err)
return
}

// TODO: Actually fix path introspection for the test configuration. This
// should probably be done globally in the future, such as in a top-level test
// somewhere.
config, err := readConfig("config.yaml")
if err != nil {
t.Error("unable to read unit test configuration:", err)
return
}

// Test absolute path matches. What this should do in FindConfig is bail and return the absolute path (corrected, if need be, such as if the absolute path specified in the test points to a symlink) before any other tests are performed.
path, err := filepath.Abs(config.Abs)
if err != nil {
t.Errorf("error reading absolute path for file %v", config.Abs)
}
test := app.FindConfig(path, "")
if strings.Compare(test, path) != 0 {
t.Error("located paths don't match")
t.Logf("wanted: %v", path)
t.Logf("found: %v", test)
}

// Test matches in CWD.
path, err = filepath.Abs(config.CWD)
if err != nil {
t.Errorf("error getting absolute path for file %v", config.CWD)
}

test = app.FindConfig(filepath.Base(path), "")
if strings.Compare(test, path) != 0 {
t.Error("located paths don't match")
t.Logf("wanted: %v", path)
t.Logf("found: %v", test)
}

// Test XDG_CONFIG_HOME. Requires changing the environment variable. We
// attempt to reconfigure it both if an error occur as well as after we're
// finished with the test.
path, err = filepath.Abs(config.XDGConfigHome)
env, envExists := os.LookupEnv("XDG_CONFIG_HOME")
if err = os.Setenv("XDG_CONFIG_HOME", filepath.Dir(path)); err != nil {
t.Error("could not set the environment variable XDG_CONFIG_HOME:", err)
if envExists {
_ = os.Setenv("XDG_CONFIG_HOME", env)
}
}

test = app.FindConfig(filepath.Base(path), "")
if strings.Compare(test, path) != 0 {
t.Error("located paths don't match")
t.Logf("wanted: %v", path)
t.Logf("found: %v", test)
}

// Reset environment.
if envExists {
_ = os.Setenv("XDG_CONFIG_HOME", env)
}

// Test XDG_CONFIG_DIRS. Requires changing the environment variable. As with
// the previous rendition(s), we attempt to reconfigure it both if an error
// occurs as well as after we've finished with the test. This will reset the
// value to its prior state.
dirs := make([]string, len(config.XDGConfigDirs))
for i, dir := range config.XDGConfigDirs {
dirs[i] = filepath.Dir(app.AbsPath(dir))
}

env, envExists = os.LookupEnv("XDG_CONFIG_DIRS")
if err = os.Setenv("XDG_CONFIG_DIRS", strings.Join(dirs, ":")); err != nil {
t.Error("could not set the environment variable XDG_CONFIG_DIRS:", err)
if envExists {
_ = os.Setenv("XDG_CONFIG_DIRS", env)
}
}

for _, path := range config.XDGConfigDirs {
path = app.AbsPath(path)
if err != nil {
t.Logf("failed reading absolute path for path %v; skipping", path)
continue
}
test = app.FindConfig(filepath.Base(path), "")
if strings.Compare(test, path) != 0 {
t.Error("located paths don't match")
t.Logf("wanted: %v", path)
t.Logf("found: %v", test)
}

// Reset environment.
if envExists {
_ = os.Setenv("XDG_CONFIG_DIRS", env)
}
}

// Test the hard-coded .config path. If xdg-config-home-base is supplied,
// we'll use that as the base directory and pass it in as FindConfig's second
// argument.
path = app.AbsPath(config.XDGConfigHomeForced)
test = app.FindConfig(filepath.Base(config.XDGConfigHomeForced),
config.XDGConfigHomeBase)
if strings.Compare(test, path) != 0 {
t.Error("located paths don't match")
t.Logf("wanted: %v", path)
t.Logf("found: %v", test)
}

// Test FindConfig's behavior with /etc.
path, _ = filepath.Abs(config.Etc)
test = app.FindConfig(filepath.Base(config.Etc), config.EtcBase)
if strings.Compare(test, path) != 0 {
t.Error("located paths don't match")
t.Logf("wanted: %v", path)
t.Logf("found: %v", test)
}
}

+ 3
- 0
config_windows_test.go View File

@@ -0,0 +1,3 @@
// +build windows

package fileutils_test

+ 37
- 0
path.go View File

@@ -0,0 +1,37 @@
// Application utilities.

package fileutils

import (
"os"
"path/filepath"
)

// AbsPath expands and calculates some UNIX-specific path expansions. At the
// time of this writing, this is limited to ~ for the user's current HOME
// directory, which is deduced from the value of the $HOME environment variable.
//
// Behavior under other operating systems may differ.
func AbsPath(s string) string {
var err error
var path string
home, exists := os.LookupEnv("HOME")
if exists {
if s[0] == '~' {
s = s[1:]
if home[len(home)-1] != '/' {
home = home + "/"
}
if s[0] == '/' {
s = home + s[1:]
} else {
s = home + s
}
}
}

if path, err = filepath.Abs(s); err != nil {
return s
}
return path
}

Loading…
Cancel
Save