Files
bsdaily/internal/bsdaily/copy.go
T
sneak a256b83734
check / check (push) Successful in 1m42s
Fix errcheck lint failures with proper error handling
Handle every error return flagged by golangci-lint's errcheck rather
than discarding it:

- run.go: add a cleanup() helper that logs a warning (and ignores
  ErrNotExist) when removing a temp file fails, so leaked scratch files
  are surfaced; use it for all best-effort removals.
- copy.go / extract.go: log a warning on deferred Close() failures for
  the source file, destination DB, and result-set rows.
- extract.go: on the rollback path, ignore the benign sql.ErrTxDone
  (already committed) and log any other rollback failure.
- verify.go: add killCat() which ignores os.ErrProcessDone (zstdcat
  already exited via SIGPIPE) and logs any unexpected kill failure.

make check is clean (0 lint issues, tests pass).
2026-06-28 10:25:47 +02:00

77 lines
2.0 KiB
Go

package bsdaily
import (
"fmt"
"io"
"log/slog"
"os"
"time"
)
const (
copyBufferSize = 256 * 1024 * 1024 // 256MB buffer for large file copies from fast storage
oneGB = 1024 * 1024 * 1024
)
func CopyFile(src, dst string) (err error) {
startTime := time.Now()
slog.Info("copying file", "src", src, "dst", dst)
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("opening source %s: %w", src, err)
}
defer func() {
if cerr := srcFile.Close(); cerr != nil {
slog.Warn("failed to close source file", "src", src, "error", cerr)
}
}()
srcInfo, err := srcFile.Stat()
if err != nil {
return fmt.Errorf("stat source %s: %w", src, err)
}
// For large files, advise kernel about sequential read pattern
if srcInfo.Size() > oneGB {
applyFileAdvice(srcFile, srcInfo.Size())
}
dstFile, err := os.Create(dst)
if err != nil {
return fmt.Errorf("creating destination %s: %w", dst, err)
}
defer func() {
if cerr := dstFile.Close(); cerr != nil && err == nil {
err = fmt.Errorf("closing destination %s: %w", dst, cerr)
}
}()
// Pre-allocate space for the destination file to avoid fragmentation
if err := dstFile.Truncate(srcInfo.Size()); err != nil {
slog.Warn("failed to pre-allocate destination file", "error", err)
}
// Use a much larger buffer for NVMe-speed copies
buf := make([]byte, copyBufferSize)
written, err := io.CopyBuffer(dstFile, srcFile, buf)
if err != nil {
return fmt.Errorf("copying data: %w", err)
}
if written != srcInfo.Size() {
return fmt.Errorf("short copy: wrote %d bytes, expected %d", written, srcInfo.Size())
}
if err := dstFile.Sync(); err != nil {
return fmt.Errorf("syncing destination %s: %w", dst, err)
}
elapsed := time.Since(startTime)
throughputMBps := float64(written) / elapsed.Seconds() / (1024 * 1024)
slog.Info("file copied", "dst", dst, "bytes", written,
"elapsed", elapsed.Round(time.Millisecond),
"throughput_mbps", fmt.Sprintf("%.1f", throughputMBps))
return nil
}