96 lines
3.8 KiB
Go

// Command wallet-outbox-archive-restore validates one downloaded COS batch and,
// only after _SUCCESS and every checksum pass, writes it to isolation tables.
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"strings"
"time"
"hyapp/services/wallet-service/internal/outboxarchiverestore"
)
const defaultDSNEnv = "WALLET_OUTBOX_ARCHIVE_RESTORE_DSN"
func main() {
dataPath := flag.String("data", "", "local data.ndjson.gz downloaded from the private archive bucket")
manifestPath := flag.String("manifest", "", "local manifest.json from the same batch directory")
successPath := flag.String("success", "", "local _SUCCESS from the same batch directory")
restore := flag.Bool("restore", false, "persist the verified batch into fixed isolation tables")
dsnEnv := flag.String("dsn-env", defaultDSNEnv, "environment variable containing the restore-target MySQL DSN")
expectedDatabase := flag.String("expected-database", "", "exact restore-target database returned by SELECT DATABASE()")
rehearsalID := flag.String("rehearsal-id", "", "stable 1..64 character restore rehearsal key")
confirmation := flag.String("confirm", "", "restore confirmation RESTORE:<rehearsal-id>:<batch-id>")
timeout := flag.Duration("timeout", 10*time.Minute, "whole verification/restore timeout, at most 30m")
flag.Parse()
if strings.TrimSpace(*dataPath) == "" || strings.TrimSpace(*manifestPath) == "" || strings.TrimSpace(*successPath) == "" {
fatal(errors.New("--data, --manifest, and --success are required"))
}
if *timeout <= 0 || *timeout > 30*time.Minute {
fatal(errors.New("--timeout must be within (0,30m]"))
}
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
verification, err := outboxarchiverestore.VerifyLocalFiles(*dataPath, *manifestPath, *successPath, outboxarchiverestore.DefaultLimits())
if err != nil {
fatal(err)
}
if !*restore {
printJSON(map[string]any{
"state": "FILES_VERIFIED", "batch_id": verification.Manifest.BatchID,
"app_code": verification.Manifest.AppCode, "row_count": verification.Manifest.RowCount,
"data_object_key": verification.Manifest.DataObjectKey,
"first_cursor": verification.Manifest.FirstCursor, "last_cursor": verification.Manifest.LastCursor,
"ndjson_sha256": verification.Manifest.NDJSONSHA256, "gzip_sha256": verification.Manifest.GzipSHA256,
"data_crc64": verification.Manifest.DataCRC64,
"manifest_sha256": verification.ManifestSHA256, "manifest_crc64": verification.ManifestCRC64,
"success_sha256": verification.SuccessSHA256, "success_crc64": verification.SuccessCRC64,
"available_delta_total": verification.Manifest.AvailableDeltaTotal,
"frozen_delta_total": verification.Manifest.FrozenDeltaTotal, "no_database_writes": true,
})
return
}
rehearsal := strings.TrimSpace(*rehearsalID)
expectedConfirmation := "RESTORE:" + rehearsal + ":" + verification.Manifest.BatchID
if rehearsal == "" || strings.TrimSpace(*confirmation) != expectedConfirmation {
fatal(fmt.Errorf("--restore requires --rehearsal-id and --confirm=%s", expectedConfirmation))
}
envName := strings.TrimSpace(*dsnEnv)
if envName == "" {
fatal(errors.New("--dsn-env cannot be empty"))
}
dsn := strings.TrimSpace(os.Getenv(envName))
if dsn == "" {
fatal(fmt.Errorf("restore DSN environment variable %s is empty", envName))
}
result, err := outboxarchiverestore.Restore(ctx, dsn, *expectedDatabase, rehearsal, verification)
if err != nil {
fatal(err)
}
printJSON(struct {
outboxarchiverestore.RestoreResult
DSNEnv string `json:"dsn_env"`
}{RestoreResult: result, DSNEnv: envName})
}
func printJSON(value any) {
body, err := json.Marshal(value)
if err != nil {
fatal(err)
}
fmt.Println(string(body))
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}