package bsdaily import ( "errors" "fmt" "io" "log/slog" "os" "time" ) const ( // 256MB buffer for large file copies from fast storage copyBufferSize = 256 * 1024 * 1024 oneMB = 1024 * 1024 oneGB = 1024 * 1024 * 1024 ) var errShortCopy = errors.New("short copy") // CopyFile copies src to dst through a large buffer, pre-allocating dst // and syncing it to disk before returning. func CopyFile(src, dst string) (err error) { startTime := time.Now() slog.Info("copying file", "src", src, "dst", dst) //nolint:gosec // src is a path this package built srcFile, err := os.Open(src) if err != nil { return fmt.Errorf("opening source %s: %w", src, err) } defer func() { cerr := srcFile.Close() if 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()) } //nolint:gosec // dst is a path this package built dstFile, err := os.Create(dst) if err != nil { return fmt.Errorf("creating destination %s: %w", dst, err) } defer func() { cerr := dstFile.Close() if cerr != nil && err == nil { err = fmt.Errorf("closing destination %s: %w", dst, cerr) } }() // Pre-allocate space for the destination file to avoid fragmentation truncErr := dstFile.Truncate(srcInfo.Size()) if truncErr != nil { slog.Warn("failed to pre-allocate destination file", "error", truncErr) } // 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("%w: wrote %d bytes, expected %d", errShortCopy, written, srcInfo.Size()) } err = dstFile.Sync() if err != nil { return fmt.Errorf("syncing destination %s: %w", dst, err) } elapsed := time.Since(startTime) throughputMBps := float64(written) / elapsed.Seconds() / oneMB slog.Info("file copied", "dst", dst, "bytes", written, "elapsed", elapsed.Round(time.Millisecond), "throughput_mbps", fmt.Sprintf("%.1f", throughputMBps)) return nil }