// 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::") 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) }