Files

115 lines
3.2 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
import (
"fmt"
"io"
"os"
"path/filepath"
)
type atomicTempFile interface {
io.Writer
Name() string
Chmod(os.FileMode) error
Sync() error
Close() error
}
var (
atomicMkdirAll = os.MkdirAll
atomicCreateTemp = func(dir, pattern string) (atomicTempFile, error) {
return os.CreateTemp(dir, pattern)
}
atomicRemove = os.Remove
atomicRename = os.Rename
)
// AtomicWrite writes data to path atomically by creating a temp file in the
// same directory, writing and fsyncing the data, then renaming over the target.
// It replaces os.WriteFile for all config and download file writes.
//
// os.WriteFile truncates the target before writing, so a process kill (CI timeout,
// OOM, Ctrl+C) between truncate and completion leaves the file empty or partial.
// AtomicWrite avoids this: on any failure the temp file is cleaned up and the
// original file remains untouched.
func AtomicWrite(path string, data []byte, perm os.FileMode) error {
return atomicWrite(path, perm, func(tmp atomicTempFile) error {
_, err := tmp.Write(data)
return err
})
}
// AtomicWriteFromReader atomically copies reader contents into path.
func AtomicWriteFromReader(path string, reader io.Reader, perm os.FileMode) (int64, error) {
var copied int64
err := atomicWrite(path, perm, func(tmp atomicTempFile) error {
n, err := io.Copy(tmp, reader)
copied = n
return err
})
if err != nil {
return 0, err
}
return copied, nil
}
// AtomicWriteJSON is a convenience wrapper for writing JSON data atomically.
// It uses 0600 permissions by default for sensitive data.
func AtomicWriteJSON(path string, data []byte) error {
return AtomicWrite(path, data, 0600)
}
func atomicWrite(path string, perm os.FileMode, writeFn func(tmp atomicTempFile) error) error {
dir := filepath.Dir(path)
// Ensure directory exists with secure permissions
if err := atomicMkdirAll(dir, 0700); err != nil {
return fmt.Errorf("create directory: %w", err)
}
tmp, err := atomicCreateTemp(dir, "."+filepath.Base(path)+".*.tmp")
if err != nil {
return fmt.Errorf("create temp file: %w", err)
}
tmpName := tmp.Name()
success := false
defer func() {
if !success {
tmp.Close()
atomicRemove(tmpName)
}
}()
if err := tmp.Chmod(perm); err != nil {
return fmt.Errorf("set permissions: %w", err)
}
if err := writeFn(tmp); err != nil {
return fmt.Errorf("write data: %w", err)
}
if err := tmp.Sync(); err != nil {
return fmt.Errorf("sync to disk: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temp file: %w", err)
}
if err := atomicRename(tmpName, path); err != nil {
return fmt.Errorf("rename to final: %w", err)
}
success = true
return nil
}