This repository has been archived by the owner on Dec 8, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from puppetlabs/features/workdir-management
adds utility for creating and managing workdirs defaulting to XDG user dirs
- Loading branch information
Showing
5 changed files
with
257 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Horsehead | ||
|
||
Named after the [Horsehead Nebula](https://en.wikipedia.org/wiki/Horsehead_Nebula). | ||
This repo provides Go packages that serve has helper functions and utility for | ||
Go-based codebases at Puppet (mostly on the Nebula project). | ||
|
||
## workdir package | ||
|
||
This package provides utilties for creating and managing working directories. | ||
It defaults to the XDG suite of directory standards from [freedesktop.org](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/). | ||
|
||
Help can be found by running `go doc -all github.com/puppetlabs/horsehead/workdir`. | ||
|
||
The functionality in this package should work on Linux, MacOS and the BSDs. | ||
|
||
### TODO | ||
|
||
- add a mechanism for root interactions | ||
- add Windows support |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package workdir | ||
|
||
import "os" | ||
|
||
// Options for changing the behavior of directory management | ||
type Options struct { | ||
// Mode is the octal filemode to use when creating each directory | ||
Mode os.FileMode | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package workdir | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
"path/filepath" | ||
) | ||
|
||
const defaultMode = 0755 | ||
|
||
type CleanupFunc func() error | ||
|
||
// WorkDir is a response type that contains the Path to a directory created by this package. | ||
type WorkDir struct { | ||
// Path is the absolute path to the directory requested. | ||
Path string | ||
// Cleanup is a function that will cleanup any directory and files under | ||
// Path. | ||
Cleanup CleanupFunc | ||
} | ||
|
||
type dirType int | ||
|
||
const ( | ||
// DirTypeConfig is a directory used to store configuration. This is commonly | ||
// used to store application configs like yaml or json files used during | ||
// the bootstrapping phase of an application startup. | ||
DirTypeConfig dirType = iota | ||
// DirTypeCache is a directory used to store any temporary cache that is generated | ||
// by the application. This directory type can be used to store tokens when logging in, | ||
// or serialized cache such as large responses that you don't want to have to request again | ||
// for some amount of time. Anything in here should be considered temporary and can be removed | ||
// at any time. | ||
DirTypeCache | ||
// DirTypeData is a directory to store long term data. This data can be database files or assets | ||
// that need to later be extracted out into another location. Things in this directory should be | ||
// considered important and backed up. | ||
DirTypeData | ||
) | ||
|
||
// dirTypeEnvDefault is a type that represents the environment variable name | ||
// and its default if it's not set. | ||
type dirTypeEnvDefault struct { | ||
// envName is the name of the environment variable we should check for first. | ||
envName string | ||
// defaultLoc is the default location for the directory type. `default` is a | ||
// keyword in the Go language, so we use defaultLoc here to prevent syntax | ||
// collisions. | ||
defaultLoc string | ||
} | ||
|
||
var dirTypeEnv = map[dirType]dirTypeEnvDefault{ | ||
DirTypeConfig: dirTypeEnvDefault{ | ||
envName: "XDG_CONFIG_HOME", | ||
defaultLoc: filepath.Join(os.Getenv("HOME"), ".config"), | ||
}, | ||
DirTypeCache: dirTypeEnvDefault{ | ||
envName: "XDG_CACHE_HOME", | ||
defaultLoc: filepath.Join(os.Getenv("HOME"), ".cache"), | ||
}, | ||
DirTypeData: dirTypeEnvDefault{ | ||
envName: "XDG_DATA_HOME", | ||
defaultLoc: filepath.Join(os.Getenv("HOME"), ".local", "share"), | ||
}, | ||
} | ||
|
||
// New returns a new WorkDir or an error. An error is returned if p is empty. | ||
// A standard cleanup function is made available so the caller can decide if they want to | ||
// remove the directory created after they are done. Options allow additional control over | ||
// the directory attributes. | ||
func New(p string, opts Options) (*WorkDir, error) { | ||
if p == "" { | ||
return nil, errors.New("path cannot be empty") | ||
} | ||
|
||
mode := os.FileMode(defaultMode) | ||
if opts.Mode != 0 { | ||
mode = opts.Mode | ||
} | ||
|
||
if err := os.MkdirAll(p, mode); err != nil { | ||
return nil, err | ||
} | ||
|
||
wd := &WorkDir{ | ||
Path: p, | ||
Cleanup: func() error { | ||
return os.RemoveAll(p) | ||
}, | ||
} | ||
|
||
return wd, nil | ||
} | ||
|
||
// Namespace holds the directory parts that will be joined together to form | ||
// a namespaced path segment in the final workdir. | ||
type Namespace struct { | ||
parts []string | ||
} | ||
|
||
// New returns a new WorkDir under the context of dt (directory type) and allows for setting | ||
// a namespace. Below is an example of its use: | ||
// wd, _ := NewNamespace([]string{"foo", "bar"}).New(DirTypeConfig, Options{}) | ||
// fmt.Println(wd.Path) | ||
// | ||
// Out: /home/kyle/.config/foo/bar | ||
func (n *Namespace) New(dt dirType, opts Options) (*WorkDir, error) { | ||
def := dirTypeEnv[dt] | ||
|
||
p := filepath.Join(def.defaultLoc, filepath.Join(n.parts...)) | ||
|
||
if os.Getenv(def.envName) != "" { | ||
p = filepath.Join(os.Getenv(def.envName), filepath.Join(n.parts...)) | ||
} | ||
|
||
return New(p, opts) | ||
} | ||
|
||
// NewNamespace returns a new Namespace with the provided parts slice set | ||
func NewNamespace(parts []string) *Namespace { | ||
return &Namespace{parts: parts} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package workdir | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/google/uuid" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestNewNamespace(t *testing.T) { | ||
t.Parallel() | ||
|
||
testID := uuid.New().String() | ||
|
||
var cases = []struct { | ||
description string | ||
setup func() | ||
dirType dirType | ||
namespace []string | ||
expected string | ||
shouldError bool | ||
}{ | ||
{ | ||
description: "can create config dirs with XDG var set", | ||
setup: func() { | ||
require.NoError(t, os.Setenv("XDG_CONFIG_HOME", "/tmp/")) | ||
}, | ||
dirType: DirTypeConfig, | ||
namespace: []string{testID, "horsehead", "config-dir-test"}, | ||
expected: filepath.Join("/tmp", testID, "horsehead", "config-dir-test"), | ||
}, | ||
{ | ||
description: "can create cache dirs with XDG var set", | ||
setup: func() { | ||
require.NoError(t, os.Setenv("XDG_CACHE_HOME", "/tmp/")) | ||
}, | ||
dirType: DirTypeCache, | ||
namespace: []string{testID, "horsehead", "cache-dir-test"}, | ||
expected: filepath.Join("/tmp", testID, "horsehead", "cache-dir-test"), | ||
}, | ||
{ | ||
description: "can create data dirs with XDG var set", | ||
setup: func() { | ||
require.NoError(t, os.Setenv("XDG_DATA_HOME", "/tmp/")) | ||
}, | ||
dirType: DirTypeData, | ||
namespace: []string{testID, "horsehead", "data-dir-test"}, | ||
expected: filepath.Join("/tmp", testID, "horsehead", "data-dir-test"), | ||
}, | ||
{ | ||
description: "can create config dirs", | ||
setup: func() { | ||
require.NoError(t, os.Setenv("XDG_CONFIG_HOME", "")) | ||
}, | ||
dirType: DirTypeConfig, | ||
namespace: []string{testID, "horsehead", "config-dir-test"}, | ||
expected: filepath.Join(os.Getenv("HOME"), ".config", testID, "horsehead", "config-dir-test"), | ||
}, | ||
{ | ||
description: "can create cache dirs", | ||
setup: func() { | ||
require.NoError(t, os.Setenv("XDG_CACHE_HOME", "")) | ||
}, | ||
dirType: DirTypeCache, | ||
namespace: []string{testID, "horsehead", "cache-dir-test"}, | ||
expected: filepath.Join(os.Getenv("HOME"), ".cache", testID, "horsehead", "cache-dir-test"), | ||
}, | ||
{ | ||
description: "can create data dirs", | ||
setup: func() { | ||
require.NoError(t, os.Setenv("XDG_DATA_HOME", "")) | ||
}, | ||
dirType: DirTypeData, | ||
namespace: []string{testID, "horsehead", "data-dir-test"}, | ||
expected: filepath.Join(os.Getenv("HOME"), ".local", "share", testID, "horsehead", "data-dir-test"), | ||
}, | ||
} | ||
|
||
for _, c := range cases { | ||
t.Run(c.description, func(t *testing.T) { | ||
if c.setup != nil { | ||
c.setup() | ||
} | ||
|
||
wd, err := NewNamespace(c.namespace).New(c.dirType, Options{}) | ||
if c.shouldError { | ||
require.Error(t, err) | ||
|
||
return | ||
} | ||
|
||
require.NoError(t, err) | ||
require.Equal(t, c.expected, wd.Path) | ||
|
||
_, err = os.Stat(c.expected) | ||
require.NoError(t, err, "directory should exist") | ||
|
||
require.NoError(t, wd.Cleanup()) | ||
|
||
_, err = os.Stat(c.expected) | ||
require.Error(t, err, "expected directory to be gone") | ||
}) | ||
} | ||
} |