Files
bsdaily/internal/bsdaily/verify.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

86 lines
2.3 KiB
Go

package bsdaily
import (
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
)
// killCat terminates the zstdcat process, ignoring the benign case where it
// has already exited (e.g. after receiving SIGPIPE when head closed the pipe)
// and logging any other failure.
func killCat(cmd *exec.Cmd) {
if cmd.Process == nil {
return
}
if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
slog.Warn("failed to kill zstdcat process", "error", err)
}
}
func VerifyOutput(path string) error {
slog.Info("running zstdmt integrity check")
testCmd := exec.Command("zstdmt", "--test", path)
var testStderr strings.Builder
testCmd.Stderr = &testStderr
if err := testCmd.Run(); err != nil {
return fmt.Errorf("zstdmt --test failed: %w; stderr: %s", err, testStderr.String())
}
slog.Info("zstdmt integrity check passed")
slog.Info("verifying SQL content")
catCmd := exec.Command("zstdcat", path)
headCmd := exec.Command("head", fmt.Sprintf("-%d", verificationHeadLines))
pipe, err := catCmd.StdoutPipe()
if err != nil {
return fmt.Errorf("creating zstdcat pipe: %w", err)
}
headCmd.Stdin = pipe
var headOut strings.Builder
headCmd.Stdout = &headOut
if err := catCmd.Start(); err != nil {
return fmt.Errorf("starting zstdcat: %w", err)
}
if err := headCmd.Start(); err != nil {
killCat(catCmd) // Clean up if head fails to start
return fmt.Errorf("starting head: %w", err)
}
// Wait for head first (it will exit when it has enough lines)
if err := headCmd.Wait(); err != nil {
killCat(catCmd)
return fmt.Errorf("head command failed: %w", err)
}
// Kill zstdcat since head closed the pipe (expected SIGPIPE)
killCat(catCmd)
_ = catCmd.Wait() // Reap the process
content := headOut.String()
if len(content) == 0 {
return fmt.Errorf("decompressed content is empty")
}
hasSQLMarker := false
for _, marker := range []string{"BEGIN TRANSACTION", "CREATE TABLE", "INSERT INTO", "PRAGMA"} {
if strings.Contains(content, marker) {
hasSQLMarker = true
break
}
}
const verificationSampleBytes = 200
if !hasSQLMarker {
return fmt.Errorf("decompressed content does not look like SQL; first %d bytes: %s",
verificationSampleBytes, content[:min(verificationSampleBytes, len(content))])
}
slog.Info("SQL content verification passed")
return nil
}