You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

62 lines
1.7 KiB

// Package atomicfile persists bounded service snapshots without exposing a
// partially written destination after a process crash.
package atomicfile
import (
"errors"
"fmt"
"os"
"path/filepath"
)
func Write(path string, content []byte, mode os.FileMode) error {
if path == "" {
return errors.New("atomic file path must not be empty")
}
path = filepath.Clean(path)
if info, err := os.Lstat(path); err == nil {
if info.Mode()&os.ModeSymlink != 0 {
return errors.New("refusing to replace a symbolic-link store path")
}
if !info.Mode().IsRegular() {
return errors.New("refusing to replace a non-regular store path")
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("inspect store path: %w", err)
}
directory := filepath.Dir(path)
temporary, err := os.CreateTemp(directory, ".tera-store-*")
if err != nil {
return fmt.Errorf("create temporary store: %w", err)
}
temporaryPath := temporary.Name()
renamed := false
defer func() {
_ = temporary.Close()
if !renamed {
_ = os.Remove(temporaryPath)
}
}()
if err := temporary.Chmod(mode.Perm()); err != nil {
return fmt.Errorf("protect temporary store: %w", err)
}
if _, err := temporary.Write(content); err != nil {
return fmt.Errorf("write temporary store: %w", err)
}
if err := temporary.Sync(); err != nil {
return fmt.Errorf("sync temporary store: %w", err)
}
if err := temporary.Close(); err != nil {
return fmt.Errorf("close temporary store: %w", err)
}
if err := os.Rename(temporaryPath, path); err != nil {
return fmt.Errorf("replace store: %w", err)
}
renamed = true
if err := syncDirectory(directory); err != nil {
return fmt.Errorf("sync store directory: %w", err)
}
return nil
}