git » chasquid » commit c12fa0e

Add a new internal/safeio package for safer I/O functions

author Alberto Bertogli
2016-07-11 19:47:15 UTC
committer Alberto Bertogli
2016-07-16 11:33:50 UTC
parent 7e7c8073c424da74d203a7060dc1f026d29ca35c

Add a new internal/safeio package for safer I/O functions

This patch adds a new internal/safeio package, which is meant to implement
safer version of some I/O related functions.

For now, only an atomic version of ioutil.WriteFile is implemented. More may
be added later if there's a need for them.

internal/safeio/safeio.go +43 -0
internal/safeio/safeio_test.go +86 -0

diff --git a/internal/safeio/safeio.go b/internal/safeio/safeio.go
new file mode 100644
index 0000000..127c506
--- /dev/null
+++ b/internal/safeio/safeio.go
@@ -0,0 +1,43 @@
+// Package safeio implements convenient I/O routines that provide additional
+// levels of safety in the presence of unexpected failures.
+package safeio
+
+import (
+	"io/ioutil"
+	"os"
+	"path"
+)
+
+// WriteFile writes data to a file named by filename, atomically.
+// It's a wrapper to ioutil.WriteFile, but provides atomicity (and increased
+// safety) by writing to a temporary file and renaming it at the end.
+//
+// Note this relies on same-directory Rename being atomic, which holds in most
+// reasonably modern filesystems.
+func WriteFile(filename string, data []byte, perm os.FileMode) error {
+	// Note we create the temporary file in the same directory, otherwise we
+	// would have no expectation of Rename being atomic.
+	tmpf, err := ioutil.TempFile(path.Dir(filename), path.Base(filename))
+	if err != nil {
+		return err
+	}
+
+	if err = os.Chmod(tmpf.Name(), perm); err != nil {
+		tmpf.Close()
+		os.Remove(tmpf.Name())
+		return err
+	}
+
+	if _, err = tmpf.Write(data); err != nil {
+		tmpf.Close()
+		os.Remove(tmpf.Name())
+		return err
+	}
+
+	if err = tmpf.Close(); err != nil {
+		os.Remove(tmpf.Name())
+		return err
+	}
+
+	return os.Rename(tmpf.Name(), filename)
+}
diff --git a/internal/safeio/safeio_test.go b/internal/safeio/safeio_test.go
new file mode 100644
index 0000000..13f3a2e
--- /dev/null
+++ b/internal/safeio/safeio_test.go
@@ -0,0 +1,86 @@
+package safeio
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+)
+
+func mustTempDir(t *testing.T) string {
+	dir, err := ioutil.TempDir("", "safeio_test")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = os.Chdir(dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	t.Logf("test directory: %q", dir)
+
+	return dir
+}
+
+func testWriteFile(fname string, data []byte, perm os.FileMode) error {
+	err := WriteFile("file1", data, perm)
+	if err != nil {
+		return fmt.Errorf("error writing new file: %v", err)
+	}
+
+	// Read and compare the contents.
+	c, err := ioutil.ReadFile(fname)
+	if err != nil {
+		return fmt.Errorf("error reading: %v", err)
+	}
+
+	if !bytes.Equal(data, c) {
+		return fmt.Errorf("expected %q, got %q", data, c)
+	}
+
+	// Check permissions.
+	st, err := os.Stat("file1")
+	if err != nil {
+		return fmt.Errorf("error in stat: %v", err)
+	}
+	if st.Mode() != perm {
+		return fmt.Errorf("permissions mismatch, expected %#o, got %#o",
+			st.Mode(), perm)
+	}
+
+	return nil
+}
+
+func TestWriteFile(t *testing.T) {
+	dir := mustTempDir(t)
+
+	// Write a new file.
+	content := []byte("content 1")
+	if err := testWriteFile("file1", content, 0660); err != nil {
+		t.Error(err)
+	}
+
+	// Write an existing file.
+	content = []byte("content 2")
+	if err := testWriteFile("file1", content, 0660); err != nil {
+		t.Error(err)
+	}
+
+	// Write again, but this time change permissions.
+	content = []byte("content 3")
+	if err := testWriteFile("file1", content, 0600); err != nil {
+		t.Error(err)
+	}
+
+	// Remove the test directory, but only if we have not failed. We want to
+	// keep the failed structure for debugging.
+	if !t.Failed() {
+		os.RemoveAll(dir)
+	}
+}
+
+// TODO: We should test the possible failure scenarios for WriteFile, but it
+// gets tricky without being able to do failure injection (or turning the code
+// into a mess).