check / check (push) Successful in 1m42s
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).
77 lines
2.0 KiB
Go
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
|
|
}
|