When we put something in the queue and respond "250 ok" to the client, that is taken as accepting the email. As part of putting something in the queue, we write it to disk, but today we don't do an fsync on that file. That leaves a gap where a badly timed crash on some systems could lead to the file being empty, causing us to lose an email that we accepted. To elliminate (or drastically reduce on some filesystems) the chances of that situation, we call fsync on the file that gets written when we put something in the queue. Thanks to nolanl@github for reporting this in https://github.com/albertito/chasquid/issues/78.
275 lines
6.4 KiB
Go
275 lines
6.4 KiB
Go
package safeio
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"blitiri.com.ar/go/chasquid/internal/testlib"
|
|
)
|
|
|
|
func testWriteFile(fname string, data []byte, perm os.FileMode, ops ...FileOp) error {
|
|
err := WriteFile("file1", data, perm, ops...)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing new file: %v", err)
|
|
}
|
|
|
|
// Read and compare the contents.
|
|
c, err := os.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 := testlib.MustTempDir(t)
|
|
defer testlib.RemoveIfOk(t, dir)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
func TestWriteFileWithOp(t *testing.T) {
|
|
dir := testlib.MustTempDir(t)
|
|
defer testlib.RemoveIfOk(t, dir)
|
|
|
|
var opFile string
|
|
op := func(f string) error {
|
|
opFile = f
|
|
return nil
|
|
}
|
|
|
|
content := []byte("content 1")
|
|
if err := testWriteFile("file1", content, 0660, op); err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
if opFile == "" {
|
|
t.Error("operation was not called")
|
|
}
|
|
if !strings.Contains(opFile, "file1") {
|
|
t.Errorf("operation called with suspicious file: %s", opFile)
|
|
}
|
|
}
|
|
|
|
func TestWriteFileWithFailingOp(t *testing.T) {
|
|
dir := testlib.MustTempDir(t)
|
|
defer testlib.RemoveIfOk(t, dir)
|
|
|
|
var opFile string
|
|
opOK := func(f string) error {
|
|
opFile = f
|
|
return nil
|
|
}
|
|
|
|
opError := errors.New("operation failed")
|
|
opFail := func(f string) error {
|
|
return opError
|
|
}
|
|
|
|
content := []byte("content 1")
|
|
err := WriteFile("file1", content, 0660, opOK, opOK, opFail)
|
|
if err != opError {
|
|
t.Errorf("different error, got %v, expected %v", err, opError)
|
|
}
|
|
|
|
if _, err := os.Stat(opFile); err == nil {
|
|
t.Errorf("temporary file was not removed after failure (%v)", opFile)
|
|
}
|
|
}
|
|
|
|
type testFile struct {
|
|
t *testing.T
|
|
|
|
name string
|
|
|
|
expectChmod os.FileMode
|
|
chmodErr error
|
|
|
|
expectChownUid, expectChownGid int
|
|
chownErr error
|
|
|
|
expectWrite []byte
|
|
writeN int
|
|
writeErr error
|
|
|
|
closeErr error
|
|
}
|
|
|
|
func (f *testFile) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
func (f *testFile) Chmod(perm os.FileMode) error {
|
|
if f.expectChmod != perm {
|
|
f.t.Errorf("unexpected Chmod(%v), expected Chmod(%v)",
|
|
perm, f.expectChmod)
|
|
}
|
|
return f.chmodErr
|
|
}
|
|
|
|
func (f *testFile) Chown(uid, gid int) error {
|
|
if f.expectChownUid != uid || f.expectChownGid != gid {
|
|
f.t.Errorf("unexpected Chown(%v, %v), expected Chown(%v, %v)",
|
|
uid, gid, f.expectChownUid, f.expectChownGid)
|
|
}
|
|
return f.chownErr
|
|
}
|
|
|
|
func (f *testFile) Write(b []byte) (int, error) {
|
|
if !bytes.Equal(b, f.expectWrite) {
|
|
f.t.Errorf("unexpected Write(%q), expected Write(%q)",
|
|
b, f.expectWrite)
|
|
}
|
|
return f.writeN, f.writeErr
|
|
}
|
|
|
|
func (f *testFile) Close() error {
|
|
return f.closeErr
|
|
}
|
|
|
|
var _ osFile = &testFile{}
|
|
|
|
func TestErrors(t *testing.T) {
|
|
dir := testlib.MustTempDir(t)
|
|
defer testlib.RemoveIfOk(t, dir)
|
|
|
|
oldCreateTemp := createTemp
|
|
defer func() { createTemp = oldCreateTemp }()
|
|
|
|
// createTemp failure.
|
|
ctError := errors.New("createTemp error")
|
|
createTemp = func(dir, pattern string) (osFile, error) {
|
|
return nil, ctError
|
|
}
|
|
err := WriteFile("fname", []byte("new content"), 0660)
|
|
if err != ctError {
|
|
t.Errorf("expected %v, got %v", ctError, err)
|
|
}
|
|
|
|
// Have a real backing file for some of the operations, like getting the
|
|
// owner.
|
|
fname := dir + "/file1"
|
|
|
|
// Test file to simulate failures on.
|
|
tf := &testFile{name: fname, t: t}
|
|
createTemp = func(dir, pattern string) (osFile, error) {
|
|
return tf, nil
|
|
}
|
|
|
|
// Test Chmod error.
|
|
testlib.Rewrite(t, fname, "old content")
|
|
tf.expectChmod = 0660
|
|
tf.chmodErr = errors.New("chmod error")
|
|
err = WriteFile(fname, []byte("new content"), 0660)
|
|
if err != tf.chmodErr {
|
|
t.Errorf("expected %v, got %v", tf.chmodErr, err)
|
|
}
|
|
checkNotExists(t, fname)
|
|
|
|
// Test Chown error.
|
|
testlib.Rewrite(t, fname, "old content")
|
|
tf.chmodErr = nil
|
|
tf.expectChownUid, tf.expectChownGid = getOwner(fname)
|
|
if tf.expectChownUid < 0 {
|
|
t.Fatalf("error getting owner of %v", fname)
|
|
}
|
|
tf.chownErr = errors.New("chown error")
|
|
err = WriteFile(fname, []byte("new content"), 0660)
|
|
if err != tf.chownErr {
|
|
t.Errorf("expected %v, got %v", tf.chownErr, err)
|
|
}
|
|
checkNotExists(t, fname)
|
|
|
|
// Test Write error.
|
|
testlib.Rewrite(t, fname, "old content")
|
|
tf.chownErr = nil
|
|
tf.expectWrite = []byte("new content")
|
|
tf.writeErr = errors.New("write error")
|
|
err = WriteFile(fname, []byte("new content"), 0660)
|
|
if err != tf.writeErr {
|
|
t.Errorf("expected %v, got %v", tf.writeErr, err)
|
|
}
|
|
checkNotExists(t, fname)
|
|
|
|
// Test Close error.
|
|
testlib.Rewrite(t, fname, "old content")
|
|
tf.writeErr = nil
|
|
tf.writeN = len(tf.expectWrite)
|
|
tf.closeErr = errors.New("close error")
|
|
err = WriteFile(fname, []byte("new content"), 0660)
|
|
if err != tf.closeErr {
|
|
t.Errorf("expected %v, got %v", tf.closeErr, err)
|
|
}
|
|
checkNotExists(t, fname)
|
|
}
|
|
|
|
func checkNotExists(t *testing.T, fname string) {
|
|
t.Helper()
|
|
if _, err := os.Stat(fname); err == nil {
|
|
t.Fatalf("file %v exists", fname)
|
|
}
|
|
}
|
|
|
|
func TestFsyncFileOp(t *testing.T) {
|
|
// Check it as an operation on a normal file.
|
|
dir := testlib.MustTempDir(t)
|
|
defer testlib.RemoveIfOk(t, dir)
|
|
|
|
content := []byte("content 1")
|
|
err := testWriteFile("file1", content, 0660, FsyncFileOp)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Use a file that doesn't exist to trigger an Open error.
|
|
err = FsyncFileOp("doesnotexist")
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
t.Errorf("expected an fs.ErrNotExist, got %#v", err)
|
|
}
|
|
|
|
// To trigger a Sync error, we use /dev/null, which is writable but cannot
|
|
// be synced. Confirm the error is a fs.PathError and comes from the sync
|
|
// operation, to make sure we're not accidentally failing on Open or
|
|
// Close.
|
|
err = FsyncFileOp("/dev/null")
|
|
if pe, ok := err.(*fs.PathError); !ok || pe.Op != "sync" {
|
|
t.Errorf("expected a fs.PathError from sync, got %#v", err)
|
|
}
|
|
}
|