118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
package firewall
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// atomicFile stages an atomic replacement of a config file. It creates a temp
|
|
// file in the destination's directory and captures the destination's ownership
|
|
// and permission mode up front; Commit restores that metadata onto the temp
|
|
// file before renaming it into place. A destination that does not yet exist
|
|
// falls back to defaultMode with the temp file's default ownership.
|
|
//
|
|
// The struct embeds *os.File so callers stream into it with the usual Write and
|
|
// fmt.Fprintln calls, whether they buffer the whole file or scan the original
|
|
// and rewrite line by line.
|
|
type atomicFile struct {
|
|
*os.File
|
|
dst string
|
|
tmp string
|
|
mode os.FileMode
|
|
uid, gid int
|
|
chown bool // True only when the destination already existed.
|
|
done bool
|
|
}
|
|
|
|
// newAtomicFile opens a temp file next to dst for streaming. The caller writes
|
|
// to the returned handle, then calls Commit to install it or Abort to discard
|
|
// it. Capturing the destination's mode and ownership here means a later
|
|
// scan-and-rewrite of the original still commits with the original's metadata.
|
|
func newAtomicFile(dst string, defaultMode os.FileMode) (*atomicFile, error) {
|
|
mode := defaultMode
|
|
var uid, gid int
|
|
var chown bool
|
|
if fi, err := os.Stat(dst); err == nil {
|
|
mode = fi.Mode().Perm()
|
|
uid, gid, chown = statOwner(fi)
|
|
}
|
|
fd, err := os.CreateTemp(filepath.Dir(dst), filepath.Base(dst)+".tmp.*")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &atomicFile{
|
|
File: fd,
|
|
dst: dst,
|
|
tmp: fd.Name(),
|
|
mode: mode,
|
|
uid: uid,
|
|
gid: gid,
|
|
chown: chown,
|
|
}, nil
|
|
}
|
|
|
|
// Commit flushes, applies the captured mode and ownership to the temp file, and
|
|
// renames it over the destination. Ownership is best-effort: a caller that can
|
|
// write the file but cannot chown it keeps the temp file's owner and still gets
|
|
// the original mode.
|
|
func (a *atomicFile) Commit() error {
|
|
if a.done {
|
|
return nil
|
|
}
|
|
// Apply mode and ownership through the fd so the staged file is never
|
|
// briefly installed with the wrong permissions.
|
|
if err := a.File.Chmod(a.mode); err != nil {
|
|
return a.fail(err)
|
|
}
|
|
if a.chown {
|
|
if err := a.File.Chown(a.uid, a.gid); err != nil && !errors.Is(err, os.ErrPermission) {
|
|
return a.fail(err)
|
|
}
|
|
}
|
|
if err := a.File.Close(); err != nil {
|
|
return a.fail(err)
|
|
}
|
|
// Install the staged file.
|
|
if err := os.Rename(a.tmp, a.dst); err != nil {
|
|
_ = os.Remove(a.tmp)
|
|
a.done = true
|
|
return err
|
|
}
|
|
a.done = true
|
|
return nil
|
|
}
|
|
|
|
// Abort discards the temp file. It is a no-op after a successful Commit, so a
|
|
// caller may defer Abort immediately after opening.
|
|
func (a *atomicFile) Abort() {
|
|
if a.done {
|
|
return
|
|
}
|
|
_ = a.File.Close()
|
|
_ = os.Remove(a.tmp)
|
|
a.done = true
|
|
}
|
|
|
|
// fail closes and removes the temp file, then returns the triggering error.
|
|
func (a *atomicFile) fail(err error) error {
|
|
_ = a.File.Close()
|
|
_ = os.Remove(a.tmp)
|
|
a.done = true
|
|
return err
|
|
}
|
|
|
|
// writeConfigFile atomically replaces path with data for callers that already
|
|
// hold the whole file in memory, preserving the existing file's mode and
|
|
// ownership (falling back to defaultMode for a file that does not yet exist).
|
|
func writeConfigFile(path string, data []byte, defaultMode os.FileMode) error {
|
|
af, err := newAtomicFile(path, defaultMode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer af.Abort()
|
|
if _, err := af.Write(data); err != nil {
|
|
return err
|
|
}
|
|
return af.Commit()
|
|
}
|