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 := EnsureWorkspaceDirectory(filepath.Dir(dst)); 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 := EnsureWorkspaceDirectory(filepath.Dir(dst)); 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 := EnsureWorkspaceDirectory(filepath.Dir(dst)); 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 }