diff options
| author | Dave Henderson <dhenderson@gmail.com> | 2022-01-09 14:45:24 -0500 |
|---|---|---|
| committer | Dave Henderson <dhenderson@gmail.com> | 2022-02-13 11:53:47 -0500 |
| commit | 4510ec9c9e9b1cdce83ec893dfe2aebfdd5db8d7 (patch) | |
| tree | 831048f23820fe9c3c8749bedbbf839a4a546621 | |
| parent | 839e8973475f1f0bdc05657ea13cd23a16d85cd8 (diff) | |
Ensure output file paths exist
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
| -rw-r--r-- | docs/content/config.md | 6 | ||||
| -rw-r--r-- | internal/iohelpers/filemode.go | 6 | ||||
| -rw-r--r-- | internal/iohelpers/filemode_test.go | 4 | ||||
| -rw-r--r-- | internal/tests/integration/basic_test.go | 45 | ||||
| -rw-r--r-- | template.go | 15 | ||||
| -rw-r--r-- | template_test.go | 6 |
6 files changed, 66 insertions, 16 deletions
diff --git a/docs/content/config.md b/docs/content/config.md index c1179f63..22b6f239 100644 --- a/docs/content/config.md +++ b/docs/content/config.md @@ -243,6 +243,9 @@ See [`--output-dir`](../usage/#--input-dir-and---output-dir). The directory to write rendered output files. Must be used with [`inputDir`](#inputdir). +If the directory is missing, it will be created with the same permissions as the +`inputDir`. + ```yaml inputDir: templates/ outputDir: out/ @@ -258,6 +261,9 @@ An array of output file paths. The special value `-` means `Stdout`. Multiple values can be set, but there must be a corresponding number of `inputFiles` entries present. +If any of the parent directories are missing, they will be created with the same +permissions as the input directories. + ```yaml inputFiles: - first.tmpl diff --git a/internal/iohelpers/filemode.go b/internal/iohelpers/filemode.go index 35a3c142..7ac0e3df 100644 --- a/internal/iohelpers/filemode.go +++ b/internal/iohelpers/filemode.go @@ -17,8 +17,10 @@ func NormalizeFileMode(mode os.FileMode) os.FileMode { } func windowsFileMode(mode os.FileMode) os.FileMode { - // non-owner and execute bits are stripped - mode &^= 0o177 + // non-owner and execute bits are stripped on files + if !mode.IsDir() { + mode &^= 0o177 + } if mode&0o200 != 0 { // writeable implies read/write on Windows diff --git a/internal/iohelpers/filemode_test.go b/internal/iohelpers/filemode_test.go index 2ebb0ed7..8a7c9a2e 100644 --- a/internal/iohelpers/filemode_test.go +++ b/internal/iohelpers/filemode_test.go @@ -2,6 +2,7 @@ package iohelpers import ( "fmt" + "io/fs" "os" "testing" @@ -34,4 +35,7 @@ func TestWindowsFileMode(t *testing.T) { assert.Equal(t, fmt.Sprintf("%o", d.expected), fmt.Sprintf("%o", actual)) assert.Equal(t, d.expected, actual) } + + // directories are always 0777 + assert.Equal(t, 0o777|fs.ModeDir, windowsFileMode(0o755|fs.ModeDir)) } diff --git a/internal/tests/integration/basic_test.go b/internal/tests/integration/basic_test.go index 243bef4b..a1e2dc70 100644 --- a/internal/tests/integration/basic_test.go +++ b/internal/tests/integration/basic_test.go @@ -1,6 +1,7 @@ package integration import ( + "io/fs" "io/ioutil" "os" "testing" @@ -8,14 +9,19 @@ import ( "github.com/hairyhenderson/gomplate/v3/internal/iohelpers" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" - "gotest.tools/v3/fs" + testfs "gotest.tools/v3/fs" ) -func setupBasicTest(t *testing.T) *fs.Dir { - tmpDir := fs.NewDir(t, "gomplate-inttests", - fs.WithFile("one", "hi\n", fs.WithMode(0640)), - fs.WithFile("two", "hello\n"), - fs.WithFile("broken", "", fs.WithMode(0000))) +func setupBasicTest(t *testing.T) *testfs.Dir { + tmpDir := testfs.NewDir(t, "gomplate-inttests", + testfs.WithFile("one", "hi\n", testfs.WithMode(0640)), + testfs.WithFile("two", "hello\n"), + testfs.WithFile("broken", "", testfs.WithMode(0000)), + testfs.WithDir("subdir", + testfs.WithFile("f1", "first\n", testfs.WithMode(0640)), + testfs.WithFile("f2", "second\n"), + ), + ) t.Cleanup(tmpDir.Remove) return tmpDir } @@ -256,3 +262,30 @@ func TestBasic_AppliesChmodBeforeWrite(t *testing.T) { assert.NilError(t, err) assert.Equal(t, "hi\n", string(content)) } + +func TestBasic_CreatesMissingDirectory(t *testing.T) { + tmpDir := setupBasicTest(t) + out := tmpDir.Join("foo/bar/baz") + o, e, err := cmd(t, "-f", tmpDir.Join("one"), "-o", out).run() + assertSuccess(t, o, e, err, "") + + info, err := os.Stat(out) + assert.NilError(t, err) + assert.Equal(t, iohelpers.NormalizeFileMode(0640), info.Mode()) + content, err := ioutil.ReadFile(out) + assert.NilError(t, err) + assert.Equal(t, "hi\n", string(content)) + + out = tmpDir.Join("outdir") + o, e, err = cmd(t, + "--input-dir", tmpDir.Join("subdir"), + "--output-dir", out, + ).run() + assertSuccess(t, o, e, err, "") + + info, err = os.Stat(out) + assert.NilError(t, err) + + assert.Equal(t, iohelpers.NormalizeFileMode(0o755|fs.ModeDir), info.Mode()) + assert.Equal(t, true, info.IsDir()) +} diff --git a/template.go b/template.go index 6e5dfb5c..9d66dee0 100644 --- a/template.go +++ b/template.go @@ -145,7 +145,7 @@ func processTemplates(cfg *config.Config, templates []*tplate) ([]*tplate, error } if t.target == nil { - out, err := openOutFile(cfg, t.targetPath, t.mode, t.modeOverride) + out, err := openOutFile(cfg, t.targetPath, 0755, t.mode, t.modeOverride) if err != nil { return nil, err } @@ -241,13 +241,13 @@ func fileToTemplates(inFile, outFile string, mode os.FileMode, modeOverride bool return tmpl, nil } -func openOutFile(cfg *config.Config, filename string, mode os.FileMode, modeOverride bool) (out io.Writer, err error) { +func openOutFile(cfg *config.Config, filename string, dirMode, mode os.FileMode, modeOverride bool) (out io.Writer, err error) { if cfg.SuppressEmpty { out = iohelpers.NewEmptySkipper(func() (io.Writer, error) { if filename == "-" { return cfg.Stdout, nil } - return createOutFile(filename, mode, modeOverride) + return createOutFile(filename, dirMode, mode, modeOverride) }) return out, nil } @@ -255,10 +255,10 @@ func openOutFile(cfg *config.Config, filename string, mode os.FileMode, modeOver if filename == "-" { return cfg.Stdout, nil } - return createOutFile(filename, mode, modeOverride) + return createOutFile(filename, dirMode, mode, modeOverride) } -func createOutFile(filename string, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) { +func createOutFile(filename string, dirMode, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) { mode = iohelpers.NormalizeFileMode(mode.Perm()) if modeOverride { err = fs.Chmod(filename, mode) @@ -268,6 +268,11 @@ func createOutFile(filename string, mode os.FileMode, modeOverride bool) (out io } open := func() (out io.WriteCloser, err error) { + // Ensure file parent dirs + if err = fs.MkdirAll(filepath.Dir(filename), dirMode); err != nil { + return nil, err + } + out, err = fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) if err != nil { return out, fmt.Errorf("failed to open output file '%s' for writing: %w", filename, err) diff --git a/template_test.go b/template_test.go index 10b2f8b9..02a88ded 100644 --- a/template_test.go +++ b/template_test.go @@ -22,7 +22,7 @@ func TestOpenOutFile(t *testing.T) { _ = fs.Mkdir("/tmp", 0777) cfg := &config.Config{} - f, err := openOutFile(cfg, "/tmp/foo", 0644, false) + f, err := openOutFile(cfg, "/tmp/foo", 0755, 0644, false) assert.NoError(t, err) wc, ok := f.(io.WriteCloser) @@ -36,7 +36,7 @@ func TestOpenOutFile(t *testing.T) { cfg.Stdout = &bytes.Buffer{} - f, err = openOutFile(cfg, "-", 0644, false) + f, err = openOutFile(cfg, "-", 0755, 0644, false) assert.NoError(t, err) assert.Equal(t, cfg.Stdout, f) } @@ -261,7 +261,7 @@ func TestCreateOutFile(t *testing.T) { fs = afero.NewMemMapFs() _ = fs.Mkdir("in", 0755) - _, err := createOutFile("in", 0644, false) + _, err := createOutFile("in", 0755, 0644, false) assert.Error(t, err) assert.IsType(t, &os.PathError{}, err) } |
