58 lines
1.6 KiB
Go
58 lines
1.6 KiB
Go
// 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)
|
|
}
|