Files

76 lines
2.1 KiB
Go
Raw Permalink Normal View History

2026-08-01 20:00:54 +00:00
// Package fileutil provides narrow filesystem helpers for operator-owned outputs.
2026-05-29 20:33:35 +00:00
package fileutil
import (
2026-08-13 02:40:29 +00:00
"context"
2026-05-29 20:33:35 +00:00
"fmt"
"os"
"path/filepath"
)
2026-08-13 02:33:32 +00:00
const (
maxFileNameBytes = 255
temporaryFilePattern = ".weatherreporter-*.tmp"
)
// ValidateAtomicPath verifies that the final path can be safely used with this
2026-08-13 02:40:29 +00:00
// package's same-directory atomic-write implementation. It accepts only an
// absent path or an existing regular file.
2026-08-13 02:33:32 +00:00
func ValidateAtomicPath(path string) error {
if len(filepath.Base(path)) > maxFileNameBytes {
return fmt.Errorf("final file name exceeds the %d-byte limit", maxFileNameBytes)
}
2026-08-13 02:40:29 +00:00
info, err := os.Lstat(path)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("inspect final destination %q: %w", path, err)
}
if !info.Mode().IsRegular() {
return fmt.Errorf("final destination %q must be absent or a regular file", path)
}
2026-08-13 02:33:32 +00:00
return nil
}
2026-05-29 20:33:35 +00:00
func WriteFileAtomic(path string, data []byte) error {
2026-08-13 02:40:29 +00:00
return WriteFileAtomicContext(context.Background(), path, data)
}
// WriteFileAtomicContext writes data through a same-directory temporary file.
// It checks cancellation immediately before the rename that publishes the file.
func WriteFileAtomicContext(ctx context.Context, path string, data []byte) error {
if ctx == nil {
ctx = context.Background()
}
2026-08-13 02:33:32 +00:00
if err := ValidateAtomicPath(path); err != nil {
return err
}
2026-05-29 20:33:35 +00:00
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create directory %q: %w", filepath.Dir(path), err)
}
2026-08-13 02:33:32 +00:00
tmp, err := os.CreateTemp(filepath.Dir(path), temporaryFilePattern)
2026-05-29 20:33:35 +00:00
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)
}
2026-08-13 02:40:29 +00:00
if err := ValidateAtomicPath(path); err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
2026-05-29 20:33:35 +00:00
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("save %q: %w", path, err)
}
return nil
}