Initial commit after importing from github
This commit is contained in:
commit
61c1049e28
|
@ -0,0 +1,39 @@
|
||||||
|
# src.lwithers.me.uk/go/writefile
|
||||||
|
|
||||||
|
This package implements the common Unix technique of writing a new file by first
|
||||||
|
writing a temporary file, then ensuring it is fsync()ed, before calling rename()
|
||||||
|
to atomically move into its final resting place.
|
||||||
|
|
||||||
|
What problem does this solve? It prevents writing a partial output file, or
|
||||||
|
corrupting an existing file with a partially-written new version, if there is a
|
||||||
|
crash or other unexpected exit of the program.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
finalFname, out, err := writefile.New("my-file")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer writefile.Abort(out)
|
||||||
|
|
||||||
|
// … write to ‘out’ …
|
||||||
|
|
||||||
|
return writefile.Commit(finalFname, out)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `New()` method returns a final filename string which is later passed to
|
||||||
|
`Commit()`. This will typically match the filename argument passed to `New()`,
|
||||||
|
except if the file is a symlink, in which case the symlink is dereferenced
|
||||||
|
and its target is overwritten. If this behaviour is undesired, the following
|
||||||
|
pattern may be used instead:
|
||||||
|
|
||||||
|
```
|
||||||
|
out, err := writefile.NewNoDeref("my-file")
|
||||||
|
// …
|
||||||
|
return writefile.Commit("my-file", out)
|
||||||
|
```
|
||||||
|
|
||||||
|
The temporary file being written will have 0600 permissions. If an existing
|
||||||
|
file is overwritten by `Commit()`, the call will attempt to inherit the existing
|
||||||
|
permissions. Otherwise, the final file is left with 0600.
|
|
@ -0,0 +1,5 @@
|
||||||
|
module src.lwithers.me.uk/go/writefile
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8
|
|
@ -0,0 +1,2 @@
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
@ -0,0 +1,264 @@
|
||||||
|
package writefile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tmpdir string
|
||||||
|
tmpfileCount int
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
var err error
|
||||||
|
if tmpdir, err = ioutil.TempDir("", "tmpfile-unit-test"); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Could not create temporary "+
|
||||||
|
"directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := m.Run()
|
||||||
|
|
||||||
|
os.RemoveAll(tmpdir)
|
||||||
|
os.Exit(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommit(t *testing.T) {
|
||||||
|
expData := []byte("test")
|
||||||
|
|
||||||
|
ff, tmpf := makeTempFile(t, "commit", "commit")
|
||||||
|
if _, err := tmpf.Write(expData); err != nil {
|
||||||
|
t.Errorf("error writing to temporary file: %v", err)
|
||||||
|
}
|
||||||
|
if err := Commit(ff, tmpf); err != nil {
|
||||||
|
t.Errorf("error committing temporary file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkFile(t, "final", ff, expData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverwrite(t *testing.T) {
|
||||||
|
origData := []byte("original")
|
||||||
|
expData := []byte("testing")
|
||||||
|
|
||||||
|
// write out original data
|
||||||
|
ff := filepath.Join(tmpdir, "overwrite")
|
||||||
|
if err := ioutil.WriteFile(ff, origData, 0666); err != nil {
|
||||||
|
t.Fatalf("could not prepare file %s: %v", ff, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open a temporary file — should not overwrite our original
|
||||||
|
_, tmpf := makeTempFile(t, "overwrite", "overwrite")
|
||||||
|
checkFile(t, "tmpfile open", ff, origData)
|
||||||
|
|
||||||
|
// write some data to temporary file, and verify original still intact
|
||||||
|
if _, err := tmpf.Write(expData); err != nil {
|
||||||
|
t.Errorf("error writing to temporary file: %v", err)
|
||||||
|
}
|
||||||
|
checkFile(t, "tmpfile modified", ff, origData)
|
||||||
|
|
||||||
|
// now Commit — original should be overwritten
|
||||||
|
if err := Commit(ff, tmpf); err != nil {
|
||||||
|
t.Errorf("error committing temporary file: %v", err)
|
||||||
|
}
|
||||||
|
checkFile(t, "tmpfile committed", ff, expData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAbort(t *testing.T) {
|
||||||
|
origData := []byte("original")
|
||||||
|
expData := []byte("testing")
|
||||||
|
|
||||||
|
// write out original data
|
||||||
|
ff := filepath.Join(tmpdir, "overwrite_abort")
|
||||||
|
if err := ioutil.WriteFile(ff, origData, 0666); err != nil {
|
||||||
|
t.Fatalf("could not prepare file %s: %v", ff, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open a temporary file — should not overwrite our original
|
||||||
|
_, tmpf := makeTempFile(t, "overwrite_abort", "overwrite_abort")
|
||||||
|
checkFile(t, "tmpfile open", ff, origData)
|
||||||
|
|
||||||
|
// write some data to temporary file, and verify original still intact
|
||||||
|
if _, err := tmpf.Write(expData); err != nil {
|
||||||
|
t.Errorf("error writing to temporary file: %v", err)
|
||||||
|
}
|
||||||
|
checkFile(t, "tmpfile modified", ff, origData)
|
||||||
|
|
||||||
|
// now Abort — original should not be overwritten
|
||||||
|
Abort(tmpf)
|
||||||
|
checkFile(t, "tmpfile aborted", ff, origData)
|
||||||
|
if err := tmpf.Close(); err == nil {
|
||||||
|
t.Error("expected error when closing aborted file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSymlinkRel(t *testing.T) {
|
||||||
|
origData := []byte("original")
|
||||||
|
expData := []byte("testing")
|
||||||
|
|
||||||
|
// write out original data
|
||||||
|
ff := filepath.Join(tmpdir, "sym_target")
|
||||||
|
if err := ioutil.WriteFile(ff, origData, 0666); err != nil {
|
||||||
|
t.Fatalf("could not prepare file %s: %v", ff, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a symlink
|
||||||
|
tgt := filepath.Join(tmpdir, "sym")
|
||||||
|
if err := os.Symlink("sym_target", tgt); err != nil {
|
||||||
|
t.Fatalf("could not prepare symlink %s: %v", tgt, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open a temporary file — should not overwrite our original
|
||||||
|
_, tmpf := makeTempFile(t, "sym", "sym_target")
|
||||||
|
checkFile(t, "tmpfile open", ff, origData)
|
||||||
|
|
||||||
|
// write some data to temporary file, and verify original still intact
|
||||||
|
if _, err := tmpf.Write(expData); err != nil {
|
||||||
|
t.Errorf("error writing to temporary file: %v", err)
|
||||||
|
}
|
||||||
|
checkFile(t, "tmpfile modified", ff, origData)
|
||||||
|
|
||||||
|
// now Commit — original should be overwritten
|
||||||
|
if err := Commit(ff, tmpf); err != nil {
|
||||||
|
t.Errorf("error committing temporary file: %v", err)
|
||||||
|
}
|
||||||
|
checkFile(t, "tmpfile committed", ff, expData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSymlinkAbs(t *testing.T) {
|
||||||
|
origData := []byte("original")
|
||||||
|
expData := []byte("testing")
|
||||||
|
|
||||||
|
// write out original data
|
||||||
|
ff := filepath.Join(tmpdir, "sym_target_abs")
|
||||||
|
if err := ioutil.WriteFile(ff, origData, 0666); err != nil {
|
||||||
|
t.Fatalf("could not prepare file %s: %v", ff, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a symlink
|
||||||
|
tgt := filepath.Join(tmpdir, "sym_abs")
|
||||||
|
if err := os.Symlink(filepath.Join(tmpdir, "sym_target_abs"), tgt); err != nil {
|
||||||
|
t.Fatalf("could not prepare symlink %s: %v", tgt, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open a temporary file — should not overwrite our original
|
||||||
|
_, tmpf := makeTempFile(t, "sym_abs", "sym_target_abs")
|
||||||
|
checkFile(t, "tmpfile open", ff, origData)
|
||||||
|
|
||||||
|
// write some data to temporary file, and verify original still intact
|
||||||
|
if _, err := tmpf.Write(expData); err != nil {
|
||||||
|
t.Errorf("error writing to temporary file: %v", err)
|
||||||
|
}
|
||||||
|
checkFile(t, "tmpfile modified", ff, origData)
|
||||||
|
|
||||||
|
// now Commit — original should be overwritten
|
||||||
|
if err := Commit(ff, tmpf); err != nil {
|
||||||
|
t.Errorf("error committing temporary file: %v", err)
|
||||||
|
}
|
||||||
|
checkFile(t, "tmpfile committed", ff, expData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSymlinkEnoent(t *testing.T) {
|
||||||
|
expData := []byte("testing")
|
||||||
|
|
||||||
|
// create a symlink (pointing at a file which doesn't exist)
|
||||||
|
tgt := filepath.Join(tmpdir, "sym_enoent")
|
||||||
|
if err := os.Symlink("enoent", tgt); err != nil {
|
||||||
|
t.Fatalf("could not prepare symlink %s: %v", tgt, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open a temporary file
|
||||||
|
ff, tmpf := makeTempFile(t, "sym_enoent", "enoent")
|
||||||
|
|
||||||
|
// write some data to temporary file, commit and verify
|
||||||
|
if _, err := tmpf.Write(expData); err != nil {
|
||||||
|
t.Errorf("error writing to temporary file: %v", err)
|
||||||
|
}
|
||||||
|
if err := Commit(ff, tmpf); err != nil {
|
||||||
|
t.Errorf("error committing temporary file: %v", err)
|
||||||
|
}
|
||||||
|
checkFile(t, "tmpfile committed", ff, expData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSymlinkLoop(t *testing.T) {
|
||||||
|
a := filepath.Join(tmpdir, "symlink_loop_a")
|
||||||
|
b := filepath.Join(tmpdir, "symlink_loop_b")
|
||||||
|
if err := os.Symlink(a, b); err != nil {
|
||||||
|
t.Fatalf("failed to create symlink %s→%s: %v", a, b, err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(b, a); err != nil {
|
||||||
|
t.Fatalf("failed to create symlink %s→%s: %v", b, a, err)
|
||||||
|
}
|
||||||
|
ff, tmpf, err := New(a)
|
||||||
|
switch err := err.(type) {
|
||||||
|
case nil:
|
||||||
|
t.Errorf("error expected, but New succeeded (%q/%q)",
|
||||||
|
ff, tmpf.Name())
|
||||||
|
case *os.PathError:
|
||||||
|
if err.Path != a {
|
||||||
|
t.Errorf("got *os.PathError with unexpected path "+
|
||||||
|
"(got %#v, expected %q)", err, a)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Errorf("got unexpected error type %T (%v)", err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSymlinkDir(t *testing.T) {
|
||||||
|
dnam := filepath.Join(tmpdir, "target_dir")
|
||||||
|
if err := os.Mkdir(dnam, 0777); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sym := filepath.Join(tmpdir, "sym_dir")
|
||||||
|
if err := os.Symlink(dnam, sym); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ff, tmpf, err := New(sym)
|
||||||
|
switch err := err.(type) {
|
||||||
|
case nil:
|
||||||
|
t.Errorf("error expected, but New succeeded (%q/%q)",
|
||||||
|
ff, tmpf.Name())
|
||||||
|
case *os.PathError:
|
||||||
|
if err.Path != sym {
|
||||||
|
t.Errorf("got *os.PathError with unexpected path "+
|
||||||
|
"(got %#v, expected %q)", err, sym)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Errorf("got unexpected error type %T (%v)", err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTempFile(t *testing.T, srcName, expFinal string) (string, *os.File) {
|
||||||
|
tmpfileCount++
|
||||||
|
final := filepath.Join(tmpdir, srcName)
|
||||||
|
ff, tmpf, err := New(final)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not create temporary file for %q: %v",
|
||||||
|
final, err)
|
||||||
|
}
|
||||||
|
if ff != filepath.Join(tmpdir, expFinal) {
|
||||||
|
t.Fatalf("unexpected final fname %q does not match expected %q",
|
||||||
|
ff, filepath.Join(tmpdir, expFinal))
|
||||||
|
}
|
||||||
|
if filepath.Dir(tmpf.Name()) != tmpdir {
|
||||||
|
t.Fatalf("unexpected final dir %q does not match expected %q",
|
||||||
|
filepath.Dir(tmpf.Name()), tmpdir)
|
||||||
|
}
|
||||||
|
return ff, tmpf
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFile(t *testing.T, label, fname string, exp []byte) {
|
||||||
|
if raw, err := ioutil.ReadFile(fname); err != nil {
|
||||||
|
t.Errorf("%s: could not read file: %v", label, err)
|
||||||
|
} else if !bytes.Equal(raw, exp) {
|
||||||
|
t.Errorf("%s: data in %s not as expected", label, fname)
|
||||||
|
t.Errorf("%s: got %q, expected %q", label, raw, exp)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
Package writefile provides simple support routines for writing data to a temporary
|
||||||
|
file before renaming it into place. This avoids pitfalls such as writing partial
|
||||||
|
content to a file and then being interruted, or trying to write to a program that
|
||||||
|
is currently being executed, etc.
|
||||||
|
|
||||||
|
This package will correctly dereference symlinks and in the case of overwriting
|
||||||
|
will retain permissions from the original, underlying file.
|
||||||
|
*/
|
||||||
|
package writefile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MaxSymlinkDeref is the maximum number of symlinks that we will
|
||||||
|
// dereference before giving up.
|
||||||
|
MaxSymlinkDeref = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// New opens a file for writing. It returns the final filename which should be
|
||||||
|
// passed to Commit; this may differ from the passed target filename if the
|
||||||
|
// target is actually a symlink.
|
||||||
|
//
|
||||||
|
// Any existing file will not be altered in any way until Commit() is called.
|
||||||
|
// Data will be written to a temporary file in the same directory as the
|
||||||
|
// target.
|
||||||
|
func New(targetFname string) (finalFname string, f *os.File, err error) {
|
||||||
|
var (
|
||||||
|
info os.FileInfo
|
||||||
|
tgt string
|
||||||
|
)
|
||||||
|
finalFname = targetFname
|
||||||
|
for i := 0; i < MaxSymlinkDeref; i++ {
|
||||||
|
info, err = os.Lstat(finalFname)
|
||||||
|
switch {
|
||||||
|
case err != nil && !os.IsNotExist(err):
|
||||||
|
return
|
||||||
|
|
||||||
|
case os.IsNotExist(err), info.Mode().IsRegular():
|
||||||
|
f, err = NewNoDeref(finalFname)
|
||||||
|
return
|
||||||
|
|
||||||
|
case info.Mode()&os.ModeType == os.ModeSymlink:
|
||||||
|
tgt, err = os.Readlink(finalFname)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if filepath.IsAbs(tgt) {
|
||||||
|
finalFname = tgt
|
||||||
|
} else {
|
||||||
|
finalFname = filepath.Clean(filepath.Join(filepath.Dir(finalFname), tgt))
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
err = &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: targetFname,
|
||||||
|
Err: errors.New("not a regular file"),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: targetFname,
|
||||||
|
Err: unix.ELOOP,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abort writing to a file. Performs a Close() and unlinks the temporary file.
|
||||||
|
func Abort(f *os.File) {
|
||||||
|
_ = f.Close()
|
||||||
|
_ = os.Remove(f.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNoDeref is similar to New, but will not dereference symlinks and will
|
||||||
|
// allow them to be overwritten.
|
||||||
|
func NewNoDeref(finalFname string) (*os.File, error) {
|
||||||
|
return ioutil.TempFile(filepath.Dir(finalFname), ".new.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the temporary file. This will close the file f and then rename it
|
||||||
|
// into place once it has been ensured the data is on disk. It will retain
|
||||||
|
// permissions from the original file if present.
|
||||||
|
func Commit(finalFname string, f *os.File) error {
|
||||||
|
if err := f.Sync(); err != nil {
|
||||||
|
os.Remove(f.Name())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the final destination file already exists, try to inherit its
|
||||||
|
// permissions, but don't return an error if we fail
|
||||||
|
if st, err := os.Stat(finalFname); err == nil { // NB: inverted
|
||||||
|
f.Chmod(st.Mode() & os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
os.Remove(f.Name())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(f.Name(), finalFname); err != nil {
|
||||||
|
os.Remove(f.Name())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue