forked from sneak/bsdaily
Add a detailed README, WTFPL LICENSE, and build/CI tooling modeled on the vaultik repo (Makefile, multi-stage digest-pinned Dockerfile, .gitea/workflows/check.yml). Bump Go to 1.26.4 and pin golangci-lint to v2.12.2. gofmt existing sources so the new fmt-check gate passes.
73 lines
1.8 KiB
Go
73 lines
1.8 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 srcFile.Close()
|
|
|
|
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
|
|
}
|