check / check (push) Waiting to run
Vendor .golangci.yml byte-identical from sneak/prompts at cc440118 and move the Dockerfile lint phase to golangci-lint v2.14.0 by the digest REPO_POLICIES.md names. Fix the code to that config with flags, help text, output files, SQL and the order of steps unchanged; long functions are split into named steps. Judgement call: the extraction transaction is now rolled back on every early return; the old deferred rollback missed most failures and could dereference a nil transaction. Wording change: the reversed-range error reads "--from is after --to (--from X, --to Y)", since err113 requires a wrapped sentinel. Ten //nolint directives (gosec, mnd, unqueryvet), each with its reason. Model: opus-5-5
126 lines
2.9 KiB
Go
126 lines
2.9 KiB
Go
// Package main is the bsdaily command. It extracts one day, or a range
|
|
// of days, from the latest daily snapshot.
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/bsdaily/internal/bsdaily"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
errDateExclusive = errors.New("--date and --from/--to are mutually exclusive")
|
|
errFromRequiresTo = errors.New("--from requires --to")
|
|
errToRequiresFrom = errors.New("--to requires --from")
|
|
errFromAfterTo = errors.New("--from is after --to")
|
|
)
|
|
|
|
func main() {
|
|
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
Level: slog.LevelInfo,
|
|
}))
|
|
slog.SetDefault(logger)
|
|
|
|
var dateFlag, fromFlag, toFlag string
|
|
|
|
rootCmd := &cobra.Command{
|
|
Use: "bsdaily",
|
|
Short: "Extract a single day's data from the latest daily snapshot",
|
|
SilenceUsage: true,
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
targetDates, err := parseTargetDates(dateFlag, fromFlag, toFlag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = bsdaily.Run(targetDates)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
slog.Info("completed successfully")
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
rootCmd.Flags().StringVarP(&dateFlag, "date", "d", "",
|
|
"target date to extract (YYYY-MM-DD); "+
|
|
"defaults to snapshot date minus one day")
|
|
rootCmd.Flags().StringVar(&fromFlag, "from", "",
|
|
"start of date range to extract (YYYY-MM-DD, inclusive); use with --to")
|
|
rootCmd.Flags().StringVar(&toFlag, "to", "",
|
|
"end of date range to extract (YYYY-MM-DD, inclusive); use with --from")
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// parseTargetDates turns the --date, --from and --to flags into the days
|
|
// to extract. It returns nil when none of them is set, which Run takes
|
|
// to mean the snapshot date minus one day.
|
|
func parseTargetDates(dateFlag, fromFlag, toFlag string) ([]time.Time, error) {
|
|
hasDate := dateFlag != ""
|
|
hasFrom := fromFlag != ""
|
|
hasTo := toFlag != ""
|
|
|
|
// Validate mutual exclusivity
|
|
if hasDate && (hasFrom || hasTo) {
|
|
return nil, errDateExclusive
|
|
}
|
|
|
|
if hasFrom != hasTo {
|
|
if hasFrom {
|
|
return nil, errFromRequiresTo
|
|
}
|
|
|
|
return nil, errToRequiresFrom
|
|
}
|
|
|
|
if hasDate {
|
|
t, err := time.Parse("2006-01-02", dateFlag)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"invalid --date %q (expected YYYY-MM-DD): %w", dateFlag, err)
|
|
}
|
|
|
|
return []time.Time{t}, nil
|
|
}
|
|
|
|
if !hasFrom {
|
|
return nil, nil
|
|
}
|
|
|
|
from, err := time.Parse("2006-01-02", fromFlag)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"invalid --from %q (expected YYYY-MM-DD): %w", fromFlag, err)
|
|
}
|
|
|
|
to, err := time.Parse("2006-01-02", toFlag)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"invalid --to %q (expected YYYY-MM-DD): %w", toFlag, err)
|
|
}
|
|
|
|
if from.After(to) {
|
|
return nil, fmt.Errorf("%w (--from %s, --to %s)",
|
|
errFromAfterTo, fromFlag, toFlag)
|
|
}
|
|
|
|
var targetDates []time.Time
|
|
|
|
for d := from; !d.After(to); d = d.AddDate(0, 0, 1) {
|
|
targetDates = append(targetDates, d)
|
|
}
|
|
|
|
return targetDates, nil
|
|
}
|