// Package fileutil provides narrow filesystem helpers for operator-owned outputs. package fileutil import ( "encoding/json" "fmt" "os" "path/filepath" ) const ( maxFileNameBytes = 255 temporaryFilePattern = ".weatherreporter-*.tmp" ) // ValidateAtomicPath verifies that the final path can be safely used with this // package's same-directory atomic-write implementation. func ValidateAtomicPath(path string) error { if len(filepath.Base(path)) > maxFileNameBytes { return fmt.Errorf("final file name exceeds the %d-byte limit", maxFileNameBytes) } return nil } func WriteFileAtomic(path string, data []byte) error { if err := ValidateAtomicPath(path); err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return fmt.Errorf("create directory %q: %w", filepath.Dir(path), err) } tmp, err := os.CreateTemp(filepath.Dir(path), temporaryFilePattern) if err != nil { return fmt.Errorf("create temporary file for %q: %w", path, err) } tmpName := tmp.Name() defer os.Remove(tmpName) if _, err := tmp.Write(data); err != nil { tmp.Close() return fmt.Errorf("write temporary file for %q: %w", path, err) } if err := tmp.Close(); err != nil { return fmt.Errorf("close temporary file for %q: %w", path, err) } if err := os.Rename(tmpName, path); err != nil { return fmt.Errorf("save %q: %w", path, err) } return nil } func WriteJSONAtomic(path string, value any) error { data, err := json.MarshalIndent(value, "", " ") if err != nil { return fmt.Errorf("marshal %q: %w", path, err) } return WriteFileAtomic(path, data) }