Add deployment-managed cache writer
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user