From 661b8073b9e77a84ecf0111ad9464b4bfe0bbfe2 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Thu, 14 Feb 2019 22:16:53 -0500 Subject: New file.Write function Signed-off-by: Dave Henderson --- docs/content/functions/file.md | 115 ++++++++++++++++++++++++++++++++++++----- file/file.go | 50 ++++++++++++++++++ file/file_test.go | 65 +++++++++++++++++++++++ funcs/file.go | 10 ++++ tests/integration/file_test.go | 21 ++++++++ 5 files changed, 249 insertions(+), 12 deletions(-) diff --git a/docs/content/functions/file.md b/docs/content/functions/file.md index 6ae0606f..3bebe6a8 100644 --- a/docs/content/functions/file.md +++ b/docs/content/functions/file.md @@ -5,16 +5,27 @@ menu: parent: functions --- + ## `file.Exists` Reports whether a file or directory exists at the given path. ### Usage ```go -file.Exists path +file.Exists path +``` + +```go +path | file.Exists ``` -### Example +### Arguments + +| name | description | +|------|-------------| +| `path` | _(required)_ The path | + +### Examples _`input.tmpl`:_ ``` @@ -35,10 +46,20 @@ Reports whether a given path is a directory. ### Usage ```go -file.IsDir path +file.IsDir path ``` -### Example +```go +path | file.IsDir +``` + +### Arguments + +| name | description | +|------|-------------| +| `path` | _(required)_ The path | + +### Examples _`input.tmpl`:_ ``` @@ -58,14 +79,23 @@ yes ## `file.Read` -Reads a given file _as text_. Note that this will succeed if the given file -is binary, but +Reads a given file _as text_. Note that this will succeed if the given file is binary, but the output may be gibberish. ### Usage ```go -file.Read path +file.Read path ``` +```go +path | file.Read +``` + +### Arguments + +| name | description | +|------|-------------| +| `path` | _(required)_ The path | + ### Examples ```console @@ -80,9 +110,19 @@ Reads a directory and lists the files and directories contained within. ### Usage ```go -file.ReadDir path +file.ReadDir path ``` +```go +path | file.ReadDir +``` + +### Arguments + +| name | description | +|------|-------------| +| `path` | _(required)_ The path | + ### Examples ```console @@ -98,15 +138,25 @@ d ## `file.Stat` -Returns a [`os.FileInfo`](https://golang.org/pkg/os/#FileInfo) describing -the named path. +Returns a [`os.FileInfo`](https://golang.org/pkg/os/#FileInfo) describing the named path. + Essentially a wrapper for Go's [`os.Stat`](https://golang.org/pkg/os/#Stat) function. ### Usage ```go -file.Stat path +file.Stat path +``` + +```go +path | file.Stat ``` +### Arguments + +| name | description | +|------|-------------| +| `path` | _(required)_ The path | + ### Examples ```console @@ -126,11 +176,20 @@ Walk does not follow symbolic links. Similar to Go's [`filepath.Walk`](https://golang.org/pkg/path/filepath/#Walk) function. ### Usage +```go +file.Walk path +``` ```go -file.Walk path +path | file.Walk ``` +### Arguments + +| name | description | +|------|-------------| +| `path` | _(required)_ The path | + ### Examples ```console @@ -151,3 +210,35 @@ $ gomplate -i '{{ range file.Walk "/tmp/foo" }}{{ if not (file.IsDir .) }}{{.}} /tmp/foo/three is a file /tmp/foo/two is a file ``` + +## `file.Write` + +Write the given data to the given file. If the file exists, it will be overwritten. + +For increased security, `file.Write` will only write to files which are contained within the current working directory. Attempts to write elsewhere will fail with an error. + +If the data is a byte array (`[]byte`), it will be written as-is. Otherwise, it will be converted to a string before being written. + +### Usage +```go +file.Write filename data +``` + +```go +data | file.Write filename +``` + +### Arguments + +| name | description | +|------|-------------| +| `filename` | _(required)_ The name of the file to write to | +| `data` | _(required)_ The data to write | + +### Examples + +```console +$ gomplate -i '{{ file.Write "/tmp/foo" "hello world" }}' +$ cat /tmp/foo +hello world +``` diff --git a/file/file.go b/file/file.go index f8fa2839..16067161 100644 --- a/file/file.go +++ b/file/file.go @@ -3,6 +3,8 @@ package file import ( "io/ioutil" "os" + "path/filepath" + "strings" "github.com/pkg/errors" @@ -43,3 +45,51 @@ func ReadDir(path string) ([]string, error) { } return nil, errors.New("file is not a directory") } + +// Write a +func Write(filename string, content []byte) error { + err := assertPathInWD(filename) + if err != nil { + return errors.Wrapf(err, "failed to open %s", filename) + } + + fi, err := os.Stat(filename) + if err != nil && !os.IsNotExist(err) { + return errors.Wrapf(err, "failed to stat %s", filename) + } + mode := os.FileMode(0644) + if fi != nil { + mode = fi.Mode() + } + inFile, err := fs.OpenFile(filename, os.O_RDWR|os.O_CREATE, mode) + if err != nil { + return errors.Wrapf(err, "failed to open %s", filename) + } + n, err := inFile.Write(content) + if err != nil { + return errors.Wrapf(err, "failed to write %s", filename) + } + if n != len(content) { + return errors.Wrapf(err, "short write on %s (%d bytes)", filename, n) + } + return nil +} + +func assertPathInWD(filename string) error { + wd, err := os.Getwd() + if err != nil { + return err + } + f, err := filepath.Abs(filename) + if err != nil { + return err + } + r, err := filepath.Rel(wd, f) + if err != nil { + return err + } + if strings.HasPrefix(r, "..") { + return errors.Errorf("path %s not contained by working directory %s (rel: %s)", filename, wd, r) + } + return nil +} diff --git a/file/file_test.go b/file/file_test.go index 144c67a5..47c0d542 100644 --- a/file/file_test.go +++ b/file/file_test.go @@ -1,8 +1,12 @@ package file import ( + "io/ioutil" + "os" + "path/filepath" "testing" + tfs "github.com/gotestyourself/gotestyourself/fs" "github.com/spf13/afero" "github.com/stretchr/testify/assert" ) @@ -41,3 +45,64 @@ func TestReadDir(t *testing.T) { _, err = ReadDir("/tmp/foo") assert.Error(t, err) } + +func TestWrite(t *testing.T) { + oldwd, _ := os.Getwd() + defer os.Chdir(oldwd) + + rootDir := tfs.NewDir(t, "gomplate-test") + defer rootDir.Remove() + + newwd := rootDir.Join("the", "path", "we", "want") + badwd := rootDir.Join("some", "other", "dir") + fs.MkdirAll(newwd, 0755) + fs.MkdirAll(badwd, 0755) + newwd, _ = filepath.EvalSymlinks(newwd) + badwd, _ = filepath.EvalSymlinks(badwd) + + err := os.Chdir(newwd) + assert.NoError(t, err) + + err = Write("/foo", []byte("Hello world")) + assert.Error(t, err) + + rel, err := filepath.Rel(newwd, badwd) + assert.NoError(t, err) + err = Write(rel, []byte("Hello world")) + assert.Error(t, err) + + foopath := filepath.Join(newwd, "foo") + err = Write(foopath, []byte("Hello world")) + assert.NoError(t, err) + + out, err := ioutil.ReadFile(foopath) + assert.NoError(t, err) + assert.Equal(t, "Hello world", string(out)) +} + +func TestAssertPathInWD(t *testing.T) { + oldwd, _ := os.Getwd() + defer os.Chdir(oldwd) + + err := assertPathInWD("/tmp") + assert.Error(t, err) + + err = assertPathInWD(filepath.Join(oldwd, "subpath")) + assert.NoError(t, err) + + err = assertPathInWD("subpath") + assert.NoError(t, err) + + err = assertPathInWD("./subpath") + assert.NoError(t, err) + + err = assertPathInWD(filepath.Join("..", "bogus")) + assert.Error(t, err) + + err = assertPathInWD(filepath.Join("..", "..", "bogus")) + assert.Error(t, err) + + base := filepath.Base(oldwd) + err = assertPathInWD(filepath.Join("..", base)) + assert.NoError(t, err) +} diff --git a/funcs/file.go b/funcs/file.go index 7ba2ec40..dc74717f 100644 --- a/funcs/file.go +++ b/funcs/file.go @@ -69,3 +69,13 @@ func (f *FileFuncs) Walk(path interface{}) ([]string, error) { }) return files, err } + +// Write - +func (f *FileFuncs) Write(path interface{}, data interface{}) (s string, err error) { + if b, ok := data.([]byte); ok { + err = file.Write(conv.ToString(path), b) + } else { + err = file.Write(conv.ToString(path), []byte(conv.ToString(data))) + } + return "", err +} diff --git a/tests/integration/file_test.go b/tests/integration/file_test.go index 2685df73..d96278c3 100644 --- a/tests/integration/file_test.go +++ b/tests/integration/file_test.go @@ -4,9 +4,16 @@ package integration import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/gotestyourself/gotestyourself/assert" + . "gopkg.in/check.v1" "github.com/gotestyourself/gotestyourself/fs" + "github.com/gotestyourself/gotestyourself/icmd" ) type FileSuite struct { @@ -28,3 +35,17 @@ func (s *FileSuite) TearDownSuite(c *C) { func (s *FileSuite) TestReadsFile(c *C) { inOutTest(c, `{{ file.Read "`+s.tmpDir.Join("one")+`"}}`, "hi") } + +func (s *FileSuite) TestWrite(c *C) { + outDir := s.tmpDir.Join("writeOutput") + os.MkdirAll(outDir, 0755) + result := icmd.RunCmd(icmd.Command(GomplateBin, + "-i", `{{ "hello world" | file.Write "./out" }}`, + ), func(cmd *icmd.Cmd) { + cmd.Dir = outDir + }) + result.Assert(c, icmd.Expected{ExitCode: 0}) + out, err := ioutil.ReadFile(filepath.Join(outDir, "out")) + assert.NilError(c, err) + assert.Equal(c, "hello world", string(out)) +} -- cgit v1.2.3