mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-21 15:17:01 +00:00
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.
This commit is contained in:
43
internal/safeio/safeio.go
Normal file
43
internal/safeio/safeio.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
86
internal/safeio/safeio_test.go
Normal file
86
internal/safeio/safeio_test.go
Normal file
@@ -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).
|
||||||
Reference in New Issue
Block a user