129 lines
3.6 KiB
Go
129 lines
3.6 KiB
Go
package fileops
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// WriteFileAtomic writes data to dst atomically via temp file + rename.
|
|
func WriteFileAtomic(dst string, data []byte, perm os.FileMode) error {
|
|
if strings.TrimSpace(dst) == "" {
|
|
return fmt.Errorf("destination path is required")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
return fmt.Errorf("create destination directory: %w", err)
|
|
}
|
|
|
|
base := filepath.Base(dst)
|
|
tmp, err := os.CreateTemp(filepath.Dir(dst), "."+base+".tmp-*")
|
|
if err != nil {
|
|
return fmt.Errorf("create temp file: %w", err)
|
|
}
|
|
tmpPath := tmp.Name()
|
|
removeTmp := true
|
|
defer func() {
|
|
if removeTmp {
|
|
_ = os.Remove(tmpPath)
|
|
}
|
|
}()
|
|
|
|
if _, err := tmp.Write(data); err != nil {
|
|
_ = tmp.Close()
|
|
return fmt.Errorf("write temp file: %w", err)
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
_ = tmp.Close()
|
|
return fmt.Errorf("sync temp file: %w", err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return fmt.Errorf("close temp file: %w", err)
|
|
}
|
|
if err := os.Chmod(tmpPath, perm); err != nil {
|
|
return fmt.Errorf("set temp file permissions: %w", err)
|
|
}
|
|
if err := os.Rename(tmpPath, dst); err != nil {
|
|
return fmt.Errorf("install temp file: %w", err)
|
|
}
|
|
removeTmp = false
|
|
return nil
|
|
}
|
|
|
|
// CopyFileAtomic copies src to dst atomically via temp file + rename.
|
|
func CopyFileAtomic(src, dst string, perm os.FileMode) error {
|
|
_, err := CopyFileAtomicWithChecksum(src, dst, perm)
|
|
return err
|
|
}
|
|
|
|
// CopyFileAtomicWithChecksum copies src to dst atomically and returns the SHA-256 checksum.
|
|
func CopyFileAtomicWithChecksum(src, dst string, perm os.FileMode) (string, error) {
|
|
if strings.TrimSpace(src) == "" || strings.TrimSpace(dst) == "" {
|
|
return "", fmt.Errorf("source and destination paths are required")
|
|
}
|
|
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return "", fmt.Errorf("open source file: %w", err)
|
|
}
|
|
defer func() { _ = in.Close() }()
|
|
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
return "", fmt.Errorf("create destination directory: %w", err)
|
|
}
|
|
|
|
base := filepath.Base(dst)
|
|
tmp, err := os.CreateTemp(filepath.Dir(dst), "."+base+".tmp-*")
|
|
if err != nil {
|
|
return "", fmt.Errorf("create temp file: %w", err)
|
|
}
|
|
tmpPath := tmp.Name()
|
|
removeTmp := true
|
|
defer func() {
|
|
if removeTmp {
|
|
_ = os.Remove(tmpPath)
|
|
}
|
|
}()
|
|
|
|
digest := sha256.New()
|
|
if _, err := io.Copy(io.MultiWriter(tmp, digest), in); err != nil {
|
|
_ = tmp.Close()
|
|
return "", fmt.Errorf("copy file: %w", err)
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
_ = tmp.Close()
|
|
return "", fmt.Errorf("sync temp file: %w", err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return "", fmt.Errorf("close temp file: %w", err)
|
|
}
|
|
if err := os.Chmod(tmpPath, perm); err != nil {
|
|
return "", fmt.Errorf("set temp file permissions: %w", err)
|
|
}
|
|
if err := os.Rename(tmpPath, dst); err != nil {
|
|
return "", fmt.Errorf("install temp file: %w", err)
|
|
}
|
|
removeTmp = false
|
|
return hex.EncodeToString(digest.Sum(nil)), nil
|
|
}
|
|
|
|
// InstallDownloadedTempFile installs a previously downloaded temp file at dst.
|
|
func InstallDownloadedTempFile(tmpPath, dst string, perm os.FileMode) error {
|
|
if strings.TrimSpace(tmpPath) == "" || strings.TrimSpace(dst) == "" {
|
|
return fmt.Errorf("temp and destination paths are required")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
return fmt.Errorf("create destination directory: %w", err)
|
|
}
|
|
if err := os.Chmod(tmpPath, perm); err != nil {
|
|
return fmt.Errorf("set temp file permissions: %w", err)
|
|
}
|
|
if err := os.Rename(tmpPath, dst); err != nil {
|
|
return fmt.Errorf("install downloaded file: %w", err)
|
|
}
|
|
return nil
|
|
}
|