Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Known Folders support on Windows #27

Merged
merged 47 commits into from Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
311ad3a
Add Known Folders support on Windows
adrg Oct 23, 2021
312fa55
Update test cases
adrg Oct 23, 2021
468bcc9
Update README.md
adrg Oct 23, 2021
28cd370
Move KnownFolderPath function to internal util package
adrg Oct 23, 2021
89fe72b
Move PathExists function to internal util package
adrg Oct 23, 2021
352dc53
Improve file naming in internal util package
adrg Oct 24, 2021
0824d81
Move ExpandHome function to internal util package
adrg Oct 24, 2021
0bccdbb
Move known folders in paths_windows.go
adrg Oct 24, 2021
6a1ba68
Handle home location per OS for more flexibility
adrg Oct 24, 2021
301dde6
Add home environment variable not set test case
adrg Oct 24, 2021
55c9c3e
Reorganize internal util package
adrg Oct 24, 2021
ba77b3a
Add test case for internal PathExists function
adrg Oct 24, 2021
50902f6
Add test case for internal ExpandHome function on Unix and Plan 9
adrg Oct 24, 2021
09084b8
Add test case for internal ExpandHome on Windows
adrg Oct 24, 2021
ccaa1b3
Move UniquePaths function to internal util package
adrg Oct 24, 2021
4e5669e
Add test case for internal UniquePaths function on Unix and Plan 9
adrg Oct 24, 2021
eb24bd7
Add test case for internal UniquePaths function on Windows
adrg Oct 24, 2021
d29e149
Improve system drive path retrieval on Windows
adrg Oct 24, 2021
82790f6
Minor refactor
adrg Oct 25, 2021
77d3b6e
Minor file naming improvement
adrg Oct 25, 2021
2ff3789
Move CreatePath function to internal util package
adrg Oct 25, 2021
aaf715f
Move SearchFile function to internal util package
adrg Oct 25, 2021
d503bca
Minor internal refactor
adrg Oct 25, 2021
ba93ffa
Rename internal util package to pathutil
adrg Oct 25, 2021
4d88426
Rename PathExists internal function to Exists
adrg Oct 25, 2021
e9a25e0
Rename pathutil.KnownFolderPath function to pathutil.KnownFolder
adrg Oct 25, 2021
e40b1c9
Rename pathutil.UniquePaths function to pathutil.Unique
adrg Oct 25, 2021
6fd52b9
Rename pathutil.CreatePath function to pathutil.Create
adrg Oct 25, 2021
f74d1dd
Add test case for internal pathutil.Create function
adrg Oct 25, 2021
7f50b42
Add test case for internal pathutil.SearchFile function
adrg Oct 25, 2021
12417dd
Rename pathutil.SearchFile function to pathutil.Search
adrg Oct 25, 2021
9ef369a
Minor pathutil refactor
adrg Oct 25, 2021
06a9e51
Update base directory tables in README.md
adrg Oct 26, 2021
a826b0c
Minor README.md update
adrg Oct 26, 2021
ec8ac25
Attempt to make README.md table full width
adrg Oct 26, 2021
f8b3157
Update base directory tables in README.md
adrg Oct 26, 2021
e8d3bc5
Update user directory tables in README.md
adrg Oct 26, 2021
44fe4cd
Update non-standard directory tables in README.md
adrg Oct 26, 2021
6ce6abb
Minor README.md improvement
adrg Oct 26, 2021
5a24e68
Additional minor README.md improvement
adrg Oct 26, 2021
9d90807
Add Windows Known Folders reference URL in README.md
adrg Oct 26, 2021
8b1c149
Add collapsible sections for the defined locations in README.md
adrg Oct 26, 2021
888ae11
Improve README.md on mobile devices
adrg Oct 26, 2021
917bb47
Improve default locations README.md section
adrg Oct 26, 2021
aedffa5
Further improve default locations README.md section
adrg Oct 26, 2021
63340f7
Improve README.md
adrg Oct 26, 2021
2928c45
Additional minor README.md improvement
adrg Oct 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
}