Skip to content

Commit

Permalink
Merge pull request #8914 from z017/master
Browse files Browse the repository at this point in the history
fn: add synchronous write file
  • Loading branch information
guggero authored Dec 16, 2024
2 parents 522bb82 + 0652cbd commit 729cd22
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 0 deletions.
45 changes: 45 additions & 0 deletions fn/io.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package fn

import (
"os"
)

// WriteFile synchronously writes data to the named file.
// If the file does not exist, WriteFile creates it with permissions perm
// (before umask); otherwise WriteFile truncates it before writing, without
// changing permissions.
// By opening the file with O_SYNC, it ensures the data is written to disk.
// If an error occurs, it does not remove the file.
func WriteFile(name string, data []byte, perm os.FileMode) error {
f, err := os.OpenFile(
name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_SYNC, perm,
)
if err != nil {
return err
}

_, err = f.Write(data)

// Prioritize the error on Write but make sure to call Close regardless
// to avoid leaking a file handle.
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}

return err
}

// WriteFileRemove synchronously writes data to the named file.
// If the file does not exist, WriteFileRemove creates it with permissions perm
// (before umask); otherwise WriteFileRemove truncates it before writing,
// without changing permissions.
// By opening the file with O_SYNC, it ensures the data is written to disk.
// If an error occurs, it removes the file.
func WriteFileRemove(name string, data []byte, perm os.FileMode) error {
err := WriteFile(name, data, perm)
if err != nil {
_ = os.Remove(name)
}

return err
}
96 changes: 96 additions & 0 deletions fn/io_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package fn

import (
"os"
"testing"

"github.com/stretchr/testify/require"
)

var (
data1 = "The network is robust in its unstructured simplicity."
data2 = "Nodes work all at once with little coordination."
)

// TestWriteFile uses same scenario of ioutil asserting the content created and
// stored on new file with the original one.
func TestWriteFile(t *testing.T) {
t.Parallel()

f, deferred := ensureTempfile(t)
filename := f.Name()
defer deferred()

// Write file.
err := WriteFile(filename, []byte(data2), 0644)
require.NoError(t, err, "couldn't write file")
ensureFileContents(t, filename, data2)

// Overwrite file.
err = WriteFile(filename, []byte(data1), 0644)
require.NoError(t, err, "couldn't overwrite file")
ensureFileContents(t, filename, data1)

// Change file permission to read-only.
err = os.Chmod(filename, 0444)
require.NoError(t, err, "couldn't change file to read-only")

// Write must fail and keep the file.
err = WriteFile(filename, []byte(data2), 0644)
require.Error(t, err, "shouldn't write a read-only file")

_, err = os.Stat(filename)
require.NoError(t, err, "shouldn't remove file on error")
ensureFileContents(t, filename, data1)
}

func TestWriteFileRemove(t *testing.T) {
t.Parallel()

f, deferred := ensureTempfile(t)
filename := f.Name()
defer deferred()

// Write file.
err := WriteFileRemove(filename, []byte(data2), 0644)
require.NoError(t, err, "couldn't write file")
ensureFileContents(t, filename, data2)

// Overwrite file.
err = WriteFileRemove(filename, []byte(data1), 0644)
require.NoError(t, err, "couldn't overwrite file")
ensureFileContents(t, filename, data1)

// Change file permission to read-only.
err = os.Chmod(filename, 0444)
require.NoError(t, err, "couldn't change file to read-only")

// Write must fail and remove the file.
err = WriteFileRemove(filename, []byte(data2), 0644)
require.Error(t, err, "shouldn't write a read-only file")

_, err = os.Stat(filename)
require.ErrorContains(
t, err, "no such file or directory",
"should remove file on error",
)
}

func ensureTempfile(t *testing.T) (*os.File, func()) {
t.Helper()
f, err := os.CreateTemp("", "fn-io-test-TestWriteFile")
require.NoError(t, err, "couldn't create temporary file")

return f, func() {
f.Close()
os.Remove(f.Name())
}
}

func ensureFileContents(t *testing.T, filename string, data string) {
t.Helper()
contents, err := os.ReadFile(filename)
require.NoError(t, err, "couldn't read file")

require.Equal(t, data, string(contents))
}

0 comments on commit 729cd22

Please sign in to comment.