Skip to content
This repository has been archived by the owner on Dec 8, 2020. It is now read-only.

Commit

Permalink
Merge pull request #7 from puppetlabs/features/workdir-management
Browse files Browse the repository at this point in the history
adds utility for creating and managing workdirs defaulting to XDG user dirs
  • Loading branch information
kyleterry authored Aug 8, 2019
2 parents 8289c5a + 50a74c4 commit 5378002
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 1 deletion.
19 changes: 19 additions & 0 deletions README.md
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261 // indirect
github.com/getsentry/raven-go v0.2.0
github.com/gogo/protobuf v1.2.1 // indirect
github.com/google/uuid v1.0.0 // indirect
github.com/google/uuid v1.0.0
github.com/gorilla/websocket v1.4.0
github.com/inconshreveable/log15 v0.0.0-20180818164646-67afb5ed74ec
github.com/mattn/go-colorable v0.1.2 // indirect
Expand Down
9 changes: 9 additions & 0 deletions workdir/options.go
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
}
122 changes: 122 additions & 0 deletions workdir/workdir.go
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}
}
106 changes: 106 additions & 0 deletions workdir/workdir_test.go
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")
})
}
}

0 comments on commit 5378002

Please sign in to comment.