author | Alberto Bertogli
<albertito@blitiri.com.ar> 2016-07-11 19:47:15 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2016-07-16 11:33:50 UTC |
parent | 7e7c8073c424da74d203a7060dc1f026d29ca35c |
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).