Add deployment-managed cache writer

This commit is contained in:
2026-09-11 14:59:27 +00:00
parent 7d9adc77ee
commit 153b1e4689
4 changed files with 273 additions and 0 deletions

View File

@@ -2,7 +2,10 @@
package fileio
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path"
@@ -92,6 +95,16 @@ func WriteJSON(root, name string, payload any, dirMode, fileMode os.FileMode) er
return WriteBytes(root, name, append(data, '\n'), dirMode, fileMode)
}
// WriteJSONWithProcessPermissions writes indented JSON beneath root using the
// process and filesystem creation policy for new directories and files.
func WriteJSONWithProcessPermissions(root, name string, payload any) error {
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return fmt.Errorf("marshal artifact %q: %w", name, err)
}
return WriteBytesWithProcessPermissions(root, name, append(data, '\n'))
}
func WriteBytes(root, name string, data []byte, dirMode, fileMode os.FileMode) error {
target, err := SafePath(root, name)
if err != nil {
@@ -109,6 +122,26 @@ func WriteBytes(root, name string, data []byte, dirMode, fileMode os.FileMode) e
return nil
}
// WriteBytesWithProcessPermissions atomically writes data beneath root while
// allowing the process umask and filesystem default ACLs to select effective
// permissions for newly created entries.
func WriteBytesWithProcessPermissions(root, name string, data []byte) error {
target, err := SafePath(root, name)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(target), 0o777); err != nil {
return fmt.Errorf("write artifact %q: %w", name, err)
}
if err := rejectSymlinkComponents(root, name); err != nil {
return err
}
if err := writeAtomicWithProcessPermissions(target, data); err != nil {
return fmt.Errorf("write artifact %q: %w", name, err)
}
return nil
}
func rejectSymlinkComponents(root, name string) error {
absRoot, err := filepath.Abs(root)
if err != nil {
@@ -163,3 +196,51 @@ func writeAtomic(target string, data []byte, fileMode os.FileMode) error {
keep = false
return nil
}
func writeAtomicWithProcessPermissions(target string, data []byte) error {
directory := filepath.Dir(target)
base := filepath.Base(target)
seen := make(map[string]struct{}, 32)
for attempt := 0; attempt < 32; attempt++ {
suffix := make([]byte, 16)
if _, err := rand.Read(suffix); err != nil {
return fmt.Errorf("generate temporary file name: %w", err)
}
temporaryPath := filepath.Join(directory, "."+base+".tmp-"+hex.EncodeToString(suffix))
if _, exists := seen[temporaryPath]; exists {
continue
}
seen[temporaryPath] = struct{}{}
temporary, err := os.OpenFile(temporaryPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
if errors.Is(err, os.ErrExist) {
continue
}
if err != nil {
return err
}
return writeAndPublish(temporary, temporaryPath, target, data)
}
return fmt.Errorf("create temporary file for %q: exhausted unique names", target)
}
func writeAndPublish(temporary *os.File, temporaryPath, target string, data []byte) error {
keep := true
defer func() {
if keep {
_ = os.Remove(temporaryPath)
}
}()
if _, err := temporary.Write(data); err != nil {
_ = temporary.Close()
return err
}
if err := temporary.Close(); err != nil {
return err
}
if err := os.Rename(temporaryPath, target); err != nil {
return err
}
keep = false
return nil
}

View File

@@ -0,0 +1,131 @@
//go:build linux || darwin
package fileio
import (
"os"
"path/filepath"
"strings"
"syscall"
"testing"
)
func TestWriteBytesWithProcessPermissionsHonorsUmask(t *testing.T) {
for _, test := range []struct {
name string
umask int
dirMode os.FileMode
fileMode os.FileMode
}{
{name: "private", umask: 0o077, dirMode: 0o700, fileMode: 0o600},
{name: "group", umask: 0o007, dirMode: 0o770, fileMode: 0o660},
} {
t.Run(test.name, func(t *testing.T) {
previousUmask := syscall.Umask(test.umask)
defer syscall.Umask(previousUmask)
root := filepath.Join(t.TempDir(), "cache")
if err := WriteBytesWithProcessPermissions(root, "nested/value", []byte("value")); err != nil {
t.Fatal(err)
}
for path, want := range map[string]os.FileMode{
root: test.dirMode,
filepath.Join(root, "nested"): test.dirMode,
filepath.Join(root, "nested", "value"): test.fileMode,
} {
assertPermissions(t, path, want)
}
assertNoTemporarySiblings(t, filepath.Join(root, "nested"))
})
}
}
func TestWriteBytesWithProcessPermissionsLeavesExistingRootUntouched(t *testing.T) {
previousUmask := syscall.Umask(0o077)
defer syscall.Umask(previousUmask)
root := filepath.Join(t.TempDir(), "cache")
syscall.Umask(0)
if err := os.Mkdir(root, 0o751); err != nil {
t.Fatal(err)
}
syscall.Umask(0o077)
if err := WriteBytesWithProcessPermissions(root, "nested/value", []byte("value")); err != nil {
t.Fatal(err)
}
assertPermissions(t, root, 0o751)
}
func TestWriteBytesWithProcessPermissionsReplacementUsesCurrentPolicy(t *testing.T) {
root := filepath.Join(t.TempDir(), "cache")
previousUmask := syscall.Umask(0o077)
defer syscall.Umask(previousUmask)
if err := WriteBytesWithProcessPermissions(root, "value", []byte("before")); err != nil {
t.Fatal(err)
}
before, err := os.Stat(filepath.Join(root, "value"))
if err != nil {
t.Fatal(err)
}
syscall.Umask(0o007)
if err := WriteBytesWithProcessPermissions(root, "value", []byte("after")); err != nil {
t.Fatal(err)
}
after, err := os.Stat(filepath.Join(root, "value"))
if err != nil {
t.Fatal(err)
}
if before.Sys().(*syscall.Stat_t).Ino == after.Sys().(*syscall.Stat_t).Ino {
t.Fatal("replacement retained the old inode")
}
assertPermissions(t, filepath.Join(root, "value"), 0o660)
data, err := os.ReadFile(filepath.Join(root, "value"))
if err != nil {
t.Fatal(err)
}
if string(data) != "after" {
t.Fatalf("replacement content = %q, want %q", data, "after")
}
}
func TestWriteJSONWithProcessPermissionsFormatsJSON(t *testing.T) {
previousUmask := syscall.Umask(0o077)
defer syscall.Umask(previousUmask)
root := filepath.Join(t.TempDir(), "cache")
if err := WriteJSONWithProcessPermissions(root, "value.json", map[string]string{"value": "ok"}); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Join(root, "value.json"))
if err != nil {
t.Fatal(err)
}
if string(data) != "{\n \"value\": \"ok\"\n}\n" {
t.Fatalf("JSON = %q", data)
}
}
func assertPermissions(t *testing.T, path string, want os.FileMode) {
t.Helper()
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if got := info.Mode().Perm(); got != want {
t.Fatalf("%s mode = %#o, want %#o", path, got, want)
}
}
func assertNoTemporarySiblings(t *testing.T, directory string) {
t.Helper()
entries, err := os.ReadDir(directory)
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
if strings.Contains(entry.Name(), ".tmp-") {
t.Fatalf("temporary file remains: %s", entry.Name())
}
}
}