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