Skip to content

Commit

Permalink
OS-specific directory building implementations
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
apparentlymart committed May 11, 2019
1 parent fb3d831 commit e020e2c
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 0 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/apparentlymart/go-userdirs

go 1.12

require golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
45 changes: 45 additions & 0 deletions internal/unix/homedir.go
Original file line number Diff line number Diff line change
@@ -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 "/"
}
8 changes: 8 additions & 0 deletions macosbase/doc.go
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions macosbase/home.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package macosbase

import (
"github.com/apparentlymart/go-userdirs/internal/unix"
)

func home() string {
return unix.Home()
}
26 changes: 26 additions & 0 deletions macosbase/macosbase.go
Original file line number Diff line number Diff line change
@@ -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")
}
6 changes: 6 additions & 0 deletions windowsbase/doc.go
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions windowsbase/impl.go
Original file line number Diff line number Diff line change
@@ -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)
}
11 changes: 11 additions & 0 deletions windowsbase/stub.go
Original file line number Diff line number Diff line change
@@ -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")
}
35 changes: 35 additions & 0 deletions windowsbase/windowsbase.go
Original file line number Diff line number Diff line change
@@ -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)
}
8 changes: 8 additions & 0 deletions xdgbase/doc.go
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions xdgbase/home.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package xdgbase

import (
"github.com/apparentlymart/go-userdirs/internal/unix"
)

func home() string {
return unix.Home()
}
105 changes: 105 additions & 0 deletions xdgbase/xdg.go
Original file line number Diff line number Diff line change
@@ -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()
}

0 comments on commit e020e2c

Please sign in to comment.