summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/content/functions/file.md115
-rw-r--r--file/file.go50
-rw-r--r--file/file_test.go65
-rw-r--r--funcs/file.go10
-rw-r--r--tests/integration/file_test.go21
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))
+}