Skip to content

Commit

Permalink
Merge pull request #27 from adrg/windows-known-folders
Browse files Browse the repository at this point in the history
Add Known Folders support on Windows
  • Loading branch information
adrg committed Oct 27, 2021
2 parents addede4 + 2928c45 commit 210f365
Show file tree
Hide file tree
Showing 24 changed files with 922 additions and 348 deletions.
154 changes: 105 additions & 49 deletions README.md

Large diffs are not rendered by default.

22 changes: 12 additions & 10 deletions base_dirs.go
@@ -1,5 +1,7 @@
package xdg

import "github.com/adrg/xdg/internal/pathutil"

// XDG Base Directory environment variables.
const (
envDataHome = "XDG_DATA_HOME"
Expand All @@ -26,41 +28,41 @@ type baseDirectories struct {
}

func (bd baseDirectories) dataFile(relPath string) (string, error) {
return createPath(relPath, append([]string{bd.dataHome}, bd.data...))
return pathutil.Create(relPath, append([]string{bd.dataHome}, bd.data...))
}

func (bd baseDirectories) configFile(relPath string) (string, error) {
return createPath(relPath, append([]string{bd.configHome}, bd.config...))
return pathutil.Create(relPath, append([]string{bd.configHome}, bd.config...))
}

func (bd baseDirectories) stateFile(relPath string) (string, error) {
return createPath(relPath, []string{bd.stateHome})
return pathutil.Create(relPath, []string{bd.stateHome})
}

func (bd baseDirectories) cacheFile(relPath string) (string, error) {
return createPath(relPath, []string{bd.cacheHome})
return pathutil.Create(relPath, []string{bd.cacheHome})
}

func (bd baseDirectories) runtimeFile(relPath string) (string, error) {
return createPath(relPath, []string{bd.runtime})
return pathutil.Create(relPath, []string{bd.runtime})
}

func (bd baseDirectories) searchDataFile(relPath string) (string, error) {
return searchFile(relPath, append([]string{bd.dataHome}, bd.data...))
return pathutil.Search(relPath, append([]string{bd.dataHome}, bd.data...))
}

func (bd baseDirectories) searchConfigFile(relPath string) (string, error) {
return searchFile(relPath, append([]string{bd.configHome}, bd.config...))
return pathutil.Search(relPath, append([]string{bd.configHome}, bd.config...))
}

func (bd baseDirectories) searchStateFile(relPath string) (string, error) {
return searchFile(relPath, []string{bd.stateHome})
return pathutil.Search(relPath, []string{bd.stateHome})
}

func (bd baseDirectories) searchCacheFile(relPath string) (string, error) {
return searchFile(relPath, []string{bd.cacheHome})
return pathutil.Search(relPath, []string{bd.cacheHome})
}

func (bd baseDirectories) searchRuntimeFile(relPath string) (string, error) {
return searchFile(relPath, []string{bd.runtime})
return pathutil.Search(relPath, []string{bd.runtime})
}
5 changes: 4 additions & 1 deletion go.mod
Expand Up @@ -2,4 +2,7 @@ module github.com/adrg/xdg

go 1.14

require github.com/stretchr/testify v1.7.0
require (
github.com/stretchr/testify v1.7.0
golang.org/x/sys v0.0.0-20211020174200-9d6173849985
)
3 changes: 2 additions & 1 deletion go.sum
Expand Up @@ -2,10 +2,11 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20211020174200-9d6173849985 h1:LOlKVhfDyahgmqa97awczplwkjzNaELFg3zRIJ13RYo=
golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
Expand Down
78 changes: 78 additions & 0 deletions internal/pathutil/pathutil.go
@@ -0,0 +1,78 @@
package pathutil

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

// Unique eliminates the duplicate paths from the provided slice and returns
// the result. The items in the output slice are in the order in which they
// occur in the input slice. If a `home` location is provided, the paths are
// expanded using the `ExpandHome` function.
func Unique(paths []string, home string) []string {
var (
uniq []string
registry = map[string]struct{}{}
)

for _, p := range paths {
p = ExpandHome(p, home)
if p != "" && filepath.IsAbs(p) {
if _, ok := registry[p]; ok {
continue
}

registry[p] = struct{}{}
uniq = append(uniq, p)
}
}

return uniq
}

// Create returns a suitable location relative to which the file with the
// specified `name` can be written. The first path from the provided `paths`
// slice which is successfully created (or already exists) is used as a base
// path for the file. The `name` parameter should contain the name of the file
// which is going to be written in the location returned by this function, but
// it can also contain a set of parent directories, which will be created
// relative to the selected parent path.
func Create(name string, paths []string) (string, error) {
var searchedPaths []string
for _, p := range paths {
p = filepath.Join(p, name)

dir := filepath.Dir(p)
if Exists(dir) {
return p, nil
}
if err := os.MkdirAll(dir, os.ModeDir|0700); err == nil {
return p, nil
}

searchedPaths = append(searchedPaths, dir)
}

return "", fmt.Errorf("could not create any of the following paths: %s",
strings.Join(searchedPaths, ", "))
}

// Search searches for the file with the specified `name` in the provided
// slice of `paths`. The `name` parameter must contain the name of the file,
// but it can also contain a set of parent directories.
func Search(name string, paths []string) (string, error) {
var searchedPaths []string
for _, p := range paths {
p = filepath.Join(p, name)
if Exists(p) {
return p, nil
}

searchedPaths = append(searchedPaths, filepath.Dir(p))
}

return "", fmt.Errorf("could not locate `%s` in any of the following paths: %s",
filepath.Base(name), strings.Join(searchedPaths, ", "))
}
29 changes: 29 additions & 0 deletions internal/pathutil/pathutil_plan9.go
@@ -0,0 +1,29 @@
package pathutil

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

// Exists returns true if the specified path exists.
func Exists(path string) bool {
_, err := os.Stat(path)
return err == nil || os.IsExist(err)
}

// ExpandHome substitutes `~` and `$home` at the start of the specified
// `path` using the provided `home` location.
func ExpandHome(path, home string) string {
if path == "" || home == "" {
return path
}
if path[0] == '~' {
return filepath.Join(home, path[1:])
}
if strings.HasPrefix(path, "$home") {
return filepath.Join(home, path[5:])
}

return path
}
52 changes: 52 additions & 0 deletions internal/pathutil/pathutil_plan9_test.go
@@ -0,0 +1,52 @@
//go:build plan9
// +build plan9

package pathutil_test

import (
"path/filepath"
"testing"

"github.com/adrg/xdg/internal/pathutil"
"github.com/stretchr/testify/require"
)

func TestExpandHome(t *testing.T) {
home := "/home/test"

require.Equal(t, home, pathutil.ExpandHome("~", home))
require.Equal(t, home, pathutil.ExpandHome("$home", home))
require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("~/appname", home))
require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("$home/appname", home))

require.Equal(t, "", pathutil.ExpandHome("", home))
require.Equal(t, home, pathutil.ExpandHome(home, ""))
require.Equal(t, "", pathutil.ExpandHome("", ""))

require.Equal(t, home, pathutil.ExpandHome(home, home))
require.Equal(t, "/", pathutil.ExpandHome("~", "/"))
require.Equal(t, "/", pathutil.ExpandHome("$home", "/"))
require.Equal(t, "/usr/bin", pathutil.ExpandHome("~/bin", "/usr"))
require.Equal(t, "/usr/bin", pathutil.ExpandHome("$home/bin", "/usr"))
}

func TestUnique(t *testing.T) {
input := []string{
"",
"/home",
"/home/test",
"a",
"~/appname",
"$home/appname",
"a",
"/home",
}

expected := []string{
"/home",
"/home/test",
"/home/test/appname",
}

require.EqualValues(t, expected, pathutil.Unique(input, "/home/test"))
}
108 changes: 108 additions & 0 deletions internal/pathutil/pathutil_test.go
@@ -0,0 +1,108 @@
package pathutil_test

import (
"os"
"path/filepath"
"testing"

"github.com/adrg/xdg/internal/pathutil"
"github.com/stretchr/testify/require"
)

func TestExists(t *testing.T) {
tempDir := os.TempDir()

// Test regular file.
pathFile := filepath.Join(tempDir, "regular")
f, err := os.Create(pathFile)
require.NoError(t, err)
require.NoError(t, f.Close())
require.True(t, pathutil.Exists(pathFile))

// Test symlink.
pathSymlink := filepath.Join(tempDir, "symlink")
require.NoError(t, os.Symlink(pathFile, pathSymlink))
require.True(t, pathutil.Exists(pathSymlink))

// Test non-existent file.
require.NoError(t, os.Remove(pathFile))
require.False(t, pathutil.Exists(pathFile))
require.False(t, pathutil.Exists(pathSymlink))
require.NoError(t, os.Remove(pathSymlink))
require.False(t, pathutil.Exists(pathSymlink))
}

func TestCreate(t *testing.T) {
tempDir := os.TempDir()

// Test path selection order.
p, err := pathutil.Create("test", []string{tempDir, "\000a"})
require.NoError(t, err)
require.Equal(t, filepath.Join(tempDir, "test"), p)

p, err = pathutil.Create("test", []string{"\000a", tempDir})
require.NoError(t, err)
require.Equal(t, filepath.Join(tempDir, "test"), p)

// Test relative parent directories.
expected := filepath.Join(tempDir, "appname", "config", "test")
p, err = pathutil.Create(filepath.Join("appname", "config", "test"), []string{"\000a", tempDir})
require.NoError(t, err)
require.Equal(t, expected, p)
require.NoError(t, os.RemoveAll(filepath.Dir(expected)))

expected = filepath.Join(tempDir, "appname", "test")
p, err = pathutil.Create(filepath.Join("appname", "test"), []string{"\000a", tempDir})
require.NoError(t, err)
require.Equal(t, expected, p)
require.NoError(t, os.RemoveAll(filepath.Dir(expected)))

// Test invalid paths.
_, err = pathutil.Create(filepath.Join("appname", "test"), []string{"\000a"})
require.Error(t, err)

_, err = pathutil.Create("test", []string{filepath.Join(tempDir, "\000a")})
require.Error(t, err)
}

func TestSearch(t *testing.T) {
tempDir := os.TempDir()

// Test file not found.
_, err := pathutil.Search("test", []string{tempDir, filepath.Join(tempDir, "appname")})
require.Error(t, err)

// Test file found.
expected := filepath.Join(tempDir, "test")
f, err := os.Create(expected)
require.NoError(t, err)
require.NoError(t, f.Close())

p, err := pathutil.Search("test", []string{tempDir})
require.NoError(t, err)
require.Equal(t, expected, p)

p, err = pathutil.Search("test", []string{filepath.Join(tempDir, "appname"), tempDir})
require.NoError(t, err)
require.Equal(t, expected, p)

require.NoError(t, os.Remove(expected))

// Test relative parent directories.
expected = filepath.Join(tempDir, "appname", "test")
_, err = pathutil.Create(filepath.Join("appname", "test"), []string{tempDir})
require.NoError(t, err)
f, err = os.Create(expected)
require.NoError(t, err)
require.NoError(t, f.Close())

p, err = pathutil.Search(filepath.Join("appname", "test"), []string{tempDir})
require.NoError(t, err)
require.Equal(t, expected, p)

p, err = pathutil.Search("test", []string{tempDir, filepath.Join(tempDir, "appname")})
require.NoError(t, err)
require.Equal(t, expected, p)

require.NoError(t, os.RemoveAll(filepath.Dir(expected)))
}
32 changes: 32 additions & 0 deletions internal/pathutil/pathutil_unix.go
@@ -0,0 +1,32 @@
//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || nacl || linux || netbsd || openbsd || solaris
// +build aix darwin dragonfly freebsd js,wasm nacl linux netbsd openbsd solaris

package pathutil

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

// Exists returns true if the specified path exists.
func Exists(path string) bool {
_, err := os.Stat(path)
return err == nil || os.IsExist(err)
}

// ExpandHome substitutes `~` and `$HOME` at the start of the specified
// `path` using the provided `home` location.
func ExpandHome(path, home string) string {
if path == "" || home == "" {
return path
}
if path[0] == '~' {
return filepath.Join(home, path[1:])
}
if strings.HasPrefix(path, "$HOME") {
return filepath.Join(home, path[5:])
}

return path
}

0 comments on commit 210f365

Please sign in to comment.