From e020e2c942209ea1d28d2780938040196169bc1b Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sat, 11 May 2019 10:42:43 -0700 Subject: [PATCH] OS-specific directory building implementations The main package will abstract over these, but they are exposed as public helpers for applications with unusual needs that our main OS-agnostic abstraction cannot meet. --- go.mod | 2 + go.sum | 2 + internal/unix/homedir.go | 45 ++++++++++++++++ macosbase/doc.go | 8 +++ macosbase/home.go | 9 ++++ macosbase/macosbase.go | 26 +++++++++ windowsbase/doc.go | 6 +++ windowsbase/impl.go | 40 ++++++++++++++ windowsbase/stub.go | 11 ++++ windowsbase/windowsbase.go | 35 +++++++++++++ xdgbase/doc.go | 8 +++ xdgbase/home.go | 9 ++++ xdgbase/xdg.go | 105 +++++++++++++++++++++++++++++++++++++ 13 files changed, 306 insertions(+) create mode 100644 go.sum create mode 100644 internal/unix/homedir.go create mode 100644 macosbase/doc.go create mode 100644 macosbase/home.go create mode 100644 macosbase/macosbase.go create mode 100644 windowsbase/doc.go create mode 100644 windowsbase/impl.go create mode 100644 windowsbase/stub.go create mode 100644 windowsbase/windowsbase.go create mode 100644 xdgbase/doc.go create mode 100644 xdgbase/home.go create mode 100644 xdgbase/xdg.go diff --git a/go.mod b/go.mod index 08499be..1ce39fb 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/apparentlymart/go-userdirs go 1.12 + +require golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..80d86f4 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug= +golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/unix/homedir.go b/internal/unix/homedir.go new file mode 100644 index 0000000..d00358b --- /dev/null +++ b/internal/unix/homedir.go @@ -0,0 +1,45 @@ +package unix + +import ( + "os" + "os/user" + "path/filepath" +) + +// Home returns the home directory for the current process, with the following +// preference order: +// +// - The value of the HOME environment variable, if it is set and contains +// an absolute path. +// - The home directory indicated in the return value of the "Current" +// function in the os/user standard library package, which has +// platform-specific behavior, if it contains an absolute path. +// - If neither of the above yields an absolute path, the string "/". +// +// In practice, POSIX requires the HOME environment variable to be set, so on +// any reasonable system it is that which will be selected. The other +// permutations are fallback behavior for less reasonable systems. +// +// XDG does not permit applications to write directly into the home directory. +// Instead, the paths returned by other functions in this package are +// potentially derived from the home path, if their explicit environment +// variables are not set. +func Home() string { + if homeDir := os.Getenv("HOME"); homeDir != "" { + if filepath.IsAbs(homeDir) { + return homeDir + } + } + + user, err := user.Current() + if err != nil { + if homeDir := user.HomeDir; homeDir != "" { + if filepath.IsAbs(homeDir) { + return homeDir + } + } + } + + // Fallback behavior mimics common choice in other software. + return "/" +} diff --git a/macosbase/doc.go b/macosbase/doc.go new file mode 100644 index 0000000..7176fe8 --- /dev/null +++ b/macosbase/doc.go @@ -0,0 +1,8 @@ +// Package macosbase contains helper functions that construct base paths +// conforming to the Mac OS user-specific file layout guidelines as +// documented in https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW6 . +// +// This package only does path construction, and doesn't depend on any Mac OS +// system APIs, so in principle it can run on other platforms but the results +// it produces in that case are undefined and unlikely to be useful. +package macosbase diff --git a/macosbase/home.go b/macosbase/home.go new file mode 100644 index 0000000..f3fa3ec --- /dev/null +++ b/macosbase/home.go @@ -0,0 +1,9 @@ +package macosbase + +import ( + "github.com/apparentlymart/go-userdirs/internal/unix" +) + +func home() string { + return unix.Home() +} diff --git a/macosbase/macosbase.go b/macosbase/macosbase.go new file mode 100644 index 0000000..9dae962 --- /dev/null +++ b/macosbase/macosbase.go @@ -0,0 +1,26 @@ +package macosbase + +import ( + "path/filepath" +) + +// ApplicationSupportDir returns the path to the current user's +// "Application Support" library directory. +func ApplicationSupportDir() string { + return filepath.Join(home(), "Library", "Application Support") +} + +// CachesDir returns the path to the current user's "Caches" library directory. +func CachesDir() string { + return filepath.Join(home(), "Library", "Caches") +} + +// FrameworksDir returns the path to the current user's "Frameworks" library directory. +func FrameworksDir() string { + return filepath.Join(home(), "Library", "Frameworks") +} + +// PreferencesDir returns the path to the current user's "Preferences" library directory. +func PreferencesDir() string { + return filepath.Join(home(), "Library", "Preferences") +} diff --git a/windowsbase/doc.go b/windowsbase/doc.go new file mode 100644 index 0000000..e7f2aa9 --- /dev/null +++ b/windowsbase/doc.go @@ -0,0 +1,6 @@ +// Package windowsbase contains helper functionality for accessing the +// "Known Folders" shell API functions on Windows systems. +// +// This package calls into Windows system DLLs, so it cannot be used on any +// other platform. +package windowsbase diff --git a/windowsbase/impl.go b/windowsbase/impl.go new file mode 100644 index 0000000..ce31b8c --- /dev/null +++ b/windowsbase/impl.go @@ -0,0 +1,40 @@ +// +build windows + +package windowsbase + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + shell32 = windows.NewLazyDLL("Shell32.dll") + ole32 = windows.NewLazyDLL("Ole32.dll") + procSHGetKnownFolderPath = modShell32.NewProc("SHGetKnownFolderPath") + procCoTaskMemFree = modOle32.NewProc("CoTaskMemFree") +) + +func knownFolderDir(fid *FolderID) (string, error) { + var path uintptr + err := SHGetKnownFolderPath(fid, 0, 0, &path) + if err != nil { + return "", err + } + defer CoTaskMemFree(path) + dir := syscall.UTF16ToString((*[1 << 16]uint16)(unsafe.Pointer(path))[:]) + return dir, nil +} + +func shGetKnownFolderPath(fid *FolderID, dwFlags uint32, hToken syscall.Handle, pszPath *uintptr) (retval error) { + r0, _, _ := procSHGetKnownFolderPath.Call(uintptr(unsafe.Pointer(rfid)), uintptr(dwFlags), uintptr(hToken), uintptr(unsafe.Pointer(pszPath)), 0, 0) + if r0 != 0 { + return syscall.Errno(r0) + } + return nil +} + +func coTaskMemFree(pv uintptr) { + procCoTaskMemFree.Call(uintptr(pv), 0, 0) +} diff --git a/windowsbase/stub.go b/windowsbase/stub.go new file mode 100644 index 0000000..21df8a3 --- /dev/null +++ b/windowsbase/stub.go @@ -0,0 +1,11 @@ +// +build !windows + +package windowsbase + +import ( + "errors" +) + +func knownFolderDir(id *FolderID) (string, error) { + return "", errors.New("cannot use Windows known folders on a non-Windows platform") +} diff --git a/windowsbase/windowsbase.go b/windowsbase/windowsbase.go new file mode 100644 index 0000000..d19f3d3 --- /dev/null +++ b/windowsbase/windowsbase.go @@ -0,0 +1,35 @@ +package windowsbase + +// FolderID is a representation of a known folder id UUID +type FolderID struct { + a uint32 + b uint16 + c uint16 + d [8]byte +} + +var ( + // RoamingAppDataID is the FolderID for the roaming application data folder + RoamingAppDataID = &FolderID{0x3EB685DB, 0x65F9, 0x4CF6, [...]byte{0xA0, 0x3A, 0xE3, 0xEF, 0x65, 0x72, 0x9F, 0x3D}} + + // LocalAppDataID is the FolderID for the local application data folder + LocalAppDataID = &FolderID{0xF1B32785, 0x6FBA, 0x4FCF, [...]byte{0x9D, 0x55, 0x7B, 0x8E, 0x7F, 0x15, 0x70, 0x91}} +) + +// KnownFolderDir returns the absolute path for the given known folder id, or +// returns an error if that is not possible. +func KnownFolderDir(id *FolderID) (string, error) { + return knownFolderDir(id) +} + +// RoamingAppDataDir returns the absolute path for the current user's roaming +// application data directory. +func RoamingAppDataDir() (string, error) { + return KnownFolderDir(RoamingAppDataID) +} + +// LocalAppDataDir returns the absolute path for the current user's local +// application data directory. +func LocalAppDataDir() (string, error) { + return KnownFolderDir(LocalAppDataID) +} diff --git a/xdgbase/doc.go b/xdgbase/doc.go new file mode 100644 index 0000000..11c5616 --- /dev/null +++ b/xdgbase/doc.go @@ -0,0 +1,8 @@ +// Package xdgbase is an implementation of the XDG Basedir Specification +// version 0.8, as published at https://specifications.freedesktop.org/basedir-spec/basedir-spec-0.8.html . +// +// This package has no checks for the host operating system, so it can in +// principle function on any operating system but in practice XDG conventions +// are followed only on some Unix-like systems, so using this library elsewhere +// would not be very useful and will produce undefined results. +package xdgbase diff --git a/xdgbase/home.go b/xdgbase/home.go new file mode 100644 index 0000000..45cbda8 --- /dev/null +++ b/xdgbase/home.go @@ -0,0 +1,9 @@ +package xdgbase + +import ( + "github.com/apparentlymart/go-userdirs/internal/unix" +) + +func home() string { + return unix.Home() +} diff --git a/xdgbase/xdg.go b/xdgbase/xdg.go new file mode 100644 index 0000000..b6dcc78 --- /dev/null +++ b/xdgbase/xdg.go @@ -0,0 +1,105 @@ +package xdgbase + +import ( + "os" + "path/filepath" +) + +// DataHome returns the value of XDG_DATA_HOME, or the specification-defined +// fallback value of $HOME/.local/share. +func DataHome() string { + return envSingle("XDG_DATA_HOME", func() string { + return filepath.Join(home(), ".local", "share") + }) +} + +// OtherDataDirs returns the values from XDG_DATA_DIRS, or the specification-defined +// fallback values "/usr/local/share/" and "/usr/share/". +func OtherDataDirs() []string { + return envMulti("XDG_DATA_DIRS", func() []string { + return []string{"/usr/local/share/", "/usr/share/"} + }) +} + +// DataDirs returns the combination of DataHome and OtherDataDirs, giving the +// full set of data directories to search, in preference order. +func DataDirs() []string { + ret := make([]string, 0, 3) // default OtherDataDirs has two elements + ret = append(ret, DataHome()) + ret = append(ret, OtherDataDirs()...) + return ret[:len(ret):len(ret)] +} + +// ConfigHome returns the value of XDG_CONFIG_HOME, or the specification-defined +// fallback value of $HOME/.config. +func ConfigHome() string { + return envSingle("XDG_CONFIG_HOME", func() string { + return filepath.Join(home(), ".config") + }) +} + +// OtherConfigDirs returns the values from XDG_CONFIG_DIRS, or the +// specification-defined fallback value "/etc/xdg". +func OtherConfigDirs() []string { + return envMulti("XDG_CONFIG_DIRS", func() []string { + return []string{"/etc/xdg"} + }) +} + +// ConfigDirs returns the combination of ConfigHome and OtherConfigDirs, giving the +// full set of config directories to search, in preference order. +func ConfigDirs() []string { + ret := make([]string, 0, 2) // default OtherConfigDirs has one element + ret = append(ret, ConfigHome()) + ret = append(ret, OtherConfigDirs()...) + return ret[:len(ret):len(ret)] +} + +// CacheHome returns the value of XDG_CACHE_HOME, or the specification-defined +// fallback value of $HOME/.cache. +func CacheHome() string { + return envSingle("XDG_CACHE_HOME", func() string { + return filepath.Join(home(), ".cache") + }) +} + +// MaybeRuntimeDir returns the value of XDG_RUNTIME_DIR, or an empty string if +// it is not set. +// +// Calling applications MUST check that the return value is non-empty before +// using it, because there is no reasonable default behavior when no runtime +// directory is defined. +func MaybeRuntimeDir() string { + return envSingle("XDG_RUNTIME_DIR", func() string { + return "" + }) +} + +func envSingle(name string, fallback func() string) string { + if p := os.Getenv(name); p != "" { + if filepath.IsAbs(p) { + return p + } + } + + return fallback() +} + +func envMulti(name string, fallback func() []string) []string { + if p := os.Getenv(name); p != "" { + parts := filepath.SplitList(p) + // Make sure all of the paths are absolute + for i := len(parts) - 1; i >= 0; i-- { + if !filepath.IsAbs(parts[i]) { + // We'll shift everything after this point in the list + // down so that this element is no longer present. + copy(parts[i:], parts[i+1:]) + parts = parts[:len(parts)-1] + } + } + parts = parts[:len(parts):len(parts)] // hide any extra capacity from the caller + return parts + } + + return fallback() +}