writefile/writefile.go

117 lines
2.9 KiB
Go

/*
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
}