From e7723cdafd44bce0444f26c050276b9f6e612562 Mon Sep 17 00:00:00 2001 From: Skip Baney Date: Sat, 10 Jun 2023 12:59:18 -0500 Subject: [PATCH] feat: add fsutil package --- .vscode/settings.json | 1 + fsutil/fsutil.go | 86 ++++++++++++++++++++++++ fsutil/fsutil_test.go | 149 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 fsutil/fsutil.go create mode 100644 fsutil/fsutil_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json index eb80476..b62276d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "Cygwin", "dasherize", "flect", + "fsutil", "gobuffalo", "golangci", "GOPROXY", diff --git a/fsutil/fsutil.go b/fsutil/fsutil.go new file mode 100644 index 0000000..23e1f6c --- /dev/null +++ b/fsutil/fsutil.go @@ -0,0 +1,86 @@ +package fsutil + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "syscall" +) + +const ( + // DefaultDirMode grants `rwx------`. + DefaultDirMode = 0700 + // DefaultFileMode grants `rw-------`. + DefaultFileMode = 0600 +) + +var ( + osUserHomeDir = os.UserHomeDir + filepathAbs = filepath.Abs + osMkdirAll = os.MkdirAll + osWriteFile = os.WriteFile +) + +func NoPathExists(path string) bool { + _, err := os.Stat(path) + // for some reason os.ErrInvalid sometimes != syscall.EINVAL :shrug: + if errors.Is(err, os.ErrNotExist) || + errors.Is(err, os.ErrInvalid) || + errors.Is(err, syscall.EINVAL) { + return true + } + return false +} + +func PathExists(path string) bool { + return !NoPathExists(path) +} + +// NormalizePath ensures that name is an absolute path. +// Environment variables (and the ~ string) are expanded. +func NormalizePath(name string) (string, error) { + normalized := strings.TrimSpace(name) + if normalized == "" { + return "", nil + } + + // Replace ENV vars + normalized = os.ExpandEnv(normalized) + + // Replace ~ + if strings.HasPrefix(normalized, "~") { + home, err := osUserHomeDir() + if err != nil { + return "", fmt.Errorf("unable to normalize %s: %w", name, err) + } + normalized = home + strings.TrimPrefix(normalized, "~") + } + + // Ensure abs path + normalized, err := filepathAbs(normalized) + if err != nil { + return "", fmt.Errorf("unable to normalize %s: %w", name, err) + } + + return normalized, nil +} + +// EnsureDirWritable ensures that path is a writable directory. +// Will attempt to create a new directory if path does not exist. +func EnsureDirWritable(path string) error { + // Ensure dir exists (and IsDir). + err := osMkdirAll(path, DefaultDirMode) + if err != nil { + return fmt.Errorf("ensure dir: %w", err) + } + + f := filepath.Join(path, ".touch") + if err := osWriteFile(f, []byte(""), DefaultFileMode); err != nil { + return fmt.Errorf("ensure writable: %w", err) + } + defer os.Remove(f) + + return nil +} diff --git a/fsutil/fsutil_test.go b/fsutil/fsutil_test.go new file mode 100644 index 0000000..7c45f4b --- /dev/null +++ b/fsutil/fsutil_test.go @@ -0,0 +1,149 @@ +package fsutil + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/prashantv/gostub" + "github.com/stretchr/testify/assert" + + "github.com/twelvelabs/termite/testutil" +) + +func TestNoPathExists(t *testing.T) { + testutil.InTempDir(t, func(dir string) { + assert.NoFileExists(t, "foo.txt") + assert.Equal(t, true, NoPathExists("foo.txt")) + + testutil.WriteFile(t, "foo.txt", []byte(""), 0600) + assert.Equal(t, false, NoPathExists("foo.txt")) + }) +} + +func TestPathExists(t *testing.T) { + testutil.InTempDir(t, func(dir string) { + assert.NoFileExists(t, "foo.txt") + assert.Equal(t, false, PathExists("foo.txt")) + + testutil.WriteFile(t, "foo.txt", []byte(""), 0600) + assert.Equal(t, true, PathExists("foo.txt")) + }) +} + +func TestNormalizePath(t *testing.T) { + homeDir, _ := os.UserHomeDir() + workingDir, _ := filepath.Abs(".") + + tests := []struct { + Desc string + EnvVars map[string]string + Input string + Output string + Err string + }{ + { + Desc: "is a noop when passed an empty string", + Input: "", + Output: "", + Err: "", + }, + { + Desc: "expands env vars", + Input: filepath.Join(".", "${FOO}-dir", "$BAR"), + Output: filepath.Join(workingDir, "aaa-dir", "bbb"), + EnvVars: map[string]string{ + "FOO": "aaa", + "BAR": "bbb", + }, + Err: "", + }, + { + Desc: "expands tilde", + Input: "~", + Output: homeDir, + Err: "", + }, + { + Desc: "expands tilde when prefix", + Input: filepath.Join("~", "foo"), + Output: filepath.Join(homeDir, "foo"), + Err: "", + }, + { + Desc: "returns an absolute path", + Input: ".", + Output: workingDir, + Err: "", + }, + } + for _, tt := range tests { + t.Run(tt.Desc, func(t *testing.T) { + if tt.EnvVars != nil { + for k, v := range tt.EnvVars { + t.Setenv(k, v) + } + } + + actual, err := NormalizePath(tt.Input) + + assert.Equal(t, tt.Output, actual) + if tt.Err == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tt.Err) + } + }) + } +} + +func TestNormalizePath_WhenUserHomeDirError(t *testing.T) { + stubs := gostub.StubFunc(&osUserHomeDir, "", errors.New("boom")) + defer stubs.Reset() + + actual, err := NormalizePath("~/foo") + + assert.Error(t, err) + assert.Equal(t, "", actual) +} + +func TestNormalizePath_WhenAbsError(t *testing.T) { + stubs := gostub.StubFunc(&filepathAbs, "foo", errors.New("boom")) + defer stubs.Reset() + + actual, err := NormalizePath("foo") + + assert.Error(t, err) + assert.Equal(t, "", actual) +} + +func TestEnsureDirWritable(t *testing.T) { + testutil.InTempDir(t, func(tmpDir string) { + dir := filepath.Join(tmpDir, "foo") + err := EnsureDirWritable(dir) + assert.NoError(t, err) + assert.DirExists(t, dir, "dir should exist") + + dirEntry := filepath.Join(dir, "bar") + testutil.WriteFile(t, dirEntry, []byte(""), 0600) + assert.FileExists(t, dirEntry, "dir should be writable") + }) +} + +func TestEnsureDirWritable_WhenMkdirAllError(t *testing.T) { + stubs := gostub.StubFunc(&osMkdirAll, errors.New("boom")) + defer stubs.Reset() + + err := EnsureDirWritable("foo") + assert.Error(t, err) +} + +func TestEnsureDirWritable_WhenWriteFileError(t *testing.T) { + stubs := gostub.StubFunc(&osMkdirAll, nil). + StubFunc(&osWriteFile, errors.New("boom")) + defer stubs.Reset() + + err := EnsureDirWritable("foo") + assert.Error(t, err) +}