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).
86 lines
2.3 KiB
Go
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
|
|
}
|